From b8fe71d5a2812dec3d0889eec49777ef4409c9cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=B0=B8=E5=BC=BA?= <11704063+s-yongqiang@user.noreply.gitee.com> Date: Tue, 15 Apr 2025 09:48:44 +0800 Subject: [PATCH] tags migrate handle exist tag name --- .../src/components/dialog/lib-settings.js | 8 +- .../dir-view-mode/dir-column-nav/index.js | 7 +- .../dir-view-mode/dir-column-view.js | 1 + .../dir-view-mode/dir-others/index.js | 14 +- .../src/components/repo-info-bar-migrate.js | 6 +- .../metadata-tags-status-dialog/index.js | 25 ++- .../lib-content-view/lib-content-view.js | 1 + frontend/src/tag/api.js | 8 + seahub/repo_metadata/apis.py | 177 +++++++++++++++++- seahub/repo_metadata/metadata_server_api.py | 20 ++ seahub/repo_metadata/urls.py | 3 +- seahub/repo_metadata/utils.py | 5 + 12 files changed, 257 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/dialog/lib-settings.js b/frontend/src/components/dialog/lib-settings.js index 1078cc0d3b..3f6eddac1d 100644 --- a/frontend/src/components/dialog/lib-settings.js +++ b/frontend/src/components/dialog/lib-settings.js @@ -22,12 +22,12 @@ const { enableSeafileAI, enableSeafileOCR } = window.app.config; const propTypes = { toggleDialog: PropTypes.func.isRequired, repoID: PropTypes.string.isRequired, - currentRepoInfo: PropTypes.object.isRequired + currentRepoInfo: PropTypes.object.isRequired, + usedRepoTags: PropTypes.array, }; -const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab, showMigrateTip }) => { +const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab, showMigrateTip, usedRepoTags, onMigrateSuccess }) => { const [activeTab, setActiveTab] = useState(tab || TAB.HISTORY_SETTING); - const toggleTab = useCallback((tab) => { setActiveTab(tab); }, []); @@ -202,6 +202,8 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab, showMig toggleDialog={toggleDialog} enableMetadata={enableMetadata} showMigrateTip={showMigrateTip} + usedRepoTags={usedRepoTags} + onMigrateSuccess={onMigrateSuccess} /> )} diff --git a/frontend/src/components/dir-view-mode/dir-column-nav/index.js b/frontend/src/components/dir-view-mode/dir-column-nav/index.js index 9afedb4252..9cb8222a42 100644 --- a/frontend/src/components/dir-view-mode/dir-column-nav/index.js +++ b/frontend/src/components/dir-view-mode/dir-column-nav/index.js @@ -33,6 +33,7 @@ const propTypes = { getMenuContainerSize: PropTypes.func, updateDirent: PropTypes.func, updateTreeNode: PropTypes.func, + usedRepoTags: PropTypes.array, }; class DirColumnNav extends React.Component { @@ -43,7 +44,7 @@ class DirColumnNav extends React.Component { render() { const { - isTreeDataLoading, userPerm, treeData, repoID, currentPath, currentRepoInfo, navRate = 0.25 + isTreeDataLoading, userPerm, treeData, repoID, currentPath, currentRepoInfo, navRate = 0.25, usedRepoTags } = this.props; const flex = navRate ? '0 0 ' + navRate * 100 + '%' : '0 0 25%'; const select = this.props.inResizing ? 'none' : ''; @@ -74,9 +75,9 @@ class DirColumnNav extends React.Component { updateDirent={this.props.updateDirent} updateTreeNode={this.props.updateTreeNode} /> - + - + )} diff --git a/frontend/src/components/dir-view-mode/dir-column-view.js b/frontend/src/components/dir-view-mode/dir-column-view.js index aef5a78696..aaef405051 100644 --- a/frontend/src/components/dir-view-mode/dir-column-view.js +++ b/frontend/src/components/dir-view-mode/dir-column-view.js @@ -185,6 +185,7 @@ class DirColumnView extends React.Component { direntList={this.props.direntList} updateDirent={this.props.updateDirent} updateTreeNode={this.props.updateTreeNode} + usedRepoTags={this.props.usedRepoTags} /> { +const DirOthers = ({ userPerm, repoID, currentRepoInfo, usedRepoTags }) => { const showSettings = currentRepoInfo.is_admin; // repo owner, department admin, shared with 'Admin' permission + const repoName = currentRepoInfo.repo_name; let [isSettingsDialogOpen, setSettingsDialogOpen] = useState(false); let [activeTab, setActiveTab] = useState(TAB.HISTORY_SETTING); let [showMigrateTip, setShowMigrateTip] = useState(false); @@ -23,6 +24,12 @@ const DirOthers = ({ userPerm, repoID, currentRepoInfo }) => { setSettingsDialogOpen(!isSettingsDialogOpen); }; + const handleMigrateSuccess = useCallback(() => { + setShowMigrateTip(false); + const serviceUrl = window.app.config.serviceURL; + window.location.href = serviceUrl + '/library/' + repoID + '/' + repoName + '/?tag=__all_tags'; + }, [repoID, repoName]); + useEffect(() => { const unsubscribeUnselectFiles = eventBus.subscribe(EVENT_BUS_TYPE.OPEN_LIBRARY_SETTINGS_TAGS, () => { setSettingsDialogOpen(true); @@ -79,6 +86,8 @@ const DirOthers = ({ userPerm, repoID, currentRepoInfo }) => { toggleDialog={toggleSettingsDialog} tab={activeTab} showMigrateTip={showMigrateTip} + usedRepoTags={usedRepoTags} + onMigrateSuccess={handleMigrateSuccess} /> )} {isRepoHistoryDialogOpen && ( @@ -97,6 +106,7 @@ DirOthers.propTypes = { userPerm: PropTypes.string, repoID: PropTypes.string, currentRepoInfo: PropTypes.object.isRequired, + usedRepoTags: PropTypes.array, }; export default DirOthers; diff --git a/frontend/src/components/repo-info-bar-migrate.js b/frontend/src/components/repo-info-bar-migrate.js index 8a47b4df86..bc2bd3f4bc 100644 --- a/frontend/src/components/repo-info-bar-migrate.js +++ b/frontend/src/components/repo-info-bar-migrate.js @@ -7,15 +7,15 @@ import { EVENT_BUS_TYPE } from '../components/common/event-bus-type'; const RepoInfoBarMigrate = () => { - const { enableMetadata } = useMetadataStatus(); - + const { enableMetadata, detailsSettings } = useMetadataStatus(); + const tagsMigrated = detailsSettings?.tags_migrated; const openMigrate = () => { eventBus.dispatch(EVENT_BUS_TYPE.OPEN_TREE_PANEL, () => eventBus.dispatch(EVENT_BUS_TYPE.OPEN_LIBRARY_SETTINGS_TAGS)); }; return (
- {enableMetadata ? + {enableMetadata && !tagsMigrated ? ( <> {gettext('Tips: There are tags of old version. Please migrate tags to new version.')} diff --git a/frontend/src/metadata/components/dialog/metadata-tags-status-dialog/index.js b/frontend/src/metadata/components/dialog/metadata-tags-status-dialog/index.js index 1c058fc9b3..af02775952 100644 --- a/frontend/src/metadata/components/dialog/metadata-tags-status-dialog/index.js +++ b/frontend/src/metadata/components/dialog/metadata-tags-status-dialog/index.js @@ -19,10 +19,11 @@ const langOptions = [ } ]; -const MetadataTagsStatusDialog = ({ value: oldValue, lang: oldLang, repoID, toggleDialog: toggle, submit, enableMetadata, showMigrateTip }) => { +const MetadataTagsStatusDialog = ({ value: oldValue, lang: oldLang, repoID, toggleDialog: toggle, submit, enableMetadata, showMigrateTip, usedRepoTags, onMigrateSuccess }) => { const [value, setValue] = useState(oldValue); const [lang, setLang] = useState(oldLang); const [submitting, setSubmitting] = useState(false); + const [migrated, setMigrated] = useState(false); const [showTurnOffConfirmDialog, setShowTurnOffConfirmDialog] = useState(false); const onToggle = useCallback(() => { @@ -46,8 +47,16 @@ const MetadataTagsStatusDialog = ({ value: oldValue, lang: oldLang, repoID, togg }, [lang, repoID, submit, toggle, value]); const migrateTag = useCallback(() => { - // TODO backend migrate old tags - }, []); + tagsAPI.migrateTags(repoID, usedRepoTags).then(res => { + setMigrated(true); + toaster.success(gettext('Tags migrated successfully')); + onMigrateSuccess && onMigrateSuccess(); + }).catch(error => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + setMigrated(false); + }); + }, [repoID, usedRepoTags, onMigrateSuccess]); const turnOffConfirmToggle = useCallback(() => { setShowTurnOffConfirmDialog(!showTurnOffConfirmDialog); @@ -110,7 +119,13 @@ const MetadataTagsStatusDialog = ({ value: oldValue, lang: oldLang, repoID, togg {showMigrateTip &&

{gettext('This library contains tags of old version. Do you like to migrate the tags to new version?')}

- +
} @@ -136,6 +151,8 @@ MetadataTagsStatusDialog.propTypes = { submit: PropTypes.func.isRequired, enableMetadata: PropTypes.bool.isRequired, showMigrateTip: PropTypes.bool, + repoTags: PropTypes.array, + onMigrateSuccess: PropTypes.func, }; export default MetadataTagsStatusDialog; diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index 39087421b2..3d0c133498 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -2350,6 +2350,7 @@ class LibContentView extends React.Component { if (!currentDirent && currentMode !== METADATA_MODE && currentMode !== TAGS_MODE) { detailPath = Utils.getDirName(this.state.path); } + const detailDirent = currentDirent || currentNode?.object || null; return ( diff --git a/frontend/src/tag/api.js b/frontend/src/tag/api.js index 1652c05d39..8e29f701cb 100644 --- a/frontend/src/tag/api.js +++ b/frontend/src/tag/api.js @@ -138,6 +138,14 @@ class TagsManagerAPI { return this.req.post(url, params); }; + migrateTags = (repoID, usedRepoTags) => { + const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/migrate-tags/'; + const params = { + tag_ids: usedRepoTags.map(tag => tag.id), + }; + return this.req.post(url, params); + }; + } const tagsAPI = new TagsManagerAPI(); diff --git a/seahub/repo_metadata/apis.py b/seahub/repo_metadata/apis.py index b14fdcb40a..18a73fa77a 100644 --- a/seahub/repo_metadata/apis.py +++ b/seahub/repo_metadata/apis.py @@ -1,6 +1,7 @@ import json import logging import os +import posixpath from datetime import datetime from rest_framework.authentication import SessionAuthentication @@ -17,11 +18,12 @@ from seahub.repo_metadata.utils import add_init_metadata_task, recognize_faces, get_unmodifiable_columns, can_read_metadata, init_faces, \ extract_file_details, get_table_by_name, remove_faces_table, FACES_SAVE_PATH, \ init_tags, init_tag_self_link_columns, remove_tags_table, add_init_face_recognition_task, init_ocr, \ - remove_ocr_column, get_update_record, update_people_cover_photo -from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records + remove_ocr_column, get_update_record, update_people_cover_photo, gen_unique_tag_name +from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records, list_eligible_metadata_records from seahub.utils.repo import is_repo_admin from seaserv import seafile_api from seahub.repo_metadata.constants import FACE_RECOGNITION_VIEW_ID, METADATA_RECORD_UPDATE_LIMIT +from seahub.file_tags.models import FileTags logger = logging.getLogger(__name__) @@ -2780,3 +2782,174 @@ class PeopleCoverPhoto(APIView): return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) return Response({'success': True}) + + +class MetadataMigrateTags(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def post(self, request, repo_id): + tag_ids = request.data.get('tag_ids') + if not tag_ids: + error_msg = 'tag_ids invalid' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + metadata = RepoMetadata.objects.filter(repo_id=repo_id).first() + if not metadata or not metadata.enabled or not metadata.tags_enabled: + error_msg = f'The tags is disabled for repo {repo_id}.' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # resource check + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if not is_repo_admin(request.user.username, repo_id): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # get all records + from seafevents.repo_metadata.constants import METADATA_TABLE + from seafevents.repo_metadata.constants import TAGS_TABLE + + try: + basic_filters = [{'column_key': METADATA_TABLE.columns.is_dir.name, 'filter_predicate': 'is', 'filter_term': 'file'}] + view = { + 'filters': [], + 'basic_filters': basic_filters + } + filter_columns = [METADATA_TABLE.columns.id.name, METADATA_TABLE.columns.file_name.name, METADATA_TABLE.columns.parent_dir.name] + results = list_eligible_metadata_records(repo_id, request.user.username, view, filter_columns) + records = results.get('results') + except Exception as e: + logger.exception(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + # 每个record对应的文件路径 + records_dict = {} # {file_path: record_id} + for record in records: + file_path = posixpath.join(record.get(METADATA_TABLE.columns.parent_dir.name), record.get(METADATA_TABLE.columns.file_name.name)) + records_dict[file_path] = record.get(METADATA_TABLE.columns.id.name) + + # list old tag info + old_repo_tag_info = {} # {repo_tag_id: {name: '', color: '', file_paths: [file_path1, file_path2]}} + try: + tagged_file_objs = FileTags.objects.filter( + repo_tag__id__in=tag_ids).select_related('repo_tag', 'file_uuid') + for tagged_file_obj in tagged_file_objs: + repo_tag_id = tagged_file_obj.repo_tag.id + parent_path = tagged_file_obj.file_uuid.parent_path + filename = tagged_file_obj.file_uuid.filename + file_path = posixpath.join(parent_path, filename) + if repo_tag_id not in old_repo_tag_info: + old_repo_tag_info[repo_tag_id] = { + 'name': tagged_file_obj.repo_tag.name, + 'color': tagged_file_obj.repo_tag.color, + 'file_paths': [] + } + old_repo_tag_info[repo_tag_id]['file_paths'].append(file_path) + except Exception as err: + logger.error(err) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + print(old_repo_tag_info, '---old_repo_tag_info') + + # file_path to record_id + repo_tag_id_map = {} # {repo_tag_id: [record_id1, record_id2]} + tags_data = [] # [{name: '', color: ''}] + for repo_tag_id, tag_data in old_repo_tag_info.items(): + repo_tag_id_map[repo_tag_id] = [] + for file_path in tag_data['file_paths']: + record_id = records_dict.get(file_path, '') + if record_id: + repo_tag_id_map[repo_tag_id].append(record_id) + tags_data.append({ + TAGS_TABLE.columns.name.name: tag_data['name'], + TAGS_TABLE.columns.color.name: tag_data['color'], + }) + + # {"tags_data":[{"_tag_color":"#46A1FD","_tag_name":"va"}]} + metadata_server_api = MetadataServerAPI(repo_id, request.user.username) + try: + tags_table = get_table_by_name(metadata_server_api, TAGS_TABLE.name) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + if not tags_table: + return api_error(status.HTTP_404_NOT_FOUND, 'tags table not found') + + tags_table_id = tags_table['id'] + exist_tags = [] + new_tags = [] + sql = f'SELECT `{TAGS_TABLE.columns.name.name}` FROM {TAGS_TABLE.name}' + + try: + exist_rows = metadata_server_api.query_rows(sql) + exist_tags = exist_rows.get('results', []) + if exist_tags: + exist_tags = [tag_data.get(TAGS_TABLE.columns.name.name, '') for tag_data in exist_tags] + except Exception as e: + logger.exception(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + # handle exist tags + if exist_tags: + for index, tag_data in enumerate(tags_data): + tag_name = tag_data.get(TAGS_TABLE.columns.name.name, '') + if tag_name not in exist_tags: + new_tags.append(tag_data) + else: + unique_tag_name = gen_unique_tag_name(tag_name, exist_tags) + new_tags.append({ + TAGS_TABLE.columns.color.name: tag_data.get(TAGS_TABLE.columns.color.name, ''), + TAGS_TABLE.columns.name.name: unique_tag_name + }) + else: + new_tags = tags_data + + try: + resp = metadata_server_api.insert_rows(tags_table_id, new_tags) + row_ids = resp.get('row_ids', []) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + # record_id : tags + # handle file_tags_data + new_tags_info_array = [] + for index, old_tag_id in enumerate(repo_tag_id_map): + new_tag_id = row_ids[index] + record_ids = repo_tag_id_map[old_tag_id] + new_tags_info_array.append({ + 'record_ids': record_ids, + 'tag_id': new_tag_id + }) + record_id_tag_id_map = {} + for tag_info in new_tags_info_array: + record_ids = tag_info.get('record_ids', []) + tag_id = tag_info.get('tag_id', '') + for record_id in record_ids: + if record_id not in record_id_tag_id_map: + record_id_tag_id_map[record_id] = [] + record_id_tag_id_map[record_id].append(tag_id) + + try: + metadata_server_api.insert_link(TAGS_TABLE.file_link_id, METADATA_TABLE.id, record_id_tag_id_map) + record = RepoMetadata.objects.filter(repo_id=repo_id).first() + if not record.details_settings: + details_settings = {} + else: + details_settings = json.loads(record.details_settings) + details_settings['tags_migrated'] = True + record.details_settings = json.dumps(details_settings) + + record.save() + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + return Response({'success': True}) diff --git a/seahub/repo_metadata/metadata_server_api.py b/seahub/repo_metadata/metadata_server_api.py index 12738dd6dd..0cc5be911b 100644 --- a/seahub/repo_metadata/metadata_server_api.py +++ b/seahub/repo_metadata/metadata_server_api.py @@ -78,6 +78,26 @@ def list_metadata_view_records(repo_id, user, view, tags_enabled, start=0, limit return response_results +def list_eligible_metadata_records(repo_id, user, view, filter_columns): + from seafevents.repo_metadata.constants import METADATA_TABLE + from seafevents.repo_metadata.utils import gen_view_data_sql + metadata_server_api = MetadataServerAPI(repo_id, user) + columns = metadata_server_api.list_columns(METADATA_TABLE.id).get('columns') + tags_data = {'metadata': [], 'results': []} + + sql = gen_view_data_sql(METADATA_TABLE, columns, view, 0, 0, {'tags_data': tags_data, 'username': user}) + query_fields_str = '' + for column in columns: + column_name = column.get('name') + if column_name in filter_columns: + column_name_str = '`%s`, ' % column_name + query_fields_str += column_name_str + query_fields_str = query_fields_str.strip(', ') + sql = sql.replace('*', query_fields_str) + response_results = metadata_server_api.query_rows(sql, []) + return response_results + + def parse_response(response): if response.status_code >= 300 or response.status_code < 200: raise ConnectionError(response.status_code, response.text) diff --git a/seahub/repo_metadata/urls.py b/seahub/repo_metadata/urls.py index 3c59b414ee..6ffde33f25 100644 --- a/seahub/repo_metadata/urls.py +++ b/seahub/repo_metadata/urls.py @@ -3,7 +3,7 @@ from .apis import MetadataRecognizeFaces, MetadataRecords, MetadataManage, Metad MetadataFolders, MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \ FaceRecognitionManage, FacesRecord, MetadataExtractFileDetails, PeoplePhotos, MetadataTagsStatusManage, MetadataTags, \ MetadataTagsLinks, MetadataFileTags, MetadataTagFiles, MetadataMergeTags, MetadataTagsFiles, MetadataDetailsSettingsView, \ - MetadataOCRManageView, PeopleCoverPhoto + MetadataOCRManageView, PeopleCoverPhoto, MetadataMigrateTags urlpatterns = [ re_path(r'^$', MetadataManage.as_view(), name='api-v2.1-metadata'), @@ -43,4 +43,5 @@ urlpatterns = [ re_path(r'^tag-files/(?P.+)/$', MetadataTagFiles.as_view(), name='api-v2.1-metadata-tag-files'), re_path(r'^merge-tags/$', MetadataMergeTags.as_view(), name='api-v2.1-metadata-merge-tags'), re_path(r'^tags-files/$', MetadataTagsFiles.as_view(), name='api-v2.1-metadata-tags-files'), + re_path(r'^migrate-tags/$', MetadataMigrateTags.as_view(), name='api-v2.1-metadata-migrate-tags'), ] diff --git a/seahub/repo_metadata/utils.py b/seahub/repo_metadata/utils.py index 845548c40e..6156e25d20 100644 --- a/seahub/repo_metadata/utils.py +++ b/seahub/repo_metadata/utils.py @@ -73,6 +73,11 @@ def gen_unique_id(id_set, length=4): return _id _id = generator_base64_code(length) +def gen_unique_tag_name(tag_name, exist_tags, counter=1): + new_name = f'{tag_name}({counter})' + if new_name not in exist_tags: + return new_name + return gen_unique_tag_name(tag_name, exist_tags, counter + 1) def get_face_columns(): from seafevents.repo_metadata.constants import FACES_TABLE