1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-05-13 18:35:20 +00:00
This commit is contained in:
awu0403 2025-04-25 14:43:57 +08:00 committed by GitHub
commit 9682647cef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 293 additions and 22 deletions
frontend/src
components
metadata/components/dialog/metadata-tags-status-dialog
pages/lib-content-view
tag
seahub/repo_metadata

View File

@ -14,6 +14,7 @@ import {
} from '../../metadata';
import SeahubModalHeader from '@/components/common/seahub-modal-header';
import { useMetadataStatus } from '../../hooks';
import Loading from '../../components/loading';
import '../../css/lib-settings.css';
@ -22,12 +23,13 @@ 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 [isMigrating, setIsMigrating] = useState(false);
const toggleTab = useCallback((tab) => {
setActiveTab(tab);
}, []);
@ -46,9 +48,38 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab, showMig
const enableAutoDelSetting = is_admin && enableRepoAutoDel;
const enableExtendedPropertiesSetting = !encrypted && is_admin && enableMetadataManagement;
const handleMigrateStart = useCallback(() => {
setIsMigrating(true);
}, []);
const handleMigrateEnd = useCallback(() => {
setIsMigrating(false);
onMigrateSuccess && onMigrateSuccess();
}, [onMigrateSuccess]);
const handleMigrateError = useCallback(() => {
setIsMigrating(false);
}, []);
return (
<div>
<Modal isOpen={true} className="lib-settings-dialog" toggle={toggleDialog}>
{isMigrating && (
<div style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(255, 255, 255, 0.8)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1050
}}>
<Loading />
</div>
)}
<SeahubModalHeader toggle={toggleDialog}>
{gettext('Settings')}
</SeahubModalHeader>
@ -202,6 +233,10 @@ const LibSettingsDialog = ({ repoID, currentRepoInfo, toggleDialog, tab, showMig
toggleDialog={toggleDialog}
enableMetadata={enableMetadata}
showMigrateTip={showMigrateTip}
usedRepoTags={usedRepoTags}
onMigrateSuccess={handleMigrateEnd}
onMigrateError={handleMigrateError}
onMigrateStart={handleMigrateStart}
/>
</TabPane>
)}

View File

@ -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}
/>
<DirViews repoID={repoID} currentPath={currentPath} userPerm={userPerm} currentRepoInfo={currentRepoInfo} />
<DirViews repoID={repoID} currentPath={currentPath} userPerm={userPerm} currentRepoInfo={currentRepoInfo} usedRepoTags={usedRepoTags} />
<DirTags repoID={repoID} currentPath={currentPath} userPerm={userPerm} currentRepoInfo={currentRepoInfo} />
<DirOthers repoID={repoID} userPerm={userPerm} currentRepoInfo={currentRepoInfo} />
<DirOthers repoID={repoID} userPerm={userPerm} currentRepoInfo={currentRepoInfo} usedRepoTags={usedRepoTags} />
</>
)}
</div>

View File

@ -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}
/>
<ResizeBar
resizeBarRef={this.resizeBarRef}

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils';
@ -12,9 +12,10 @@ import { TAB } from '../../../constants/repo-setting-tabs';
import './index.css';
const DirOthers = ({ userPerm, repoID, currentRepoInfo }) => {
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;

View File

@ -7,25 +7,24 @@ import { EVENT_BUS_TYPE } from '../components/common/event-bus-type';
const RepoInfoBarMigrate = () => {
const { enableMetadata } = useMetadataStatus();
const { enableMetadataManagement, 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 (
<div className="repo-info-bar-migrate mt-2">
{enableMetadata ?
(
{!tagsMigrated && (
enableMetadataManagement ? (
<>
{gettext('Tips: There are tags of old version. Please migrate tags to new version.')}
<Button color="link" size="sm" tag="a" onClick={openMigrate}>{gettext('Migrate')}</Button>
</>
) :
(
) : (
<>{gettext('Tips: These are tags of old version. The feature is deprecated and can no longer be used.')}</>
)
}
)}
</div>
);
};

View File

@ -19,12 +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, onMigrateStart, onMigrateError }) => {
const [value, setValue] = useState(oldValue);
const [lang, setLang] = useState(oldLang);
const [submitting, setSubmitting] = useState(false);
const [showTurnOffConfirmDialog, setShowTurnOffConfirmDialog] = useState(false);
const onToggle = useCallback(() => {
toggle();
}, [toggle]);
@ -46,8 +45,16 @@ const MetadataTagsStatusDialog = ({ value: oldValue, lang: oldLang, repoID, togg
}, [lang, repoID, submit, toggle, value]);
const migrateTag = useCallback(() => {
// TODO backend migrate old tags
}, []);
onMigrateStart && onMigrateStart();
tagsAPI.migrateTags(repoID, usedRepoTags).then(res => {
toaster.success(gettext('Tags migrated successfully'));
onMigrateSuccess && onMigrateSuccess();
}).catch(error => {
const errorMsg = Utils.getErrorMsg(error);
toaster.danger(errorMsg);
onMigrateError && onMigrateError();
});
}, [repoID, usedRepoTags, onMigrateSuccess, onMigrateStart, onMigrateError]);
const turnOffConfirmToggle = useCallback(() => {
setShowTurnOffConfirmDialog(!showTurnOffConfirmDialog);
@ -110,7 +117,12 @@ const MetadataTagsStatusDialog = ({ value: oldValue, lang: oldLang, repoID, togg
{showMigrateTip &&
<FormGroup className="mt-6">
<p>{gettext('This library contains tags of old version. Do you like to migrate the tags to new version?')}</p>
<Button color="primary" onClick={migrateTag}>{gettext('Migrate old version tags')}</Button>
<Button
color="primary"
onClick={migrateTag}
>
{gettext('Migrate old version tags')}
</Button>
</FormGroup>
}
</ModalBody>
@ -136,6 +148,10 @@ MetadataTagsStatusDialog.propTypes = {
submit: PropTypes.func.isRequired,
enableMetadata: PropTypes.bool.isRequired,
showMigrateTip: PropTypes.bool,
repoTags: PropTypes.array,
onMigrateSuccess: PropTypes.func,
onMigrateError: PropTypes.func,
onMigrateStart: PropTypes.func,
};
export default MetadataTagsStatusDialog;

View File

@ -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 (
<MetadataStatusProvider repoID={repoID} repoInfo={currentRepoInfo} hideMetadataView={this.hideMetadataView} statusCallback={this.metadataStatusCallback} >

View File

@ -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();

View File

@ -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
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
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,195 @@ 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)
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)
# list old tag info
source_tags_info = {} # {tag_id: {name: '', color: '', file_paths: [file_path1, file_path2]}}
file_paths_set = set() # Used for querying metadata later
try:
tagged_files = FileTags.objects.filter(
repo_tag__id__in=tag_ids).select_related('repo_tag', 'file_uuid')
for tagged_file in tagged_files:
tag_id = tagged_file.repo_tag.id
parent_path = tagged_file.file_uuid.parent_path
filename = tagged_file.file_uuid.filename
file_path = posixpath.join(parent_path, filename)
if tag_id not in source_tags_info:
source_tags_info[tag_id] = {
'name': tagged_file.repo_tag.name,
'color': tagged_file.repo_tag.color,
'file_paths': []
}
source_tags_info[tag_id]['file_paths'].append(file_path)
file_paths_set.add(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)
# query records
from seafevents.repo_metadata.constants import METADATA_TABLE
from seafevents.repo_metadata.constants import TAGS_TABLE
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
try:
dir_paths = [os.path.dirname(path) for path in file_paths_set]
filenames = [os.path.basename(path) for path in file_paths_set]
filenames_str = ', '.join([f'"{name}"' for name in filenames])
dir_paths_str = ', '.join([f'"{path}"' for path in dir_paths])
sql = f'''
SELECT `{METADATA_TABLE.columns.id.name}`,
`{METADATA_TABLE.columns.file_name.name}`,
`{METADATA_TABLE.columns.parent_dir.name}`
FROM `{METADATA_TABLE.name}`
WHERE `{METADATA_TABLE.columns.is_dir.name}` = False
AND `{METADATA_TABLE.columns.file_name.name}` IN ({filenames_str})
AND `{METADATA_TABLE.columns.parent_dir.name}` IN ({dir_paths_str})
'''
query_result = metadata_server_api.query_rows(sql, [])
metadata_records = query_result.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)
file_to_record_map = {} # {file_path: record_id}
for record in metadata_records:
parent_dir = record.get(METADATA_TABLE.columns.parent_dir.name)
file_name = record.get(METADATA_TABLE.columns.file_name.name)
record_id = record.get(METADATA_TABLE.columns.id.name)
file_path = posixpath.join(parent_dir, file_name)
file_to_record_map[file_path] = record_id
tags_data = [] # [{name: '', color: ''}]
for tag_id, tag_info in source_tags_info.items():
tags_data.append({
TAGS_TABLE.columns.name.name: tag_info['name'],
TAGS_TABLE.columns.color.name: tag_info['color'],
})
try:
tags_table = get_table_by_name(metadata_server_api, TAGS_TABLE.name)
if not tags_table:
return api_error(status.HTTP_404_NOT_FOUND, 'tags table not found')
sql = f'SELECT `{TAGS_TABLE.columns.name.name}` FROM {TAGS_TABLE.name}'
existing_tags_result = metadata_server_api.query_rows(sql)
existing_tag_records = existing_tags_result.get('results', [])
existing_tag_names = []
if existing_tag_records:
existing_tag_names = [
tag_dict.get(TAGS_TABLE.columns.name.name, '')
for tag_dict in existing_tag_records
]
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
# handle tag name conflict
tags_table_id = tags_table['id']
tags_to_create = []
if existing_tag_names:
for idx, tag_data in enumerate(tags_data):
tag_name = tag_data.get(TAGS_TABLE.columns.name.name, '')
tag_color = tag_data.get(TAGS_TABLE.columns.color.name, '')
if tag_name not in existing_tag_names:
tags_to_create.append(tag_data)
else:
unique_name = gen_unique_tag_name(tag_name, existing_tag_names)
tags_to_create.append({
TAGS_TABLE.columns.color.name: tag_color,
TAGS_TABLE.columns.name.name: unique_name
})
else:
tags_to_create = tags_data
try:
response = metadata_server_api.insert_rows(tags_table_id, tags_to_create)
new_tag_ids = response.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)
# create new old tag id mapping
destination_tags_info = {} # new tag info
for idx, old_tag_id in enumerate(source_tags_info):
new_tag_id = new_tag_ids[idx]
file_paths = source_tags_info[old_tag_id]['file_paths']
destination_tags_info[new_tag_id] = {
'file_paths': file_paths
}
# create record id to tag id mapping
record_to_tags_map = {} # {record_id: [tag_id1, tag_id2]}
for tag_id, tag_info in destination_tags_info.items():
for file_path in tag_info['file_paths']:
record_id = file_to_record_map.get(file_path)
if not record_id:
continue
if record_id not in record_to_tags_map:
record_to_tags_map[record_id] = []
record_to_tags_map[record_id].append(tag_id)
try:
metadata_server_api.insert_link(
TAGS_TABLE.file_link_id,
METADATA_TABLE.id,
record_to_tags_map
)
# Record that the old version tags have been migrated
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})

View File

@ -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<tag_id>.+)/$', 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'),
]

View File

@ -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