mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-04 08:28:11 +00:00
face_cluster (#6470)
* face_cluster * update * update * feat: update ui * feat: optimize code * feat: update code * feat: optimize ui * feat: optimize view name --------- Co-authored-by: zheng.shen <zheng.shen@seafile.com> Co-authored-by: 杨国璇 <ygx@Hello-word.local>
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
export const LIST_MODE = 'list';
|
export const LIST_MODE = 'list';
|
||||||
export const GRID_MODE = 'grid';
|
export const GRID_MODE = 'grid';
|
||||||
export const METADATA_MODE = 'metadata';
|
export const METADATA_MODE = 'metadata';
|
||||||
|
export const FACE_RECOGNITION_MODE = 'person_image';
|
||||||
|
@@ -9,7 +9,8 @@ import ResizeBar from '../resize-bar';
|
|||||||
import { DRAG_HANDLER_HEIGHT, MAX_SIDE_PANEL_RATE, MIN_SIDE_PANEL_RATE } from '../resize-bar/constants';
|
import { DRAG_HANDLER_HEIGHT, MAX_SIDE_PANEL_RATE, MIN_SIDE_PANEL_RATE } from '../resize-bar/constants';
|
||||||
import { SeafileMetadata } from '../../metadata';
|
import { SeafileMetadata } from '../../metadata';
|
||||||
import { mediaUrl } from '../../utils/constants';
|
import { mediaUrl } from '../../utils/constants';
|
||||||
import { GRID_MODE, LIST_MODE, METADATA_MODE } from './constants';
|
import { GRID_MODE, LIST_MODE, METADATA_MODE, FACE_RECOGNITION_MODE } from './constants';
|
||||||
|
import FaceRecognition from '../../metadata/views/face-recognition';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
isSidePanelFolded: PropTypes.bool,
|
isSidePanelFolded: PropTypes.bool,
|
||||||
@@ -203,6 +204,9 @@ class DirColumnView extends React.Component {
|
|||||||
renameFileCallback={this.props.renameFileCallback}
|
renameFileCallback={this.props.renameFileCallback}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
{currentMode === FACE_RECOGNITION_MODE &&
|
||||||
|
<FaceRecognition repoID={this.props.repoID}/>
|
||||||
|
}
|
||||||
{currentMode === LIST_MODE &&
|
{currentMode === LIST_MODE &&
|
||||||
<DirListView
|
<DirListView
|
||||||
path={this.props.path}
|
path={this.props.path}
|
||||||
|
@@ -2,7 +2,7 @@ import React, { useCallback, useMemo, useState } from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { gettext } from '../../utils/constants';
|
import { gettext } from '../../utils/constants';
|
||||||
import TreeSection from '../tree-section';
|
import TreeSection from '../tree-section';
|
||||||
import { MetadataStatusManagementDialog, MetadataTreeView, useMetadata } from '../../metadata';
|
import { MetadataStatusManagementDialog, MetadataFaceRecognitionDialog, MetadataTreeView, useMetadata } from '../../metadata';
|
||||||
import ExtensionPrompts from './extension-prompts';
|
import ExtensionPrompts from './extension-prompts';
|
||||||
|
|
||||||
const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
|
const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
|
||||||
@@ -12,18 +12,31 @@ const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
|
|||||||
}, [window.app.pageOptions.enableMetadataManagement]);
|
}, [window.app.pageOptions.enableMetadataManagement]);
|
||||||
|
|
||||||
const [showMetadataStatusManagementDialog, setShowMetadataStatusManagementDialog] = useState(false);
|
const [showMetadataStatusManagementDialog, setShowMetadataStatusManagementDialog] = useState(false);
|
||||||
const { enableMetadata, updateEnableMetadata, navigation } = useMetadata();
|
const [showMetadataFaceRecognitionDialog, setShowMetadataFaceRecognitionDialog] = useState(false);
|
||||||
|
const { enableMetadata, updateEnableMetadata, enableFaceRecognition, updateEnableFaceRecognition, navigation } = useMetadata();
|
||||||
const moreOperations = useMemo(() => {
|
const moreOperations = useMemo(() => {
|
||||||
if (!enableMetadataManagement || !currentRepoInfo.is_admin) return [];
|
if (!enableMetadataManagement || !currentRepoInfo.is_admin) return [];
|
||||||
return [
|
let operations = [
|
||||||
{ key: 'extended-properties', value: gettext('Extended properties') }
|
{ key: 'extended-properties', value: gettext('Extended properties') }
|
||||||
];
|
];
|
||||||
}, [enableMetadataManagement, currentRepoInfo]);
|
if (enableMetadata) {
|
||||||
|
operations.push({ key: 'face-recognition', value: gettext('Face recognition') });
|
||||||
|
}
|
||||||
|
return operations;
|
||||||
|
}, [enableMetadataManagement, enableMetadata, currentRepoInfo]);
|
||||||
|
|
||||||
const moreOperationClick = useCallback((operationKey) => {
|
const moreOperationClick = useCallback((operationKey) => {
|
||||||
if (operationKey === 'extended-properties') {
|
switch (operationKey) {
|
||||||
setShowMetadataStatusManagementDialog(true);
|
case 'extended-properties': {
|
||||||
return;
|
setShowMetadataStatusManagementDialog(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'face-recognition': {
|
||||||
|
setShowMetadataFaceRecognitionDialog(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -31,6 +44,14 @@ const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
|
|||||||
setShowMetadataStatusManagementDialog(false);
|
setShowMetadataStatusManagementDialog(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const closeMetadataFaceRecognitionDialog = useCallback(() => {
|
||||||
|
setShowMetadataFaceRecognitionDialog(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openMetadataFaceRecognition = useCallback(() => {
|
||||||
|
updateEnableFaceRecognition(true);
|
||||||
|
}, [updateEnableFaceRecognition]);
|
||||||
|
|
||||||
const toggleMetadataStatus = useCallback((value) => {
|
const toggleMetadataStatus = useCallback((value) => {
|
||||||
updateEnableMetadata(value);
|
updateEnableMetadata(value);
|
||||||
}, [updateEnableMetadata]);
|
}, [updateEnableMetadata]);
|
||||||
@@ -63,6 +84,14 @@ const DirViews = ({ userPerm, repoID, currentPath, currentRepoInfo }) => {
|
|||||||
submit={toggleMetadataStatus}
|
submit={toggleMetadataStatus}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{showMetadataFaceRecognitionDialog && (
|
||||||
|
<MetadataFaceRecognitionDialog
|
||||||
|
value={enableFaceRecognition}
|
||||||
|
repoID={repoID}
|
||||||
|
toggle={closeMetadataFaceRecognitionDialog}
|
||||||
|
submit={openMetadataFaceRecognition}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -52,9 +52,9 @@ class ItemDropdownMenu extends React.Component {
|
|||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) { // for toolbar item operation
|
UNSAFE_componentWillReceiveProps(nextProps) { // for toolbar item operation
|
||||||
let { item } = nextProps;
|
let { item } = nextProps;
|
||||||
if (item.name !== this.props.item.name) {
|
const nextMenuList = nextProps.getMenuList(item);
|
||||||
let menuList = this.props.getMenuList(item);
|
if (item.name !== this.props.item.name || this.state.menuList !== nextMenuList) {
|
||||||
this.setState({ menuList: menuList });
|
this.setState({ menuList: nextMenuList });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,7 +4,8 @@ import KeyCodes from './keyCodes';
|
|||||||
export const DIALOG_MAX_HEIGHT = window.innerHeight - 56; // Dialog margin is 3.5rem (56px)
|
export const DIALOG_MAX_HEIGHT = window.innerHeight - 56; // Dialog margin is 3.5rem (56px)
|
||||||
|
|
||||||
export const PRIVATE_FILE_TYPE = {
|
export const PRIVATE_FILE_TYPE = {
|
||||||
FILE_EXTENDED_PROPERTIES: '__file_extended_properties'
|
FILE_EXTENDED_PROPERTIES: '__file_extended_properties',
|
||||||
|
FACE_RECOGNITION: '__face_recognition',
|
||||||
};
|
};
|
||||||
|
|
||||||
const TAG_COLORS = ['#FBD44A', '#EAA775', '#F4667C', '#DC82D2', '#9860E5', '#9F8CF1', '#59CB74', '#ADDF84',
|
const TAG_COLORS = ['#FBD44A', '#EAA775', '#F4667C', '#DC82D2', '#9860E5', '#9F8CF1', '#59CB74', '#ADDF84',
|
||||||
|
@@ -247,6 +247,32 @@ class MetadataManagerAPI {
|
|||||||
};
|
};
|
||||||
return this.req.delete(url, { data });
|
return this.req.delete(url, { data });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// face recognition
|
||||||
|
getFaceRecognitionStatus(repoID) {
|
||||||
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-recognition/';
|
||||||
|
return this.req.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
openFaceRecognition = (repoID) => {
|
||||||
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-recognition/';
|
||||||
|
return this.req.post(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
getFaceData = (repoID, start = 0, limit = 1000) => {
|
||||||
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-records/?start=' + start + '&limit=' + limit;
|
||||||
|
return this.req.get(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateFaceName = (repoID, recordID, name) => {
|
||||||
|
const url = this.server + '/api/v2.1/repos/' + repoID + '/metadata/face-record/';
|
||||||
|
const params = {
|
||||||
|
record_id: recordID,
|
||||||
|
name: name,
|
||||||
|
};
|
||||||
|
return this.req.put(url, params);
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadataAPI = new MetadataManagerAPI();
|
const metadataAPI = new MetadataManagerAPI();
|
||||||
|
@@ -0,0 +1,51 @@
|
|||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';
|
||||||
|
import { gettext } from '../../../../utils/constants';
|
||||||
|
import metadataAPI from '../../../api';
|
||||||
|
import toaster from '../../../../components/toast';
|
||||||
|
import { Utils } from '../../../../utils/utils';
|
||||||
|
|
||||||
|
const MetadataFaceRecognitionDialog = ({ value, repoID, toggle, submit }) => {
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const onToggle = useCallback(() => {
|
||||||
|
toggle();
|
||||||
|
}, [toggle]);
|
||||||
|
|
||||||
|
const onSubmit = useCallback(() => {
|
||||||
|
setSubmitting(true);
|
||||||
|
metadataAPI.openFaceRecognition(repoID).then(res => {
|
||||||
|
submit(true);
|
||||||
|
toggle();
|
||||||
|
}).catch(error => {
|
||||||
|
const errorMsg = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errorMsg);
|
||||||
|
setSubmitting(false);
|
||||||
|
});
|
||||||
|
}, [repoID, submit, toggle]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal className="metadata-face-recognition-dialog" isOpen={true} toggle={onToggle}>
|
||||||
|
<ModalHeader toggle={onToggle}>{gettext('Face recognition')}</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
{value ? gettext('Face recognition enabled.') : gettext('Whether to enable face recognition?')}
|
||||||
|
</ModalBody>
|
||||||
|
{!value && (
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="secondary" onClick={onToggle}>{gettext('Cancel')}</Button>
|
||||||
|
<Button color="primary" disabled={submitting} onClick={onSubmit}>{gettext('Submit')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
MetadataFaceRecognitionDialog.propTypes = {
|
||||||
|
value: PropTypes.bool.isRequired,
|
||||||
|
repoID: PropTypes.string.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
submit: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MetadataFaceRecognitionDialog;
|
@@ -11,6 +11,7 @@ export const NOT_DISPLAY_COLUMN_KEYS = [
|
|||||||
PRIVATE_COLUMN_KEY.FILE_DETAILS,
|
PRIVATE_COLUMN_KEY.FILE_DETAILS,
|
||||||
PRIVATE_COLUMN_KEY.LOCATION,
|
PRIVATE_COLUMN_KEY.LOCATION,
|
||||||
PRIVATE_COLUMN_KEY.IS_DIR,
|
PRIVATE_COLUMN_KEY.IS_DIR,
|
||||||
|
PRIVATE_COLUMN_KEY.FACE_LINKS,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const VIEW_NOT_DISPLAY_COLUMN_KEYS = [
|
export const VIEW_NOT_DISPLAY_COLUMN_KEYS = [
|
||||||
|
@@ -29,6 +29,7 @@ export const PRIVATE_COLUMN_KEY = {
|
|||||||
CAPTURE_TIME: '_capture_time',
|
CAPTURE_TIME: '_capture_time',
|
||||||
FILE_REVIEWER: '_reviewer',
|
FILE_REVIEWER: '_reviewer',
|
||||||
OWNER: '_owner',
|
OWNER: '_owner',
|
||||||
|
FACE_LINKS: '_face_links',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PRIVATE_COLUMN_KEYS = [
|
export const PRIVATE_COLUMN_KEYS = [
|
||||||
@@ -59,6 +60,7 @@ export const PRIVATE_COLUMN_KEYS = [
|
|||||||
PRIVATE_COLUMN_KEY.CAPTURE_TIME,
|
PRIVATE_COLUMN_KEY.CAPTURE_TIME,
|
||||||
PRIVATE_COLUMN_KEY.FILE_REVIEWER,
|
PRIVATE_COLUMN_KEY.FILE_REVIEWER,
|
||||||
PRIVATE_COLUMN_KEY.OWNER,
|
PRIVATE_COLUMN_KEY.OWNER,
|
||||||
|
PRIVATE_COLUMN_KEY.FACE_LINKS,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const EDITABLE_PRIVATE_COLUMN_KEYS = [
|
export const EDITABLE_PRIVATE_COLUMN_KEYS = [
|
||||||
|
@@ -6,7 +6,7 @@ import { SORT_COLUMN_OPTIONS, GALLERY_SORT_COLUMN_OPTIONS, GALLERY_FIRST_SORT_CO
|
|||||||
|
|
||||||
export const VIEW_TYPE = {
|
export const VIEW_TYPE = {
|
||||||
TABLE: 'table',
|
TABLE: 'table',
|
||||||
GALLERY: 'gallery'
|
GALLERY: 'gallery',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VIEW_TYPE_ICON = {
|
export const VIEW_TYPE_ICON = {
|
||||||
|
@@ -15,8 +15,10 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
|||||||
}, [window.app.pageOptions.enableMetadataManagement]);
|
}, [window.app.pageOptions.enableMetadataManagement]);
|
||||||
|
|
||||||
const [enableMetadata, setEnableExtendedProperties] = useState(false);
|
const [enableMetadata, setEnableExtendedProperties] = useState(false);
|
||||||
|
const [enableFaceRecognition, setEnableFaceRecognition] = useState(false);
|
||||||
const [showFirstView, setShowFirstView] = useState(false);
|
const [showFirstView, setShowFirstView] = useState(false);
|
||||||
const [navigation, setNavigation] = useState([]);
|
const [navigation, setNavigation] = useState([]);
|
||||||
|
const [staticView, setStaticView] = useState([]);
|
||||||
const [, setCount] = useState(0);
|
const [, setCount] = useState(0);
|
||||||
const viewsMap = useRef({});
|
const viewsMap = useRef({});
|
||||||
|
|
||||||
@@ -55,12 +57,21 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
|||||||
if (!newValue) {
|
if (!newValue) {
|
||||||
hideMetadataView && hideMetadataView();
|
hideMetadataView && hideMetadataView();
|
||||||
cancelURLView();
|
cancelURLView();
|
||||||
|
setEnableFaceRecognition(false);
|
||||||
} else {
|
} else {
|
||||||
setShowFirstView(true);
|
setShowFirstView(true);
|
||||||
}
|
}
|
||||||
setEnableExtendedProperties(newValue);
|
setEnableExtendedProperties(newValue);
|
||||||
}, [enableMetadata, hideMetadataView, cancelURLView]);
|
}, [enableMetadata, hideMetadataView, cancelURLView]);
|
||||||
|
|
||||||
|
const updateEnableFaceRecognition = useCallback((newValue) => {
|
||||||
|
if (newValue === enableFaceRecognition) return;
|
||||||
|
setEnableFaceRecognition(newValue);
|
||||||
|
if (newValue) {
|
||||||
|
toaster.success(gettext('Recognizing portraits. Please refresh the page later.'));
|
||||||
|
}
|
||||||
|
}, [enableFaceRecognition]);
|
||||||
|
|
||||||
// views
|
// views
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enableMetadata) {
|
if (enableMetadata) {
|
||||||
@@ -71,6 +82,11 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
|||||||
viewsMap.current[view._id] = view;
|
viewsMap.current[view._id] = view;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
viewsMap.current['_face_recognition'] = {
|
||||||
|
_id: '_face_recognition',
|
||||||
|
name: gettext('Photos - classfied by people'),
|
||||||
|
type: PRIVATE_FILE_TYPE.FACE_RECOGNITION,
|
||||||
|
};
|
||||||
setNavigation(navigation);
|
setNavigation(navigation);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
const errorMsg = Utils.getErrorMsg(error);
|
const errorMsg = Utils.getErrorMsg(error);
|
||||||
@@ -84,8 +100,31 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [repoID, enableMetadata]);
|
}, [repoID, enableMetadata]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enableMetadata) {
|
||||||
|
setStaticView([]);
|
||||||
|
setEnableFaceRecognition(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metadataAPI.getFaceRecognitionStatus(repoID).then(res => {
|
||||||
|
setEnableFaceRecognition(res.data.enabled);
|
||||||
|
}).catch(error => {
|
||||||
|
const errorMsg = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errorMsg);
|
||||||
|
});
|
||||||
|
}, [repoID, enableMetadata]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enableFaceRecognition) {
|
||||||
|
setStaticView([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStaticView([{ _id: '_face_recognition', type: 'view' }]);
|
||||||
|
}, [enableFaceRecognition]);
|
||||||
|
|
||||||
const selectView = useCallback((view, isSelected) => {
|
const selectView = useCallback((view, isSelected) => {
|
||||||
if (isSelected) return;
|
if (isSelected) return;
|
||||||
|
const isFaceRecognitionView = view.type === PRIVATE_FILE_TYPE.FACE_RECOGNITION;
|
||||||
const node = {
|
const node = {
|
||||||
children: [],
|
children: [],
|
||||||
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id,
|
path: '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id,
|
||||||
@@ -94,9 +133,9 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
|||||||
isPreload: true,
|
isPreload: true,
|
||||||
object: {
|
object: {
|
||||||
file_tags: [],
|
file_tags: [],
|
||||||
id: PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
|
id: isFaceRecognitionView ? PRIVATE_FILE_TYPE.FACE_RECOGNITION : PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
|
||||||
name: gettext('File extended properties'),
|
name: isFaceRecognitionView ? gettext('Photos - classfied by people') : gettext('File extended properties'),
|
||||||
type: PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
|
type: isFaceRecognitionView ? PRIVATE_FILE_TYPE.FACE_RECOGNITION : PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES,
|
||||||
isDir: () => false,
|
isDir: () => false,
|
||||||
},
|
},
|
||||||
parentNode: {},
|
parentNode: {},
|
||||||
@@ -177,9 +216,12 @@ export const MetadataProvider = ({ repoID, hideMetadataView, selectMetadataView,
|
|||||||
<MetadataContext.Provider value={{
|
<MetadataContext.Provider value={{
|
||||||
enableMetadata,
|
enableMetadata,
|
||||||
updateEnableMetadata,
|
updateEnableMetadata,
|
||||||
|
enableFaceRecognition,
|
||||||
|
updateEnableFaceRecognition,
|
||||||
showFirstView,
|
showFirstView,
|
||||||
setShowFirstView,
|
setShowFirstView,
|
||||||
navigation,
|
navigation,
|
||||||
|
staticView,
|
||||||
viewsMap: viewsMap.current,
|
viewsMap: viewsMap.current,
|
||||||
selectView,
|
selectView,
|
||||||
addView,
|
addView,
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import SeafileMetadata, { Context as MetadataContext } from './metadata-view';
|
import SeafileMetadata, { Context as MetadataContext } from './metadata-view';
|
||||||
import MetadataStatusManagementDialog from './components/dialog/metadata-status-manage-dialog';
|
import MetadataStatusManagementDialog from './components/dialog/metadata-status-manage-dialog';
|
||||||
|
import MetadataFaceRecognitionDialog from './components/dialog/metadata-face-recognition-dialog';
|
||||||
import MetadataDetails from './components/metadata-details';
|
import MetadataDetails from './components/metadata-details';
|
||||||
import MetadataTreeView from './metadata-tree-view';
|
import MetadataTreeView from './metadata-tree-view';
|
||||||
import metadataAPI from './api';
|
import metadataAPI from './api';
|
||||||
@@ -11,6 +12,7 @@ export {
|
|||||||
MetadataContext,
|
MetadataContext,
|
||||||
SeafileMetadata,
|
SeafileMetadata,
|
||||||
MetadataStatusManagementDialog,
|
MetadataStatusManagementDialog,
|
||||||
|
MetadataFaceRecognitionDialog,
|
||||||
MetadataTreeView,
|
MetadataTreeView,
|
||||||
MetadataDetails,
|
MetadataDetails,
|
||||||
};
|
};
|
||||||
|
@@ -38,8 +38,10 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
|||||||
}, [userPerm]);
|
}, [userPerm]);
|
||||||
const [, setState] = useState(0);
|
const [, setState] = useState(0);
|
||||||
const {
|
const {
|
||||||
|
enableFaceRecognition,
|
||||||
showFirstView,
|
showFirstView,
|
||||||
navigation,
|
navigation,
|
||||||
|
staticView,
|
||||||
viewsMap,
|
viewsMap,
|
||||||
selectView,
|
selectView,
|
||||||
addView,
|
addView,
|
||||||
@@ -196,6 +198,20 @@ const MetadataTreeView = ({ userPerm, currentPath }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{enableFaceRecognition && staticView.map((item) => {
|
||||||
|
const view = viewsMap[item._id];
|
||||||
|
const viewPath = '/' + PRIVATE_FILE_TYPE.FILE_EXTENDED_PROPERTIES + '/' + view._id;
|
||||||
|
const isSelected = currentPath === viewPath;
|
||||||
|
return (
|
||||||
|
<ViewItem
|
||||||
|
key={view._id}
|
||||||
|
userPerm="r"
|
||||||
|
view={view}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onClick={(view) => selectView(view, isSelected)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{canAdd && (
|
{canAdd && (
|
||||||
<div id="sf-metadata-view-popover">
|
<div id="sf-metadata-view-popover">
|
||||||
<CustomizeAddTool
|
<CustomizeAddTool
|
||||||
|
@@ -207,6 +207,8 @@ export const getColumnDisplayName = (key, name) => {
|
|||||||
return gettext('Capture time');
|
return gettext('Capture time');
|
||||||
case PRIVATE_COLUMN_KEY.OWNER:
|
case PRIVATE_COLUMN_KEY.OWNER:
|
||||||
return gettext('File owner');
|
return gettext('File owner');
|
||||||
|
case PRIVATE_COLUMN_KEY.FACE_FEATURES:
|
||||||
|
return gettext('Face Features');
|
||||||
default:
|
default:
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
110
frontend/src/metadata/views/face-recognition/face-group.js
Normal file
110
frontend/src/metadata/views/face-recognition/face-group.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Input } from 'reactstrap';
|
||||||
|
import toaster from '../../../components/toast';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import { gettext, siteRoot } from '../../../utils/constants';
|
||||||
|
import metadataAPI from '../../api';
|
||||||
|
import isHotkey from 'is-hotkey';
|
||||||
|
import { isEnter } from '../../utils/hotkey';
|
||||||
|
|
||||||
|
const theadData = [
|
||||||
|
{ width: '5%', text: '' },
|
||||||
|
{ width: '39%', text: gettext('Name') },
|
||||||
|
{ width: '34%', text: gettext('Original path') },
|
||||||
|
{ width: '11%', text: gettext('Size') },
|
||||||
|
{ width: '11%', text: gettext('Last Update') },
|
||||||
|
];
|
||||||
|
|
||||||
|
const FaceGroup = ({ repoID, group, onPhotoClick }) => {
|
||||||
|
const [name, setName] = useState(group.name);
|
||||||
|
const [isRenaming, setRenaming] = useState(false);
|
||||||
|
const serverName = useRef(group.name);
|
||||||
|
|
||||||
|
const showPhoto = useCallback((event, photo) => {
|
||||||
|
event.preventDefault();
|
||||||
|
onPhotoClick(photo);
|
||||||
|
}, [onPhotoClick]);
|
||||||
|
|
||||||
|
const changeName = useCallback((event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
if (name === value) return;
|
||||||
|
setName(value);
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
const renameName = useCallback(() => {
|
||||||
|
setRenaming(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateName = useCallback(() => {
|
||||||
|
if (name === serverName.current) {
|
||||||
|
setRenaming(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metadataAPI.updateFaceName(repoID, group.record_id, name).then(res => {
|
||||||
|
serverName.current = name;
|
||||||
|
setRenaming(false);
|
||||||
|
}).catch(err => {
|
||||||
|
const errorMsg = Utils.getErrorMsg(err);
|
||||||
|
toaster.danger(errorMsg);
|
||||||
|
setName(serverName.current);
|
||||||
|
setRenaming(false);
|
||||||
|
});
|
||||||
|
}, [repoID, group, name]);
|
||||||
|
|
||||||
|
const onRenameKeyDown = useCallback((event) => {
|
||||||
|
if (isEnter(event)) {
|
||||||
|
updateName();
|
||||||
|
} else if (isHotkey('esc', event)) {
|
||||||
|
setName(serverName.current);
|
||||||
|
setRenaming(false);
|
||||||
|
}
|
||||||
|
}, [updateName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={group.record_id} className="sf-metadata-face-recognition-item">
|
||||||
|
{isRenaming ?
|
||||||
|
(<Input
|
||||||
|
autoFocus
|
||||||
|
value={name}
|
||||||
|
onChange={changeName}
|
||||||
|
onBlur={updateName}
|
||||||
|
onKeyDown={onRenameKeyDown}
|
||||||
|
/>)
|
||||||
|
:
|
||||||
|
(<div className="sf-metadata-face-recognition-name form-control" onClick={renameName}>{name}</div>)
|
||||||
|
}
|
||||||
|
<table className="table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{theadData.map((item, index) => {
|
||||||
|
return <th key={index} width={item.width}>{item.text}</th>;
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{group.photos.map((photo, index) => {
|
||||||
|
return (
|
||||||
|
<tr key={index} onClick={(event) => showPhoto(event, photo)}>
|
||||||
|
<td className="text-center"><img src={photo.src} alt="" className="thumbnail cursor-pointer" /></td>
|
||||||
|
<td><a href={`${siteRoot}lib/${repoID}/file${photo.path}`} onClick={(event) => showPhoto(event, photo)}>{photo.file_name}</a></td>
|
||||||
|
<td>{photo.parent_dir}</td>
|
||||||
|
<td>{Utils.bytesToSize(photo.size)}</td>
|
||||||
|
<td title={moment(photo.mtime).fromNow()}>{moment(photo.mtime).fromNow()}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FaceGroup.propTypes = {
|
||||||
|
repoID: PropTypes.string,
|
||||||
|
group: PropTypes.object.isRequired,
|
||||||
|
onPhotoClick: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FaceGroup;
|
29
frontend/src/metadata/views/face-recognition/index.css
Normal file
29
frontend/src/metadata/views/face-recognition/index.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.sf-metadata-face-recognition {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-face-recognition .sf-metadata-face-recognition-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-face-recognition .sf-metadata-face-recognition-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-face-recognition .sf-metadata-face-recognition-name {
|
||||||
|
border-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sf-metadata-face-recognition .sf-metadata-face-recognition-loading-more {
|
||||||
|
height: 30px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
161
frontend/src/metadata/views/face-recognition/index.js
Normal file
161
frontend/src/metadata/views/face-recognition/index.js
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CenteredLoading } from '@seafile/sf-metadata-ui-component';
|
||||||
|
import toaster from '../../../components/toast';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
import metadataAPI from '../../api';
|
||||||
|
import FaceGroup from './face-group';
|
||||||
|
import ImageDialog from '../../../components/dialog/image-dialog';
|
||||||
|
import ModalPortal from '../../../components/modal-portal';
|
||||||
|
import { siteRoot, gettext, thumbnailSizeForOriginal, thumbnailDefaultSize } from '../../../utils/constants';
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
const LIMIT = 1000;
|
||||||
|
|
||||||
|
const FaceRecognition = ({ repoID }) => {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [faceOriginData, setFaceOriginData] = useState([]);
|
||||||
|
const [isLoadingMore, setLoadingMore] = useState(false);
|
||||||
|
const [isImagePopupOpen, setIsImagePopupOpen] = useState(false);
|
||||||
|
const [imageIndex, setImageIndex] = useState(-1);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const hasMore = useRef(true);
|
||||||
|
|
||||||
|
const faceData = useMemo(() => {
|
||||||
|
if (!Array.isArray(faceOriginData) || faceOriginData.length === 0) return [];
|
||||||
|
const data = faceOriginData.map(dataItem => {
|
||||||
|
const { record_id, link_photos } = dataItem;
|
||||||
|
const linkPhotos = link_photos || [];
|
||||||
|
const name = dataItem.name || gettext('Person Image');
|
||||||
|
return {
|
||||||
|
record_id: record_id,
|
||||||
|
name: name || gettext('Person Image'),
|
||||||
|
photos: linkPhotos.map(photo => {
|
||||||
|
const { path } = photo;
|
||||||
|
return {
|
||||||
|
...photo,
|
||||||
|
name: photo.file_name,
|
||||||
|
url: `${siteRoot}lib/${repoID}/file${path}`,
|
||||||
|
default_url: `${siteRoot}thumbnail/${repoID}/${thumbnailDefaultSize}${path}`,
|
||||||
|
src: `${siteRoot}thumbnail/${repoID}/${thumbnailDefaultSize}${path}`,
|
||||||
|
thumbnail: `${siteRoot}thumbnail/${repoID}/${thumbnailSizeForOriginal}${path}`,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}, [repoID, faceOriginData]);
|
||||||
|
|
||||||
|
const imageItems = useMemo(() => {
|
||||||
|
return faceData.map(group => group.photos).flat();
|
||||||
|
}, [faceData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
metadataAPI.getFaceData(repoID, 0, LIMIT).then(res => {
|
||||||
|
const faceOriginData = res.data.results || [];
|
||||||
|
if (faceOriginData.length < LIMIT) {
|
||||||
|
hasMore.current = false;
|
||||||
|
}
|
||||||
|
setFaceOriginData(faceOriginData);
|
||||||
|
setLoading(false);
|
||||||
|
}).catch(error => {
|
||||||
|
const errorMsg = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errorMsg);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadMore = useCallback(() => {
|
||||||
|
if (!hasMore.current) return;
|
||||||
|
setLoadingMore(true);
|
||||||
|
metadataAPI.getFaceData(repoID, faceOriginData.length, LIMIT).then(res => {
|
||||||
|
const newFaceData = res.data.results || [];
|
||||||
|
if (newFaceData.length < LIMIT) {
|
||||||
|
hasMore.current = false;
|
||||||
|
}
|
||||||
|
setFaceOriginData([...faceOriginData, ...newFaceData]);
|
||||||
|
setLoadingMore(false);
|
||||||
|
}).catch(error => {
|
||||||
|
const errorMsg = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errorMsg);
|
||||||
|
setLoadingMore(false);
|
||||||
|
});
|
||||||
|
}, [repoID, faceOriginData]);
|
||||||
|
|
||||||
|
const handleScroll = useCallback(() => {
|
||||||
|
if (!containerRef.current) return;
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
|
||||||
|
if (scrollTop + clientHeight >= scrollHeight - 10) {
|
||||||
|
loadMore();
|
||||||
|
}
|
||||||
|
}, [loadMore]);
|
||||||
|
|
||||||
|
const onPhotoClick = useCallback((photo) => {
|
||||||
|
let imageIndex = imageItems.findIndex(item => item.url === photo.url);
|
||||||
|
if (imageIndex < 0) imageIndex = 0;
|
||||||
|
setImageIndex(imageIndex);
|
||||||
|
setIsImagePopupOpen(true);
|
||||||
|
}, [imageItems]);
|
||||||
|
|
||||||
|
const closeImagePopup = useCallback(() => {
|
||||||
|
setIsImagePopupOpen(false);
|
||||||
|
setImageIndex(-1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const moveToPrevImage = useCallback(() => {
|
||||||
|
let prevImageIndex = imageIndex - 1;
|
||||||
|
if (prevImageIndex < 0) prevImageIndex = imageItems.length - 1;
|
||||||
|
setImageIndex(prevImageIndex);
|
||||||
|
}, [imageIndex, imageItems]);
|
||||||
|
|
||||||
|
const moveToNextImage = useCallback(() => {
|
||||||
|
let nextImageIndex = imageIndex + 1;
|
||||||
|
if (nextImageIndex > imageItems.length - 1) nextImageIndex = 0;
|
||||||
|
setImageIndex(nextImageIndex);
|
||||||
|
}, [imageIndex, imageItems]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (<CenteredLoading />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="sf-metadata-wrapper">
|
||||||
|
<div className="sf-metadata-main">
|
||||||
|
<div className="sf-metadata-container">
|
||||||
|
<div className="sf-metadata-face-recognition" ref={containerRef} onScroll={handleScroll}>
|
||||||
|
{faceData.length > 0 && faceData.map((face) => {
|
||||||
|
return (<FaceGroup key={face.record_id} group={face} repoID={repoID} onPhotoClick={onPhotoClick} />);
|
||||||
|
})}
|
||||||
|
{isLoadingMore && (
|
||||||
|
<div className="sf-metadata-face-recognition-loading-more">
|
||||||
|
<CenteredLoading />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isImagePopupOpen && (
|
||||||
|
<ModalPortal>
|
||||||
|
<ImageDialog
|
||||||
|
imageItems={imageItems}
|
||||||
|
imageIndex={imageIndex}
|
||||||
|
closeImagePopup={closeImagePopup}
|
||||||
|
moveToPrevImage={moveToPrevImage}
|
||||||
|
moveToNextImage={moveToNextImage}
|
||||||
|
/>
|
||||||
|
</ModalPortal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
FaceRecognition.propTypes = {
|
||||||
|
repoID: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FaceRecognition;
|
@@ -340,17 +340,17 @@ const Gallery = () => {
|
|||||||
onDownload={handleDownload}
|
onDownload={handleDownload}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
/>
|
/>
|
||||||
{isImagePopupOpen &&
|
{isImagePopupOpen && (
|
||||||
<ModalPortal>
|
<ModalPortal>
|
||||||
<ImageDialog
|
<ImageDialog
|
||||||
imageItems={imageItems}
|
imageItems={imageItems}
|
||||||
imageIndex={imageIndex}
|
imageIndex={imageIndex}
|
||||||
closeImagePopup={closeImagePopup}
|
closeImagePopup={closeImagePopup}
|
||||||
moveToPrevImage={moveToPrevImage}
|
moveToPrevImage={moveToPrevImage}
|
||||||
moveToNextImage={moveToNextImage}
|
moveToNextImage={moveToNextImage}
|
||||||
/>
|
/>
|
||||||
</ModalPortal>
|
</ModalPortal>
|
||||||
}
|
)}
|
||||||
{isZipDialogOpen &&
|
{isZipDialogOpen &&
|
||||||
<ModalPortal>
|
<ModalPortal>
|
||||||
<ZipDownloadDialog
|
<ZipDownloadDialog
|
||||||
|
@@ -22,7 +22,7 @@ import DeleteFolderDialog from '../../components/dialog/delete-folder-dialog';
|
|||||||
import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
|
import { EVENT_BUS_TYPE } from '../../components/common/event-bus-type';
|
||||||
import { PRIVATE_FILE_TYPE } from '../../constants';
|
import { PRIVATE_FILE_TYPE } from '../../constants';
|
||||||
import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks';
|
import { MetadataProvider, CollaboratorsProvider } from '../../metadata/hooks';
|
||||||
import { LIST_MODE, METADATA_MODE } from '../../components/dir-view-mode/constants';
|
import { LIST_MODE, METADATA_MODE, FACE_RECOGNITION_MODE } from '../../components/dir-view-mode/constants';
|
||||||
import CurDirPath from '../../components/cur-dir-path';
|
import CurDirPath from '../../components/cur-dir-path';
|
||||||
import DirTool from '../../components/cur-dir-path/dir-tool';
|
import DirTool from '../../components/cur-dir-path/dir-tool';
|
||||||
import DetailContainer from '../../components/dirent-detail/detail-container';
|
import DetailContainer from '../../components/dirent-detail/detail-container';
|
||||||
@@ -555,6 +555,19 @@ class LibContentView extends React.Component {
|
|||||||
window.history.pushState({ url: url, path: '' }, '', url);
|
window.history.pushState({ url: url, path: '' }, '', url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
showFaceRecognition = (filePath, viewId) => {
|
||||||
|
const repoID = this.props.repoID;
|
||||||
|
const repoInfo = this.state.currentRepoInfo;
|
||||||
|
this.setState({
|
||||||
|
currentMode: FACE_RECOGNITION_MODE,
|
||||||
|
path: filePath,
|
||||||
|
viewId: viewId,
|
||||||
|
isDirentDetailShow: false
|
||||||
|
});
|
||||||
|
const url = `${siteRoot}library/${repoID}/${encodeURIComponent(repoInfo.repo_name)}/?view=${encodeURIComponent(viewId)}`;
|
||||||
|
window.history.pushState({ url: url, path: '' }, '', url);
|
||||||
|
};
|
||||||
|
|
||||||
hideFileMetadata = () => {
|
hideFileMetadata = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
currentMode: LIST_MODE,
|
currentMode: LIST_MODE,
|
||||||
@@ -1890,6 +1903,10 @@ class LibContentView extends React.Component {
|
|||||||
if (node.path !== this.state.path) {
|
if (node.path !== this.state.path) {
|
||||||
this.showFileMetadata(node.path, node.view_id || '0000');
|
this.showFileMetadata(node.path, node.view_id || '0000');
|
||||||
}
|
}
|
||||||
|
} else if (Utils.isFaceRecognition(node?.object?.type)) {
|
||||||
|
if (node.path !== this.state.path) {
|
||||||
|
this.showFaceRecognition(node.path, node.view_id || '0000');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(node.path);
|
let url = siteRoot + 'lib/' + repoID + '/file' + Utils.encodePath(node.path);
|
||||||
let dirent = node.object;
|
let dirent = node.object;
|
||||||
@@ -2027,7 +2044,7 @@ class LibContentView extends React.Component {
|
|||||||
isDirentSelected: false,
|
isDirentSelected: false,
|
||||||
isAllDirentSelected: false,
|
isAllDirentSelected: false,
|
||||||
});
|
});
|
||||||
if (this.state.currentMode === METADATA_MODE) {
|
if (this.state.currentMode === METADATA_MODE || this.state.currentMode === FACE_RECOGNITION_MODE) {
|
||||||
this.setState({
|
this.setState({
|
||||||
currentMode: cookie.load('seafile_view_mode') || LIST_MODE,
|
currentMode: cookie.load('seafile_view_mode') || LIST_MODE,
|
||||||
});
|
});
|
||||||
|
@@ -154,6 +154,10 @@ export const Utils = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isFaceRecognition: function (type) {
|
||||||
|
return type === PRIVATE_FILE_TYPE.FACE_RECOGNITION;
|
||||||
|
},
|
||||||
|
|
||||||
getShareLinkPermissionList: function (itemType, permission, path, canEdit) {
|
getShareLinkPermissionList: function (itemType, permission, path, canEdit) {
|
||||||
// itemType: library, dir, file
|
// itemType: library, dir, file
|
||||||
// permission: rw, r, admin, cloud-edit, preview, custom-*
|
// permission: rw, r, admin, cloud-edit, preview, custom-*
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from rest_framework.authentication import SessionAuthentication
|
from rest_framework.authentication import SessionAuthentication
|
||||||
@@ -12,7 +13,7 @@ from seahub.api2.authentication import TokenAuthentication
|
|||||||
from seahub.repo_metadata.models import RepoMetadata, RepoMetadataViews
|
from seahub.repo_metadata.models import RepoMetadata, RepoMetadataViews
|
||||||
from seahub.views import check_folder_permission
|
from seahub.views import check_folder_permission
|
||||||
from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id, init_metadata, \
|
from seahub.repo_metadata.utils import add_init_metadata_task, gen_unique_id, init_metadata, \
|
||||||
get_unmodifiable_columns, can_read_metadata
|
get_unmodifiable_columns, can_read_metadata, init_faces, add_init_face_recognition_task, get_metadata_by_faces
|
||||||
from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records
|
from seahub.repo_metadata.metadata_server_api import MetadataServerAPI, list_metadata_view_records
|
||||||
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
||||||
from seahub.utils.repo import is_repo_admin
|
from seahub.utils.repo import is_repo_admin
|
||||||
@@ -815,3 +816,248 @@ class MetadataViewsMoveView(APIView):
|
|||||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
return Response({'navigation': results['navigation']})
|
return Response({'navigation': results['navigation']})
|
||||||
|
|
||||||
|
|
||||||
|
class FacesRecords(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def get(self, request, repo_id):
|
||||||
|
start = request.GET.get('start', 0)
|
||||||
|
limit = request.GET.get('limit', 100)
|
||||||
|
|
||||||
|
try:
|
||||||
|
start = int(start)
|
||||||
|
limit = int(limit)
|
||||||
|
except:
|
||||||
|
start = 0
|
||||||
|
limit = 1000
|
||||||
|
|
||||||
|
if start < 0:
|
||||||
|
error_msg = 'start invalid'
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
if limit < 0:
|
||||||
|
error_msg = 'limit 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:
|
||||||
|
error_msg = f'The metadata module is disabled for repo {repo_id}.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
permission = check_folder_permission(request, repo_id, '/')
|
||||||
|
if not permission:
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, 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)
|
||||||
|
|
||||||
|
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
|
||||||
|
|
||||||
|
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata = metadata_server_api.get_metadata()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
tables = metadata.get('tables', [])
|
||||||
|
faces_table_id = [table['id'] for table in tables if table['name'] == FACES_TABLE.name]
|
||||||
|
faces_table_id = faces_table_id[0] if faces_table_id else None
|
||||||
|
if not faces_table_id:
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, 'face recognition not be used')
|
||||||
|
|
||||||
|
sql = f'SELECT * FROM `{FACES_TABLE.name}` WHERE `{FACES_TABLE.columns.photo_links.name}` IS NOT NULL LIMIT {start}, {limit}'
|
||||||
|
|
||||||
|
try:
|
||||||
|
query_result = metadata_server_api.query_rows(sql)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
faces = query_result.get('results')
|
||||||
|
|
||||||
|
if not faces:
|
||||||
|
error_msg = 'Records not found'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
query_result = get_metadata_by_faces(faces, metadata_server_api)
|
||||||
|
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 query_result:
|
||||||
|
error_msg = 'Records not found'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
classify_result = dict()
|
||||||
|
for row in query_result:
|
||||||
|
link_row_ids = [item['row_id'] for item in row.get(METADATA_TABLE.columns.face_links.name, [])]
|
||||||
|
if not link_row_ids:
|
||||||
|
continue
|
||||||
|
for link_row_id in link_row_ids:
|
||||||
|
if link_row_id not in classify_result:
|
||||||
|
classify_result[link_row_id] = []
|
||||||
|
file_name = row.get(METADATA_TABLE.columns.file_name.name, '')
|
||||||
|
parent_dir = row.get(METADATA_TABLE.columns.parent_dir.name, '')
|
||||||
|
size = row.get(METADATA_TABLE.columns.size.name, 0)
|
||||||
|
mtime = row.get('_mtime')
|
||||||
|
classify_result[link_row_id].append({
|
||||||
|
'path': os.path.join(parent_dir, file_name),
|
||||||
|
'file_name': file_name,
|
||||||
|
'parent_dir': parent_dir,
|
||||||
|
'size': size,
|
||||||
|
'mtime': mtime
|
||||||
|
})
|
||||||
|
|
||||||
|
id_to_name = {item.get(FACES_TABLE.columns.id.name): item.get(FACES_TABLE.columns.name.name, '') for item in faces}
|
||||||
|
classify_result = [{
|
||||||
|
'record_id': key,
|
||||||
|
'name': id_to_name.get(key, ''),
|
||||||
|
'link_photos': value
|
||||||
|
} for key, value in classify_result.items()]
|
||||||
|
return Response({'results': classify_result})
|
||||||
|
|
||||||
|
|
||||||
|
class FacesRecord(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def put(self, request, repo_id):
|
||||||
|
name = request.data.get('name')
|
||||||
|
record_id = request.data.get('record_id')
|
||||||
|
metadata = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not metadata or not metadata.enabled:
|
||||||
|
error_msg = f'The metadata module is not enabled 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)
|
||||||
|
|
||||||
|
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
|
||||||
|
from seafevents.repo_metadata.utils import FACES_TABLE
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata = metadata_server_api.get_metadata()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
tables = metadata.get('tables', [])
|
||||||
|
faces_table_id = [table['id'] for table in tables if table['name'] == FACES_TABLE.name]
|
||||||
|
faces_table_id = faces_table_id[0] if faces_table_id else None
|
||||||
|
if not faces_table_id:
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, 'face recognition not be used')
|
||||||
|
|
||||||
|
sql = f'SELECT * FROM `{FACES_TABLE.name}` WHERE `{FACES_TABLE.columns.id.name}` = "{record_id}"'
|
||||||
|
try:
|
||||||
|
results = metadata_server_api.query_rows(sql).get('results', [])
|
||||||
|
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 results:
|
||||||
|
error_msg = 'Record not found'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
update_row = {
|
||||||
|
FACES_TABLE.columns.id.name: record_id,
|
||||||
|
FACES_TABLE.columns.name.name: name,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata_server_api.update_rows(faces_table_id, [update_row])
|
||||||
|
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})
|
||||||
|
|
||||||
|
|
||||||
|
class FaceRecognitionManage(APIView):
|
||||||
|
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||||
|
permission_classes = (IsAuthenticated, )
|
||||||
|
throttle_classes = (UserRateThrottle, )
|
||||||
|
|
||||||
|
def get(self, request, repo_id):
|
||||||
|
# recource 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)
|
||||||
|
|
||||||
|
metadata = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not metadata or not metadata.enabled:
|
||||||
|
error_msg = f'The metadata module is not enabled for repo {repo_id}.'
|
||||||
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
|
# permission check
|
||||||
|
if not can_read_metadata(request, repo_id):
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||||
|
|
||||||
|
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
|
||||||
|
from seafevents.repo_metadata.utils import FACES_TABLE
|
||||||
|
|
||||||
|
try:
|
||||||
|
metadata = metadata_server_api.get_metadata()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
|
||||||
|
tables = metadata.get('tables', [])
|
||||||
|
faces_table_id = [table['id'] for table in tables if table['name'] == FACES_TABLE.name]
|
||||||
|
is_enabled = True if faces_table_id else False
|
||||||
|
|
||||||
|
return Response({'enabled': is_enabled})
|
||||||
|
|
||||||
|
def post(self, request, repo_id):
|
||||||
|
metadata = RepoMetadata.objects.filter(repo_id=repo_id).first()
|
||||||
|
if not metadata or not metadata.enabled:
|
||||||
|
error_msg = f'The metadata module is not enabled 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)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'repo_id': repo_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata_server_api = MetadataServerAPI(repo_id, request.user.username)
|
||||||
|
init_faces(metadata_server_api)
|
||||||
|
|
||||||
|
try:
|
||||||
|
task_id = add_init_face_recognition_task(params=params)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')
|
||||||
|
|
||||||
|
return Response({'task_id': task_id})
|
||||||
|
@@ -171,3 +171,16 @@ class MetadataServerAPI:
|
|||||||
}
|
}
|
||||||
response = requests.put(url, json=data, headers=self.headers, timeout=self.timeout)
|
response = requests.put(url, json=data, headers=self.headers, timeout=self.timeout)
|
||||||
return parse_response(response)
|
return parse_response(response)
|
||||||
|
|
||||||
|
def create_table(self, table_name):
|
||||||
|
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/tables'
|
||||||
|
data = {
|
||||||
|
'name': table_name,
|
||||||
|
}
|
||||||
|
response = requests.post(url, json=data, headers=self.headers, timeout=self.timeout)
|
||||||
|
return parse_response(response)
|
||||||
|
|
||||||
|
def get_metadata(self):
|
||||||
|
url = f'{METADATA_SERVER_URL}/api/v1/base/{self.base_id}/metadata'
|
||||||
|
response = requests.get(url, headers=self.headers, timeout=self.timeout)
|
||||||
|
return parse_response(response)
|
||||||
|
@@ -20,6 +20,42 @@ def add_init_metadata_task(params):
|
|||||||
return json.loads(resp.content)['task_id']
|
return json.loads(resp.content)['task_id']
|
||||||
|
|
||||||
|
|
||||||
|
def add_init_face_recognition_task(params):
|
||||||
|
payload = {'exp': int(time.time()) + 300, }
|
||||||
|
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
|
||||||
|
headers = {"Authorization": "Token %s" % token}
|
||||||
|
url = urljoin(SEAFEVENTS_SERVER_URL, '/add-init-face-recognition-task')
|
||||||
|
resp = requests.get(url, params=params, headers=headers)
|
||||||
|
return json.loads(resp.content)['task_id']
|
||||||
|
|
||||||
|
|
||||||
|
def get_metadata_by_faces(faces, metadata_server_api):
|
||||||
|
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
|
||||||
|
sql = f'SELECT * FROM `{METADATA_TABLE.name}` WHERE `{METADATA_TABLE.columns.id.name}` IN ('
|
||||||
|
parameters = []
|
||||||
|
query_result = []
|
||||||
|
for face in faces:
|
||||||
|
link_row_ids = [item['row_id'] for item in face.get(FACES_TABLE.columns.photo_links.name, [])]
|
||||||
|
if not link_row_ids:
|
||||||
|
continue
|
||||||
|
for link_row_id in link_row_ids:
|
||||||
|
sql += '?, '
|
||||||
|
parameters.append(link_row_id)
|
||||||
|
if len(parameters) >= 10000:
|
||||||
|
sql = sql.rstrip(', ') + ');'
|
||||||
|
results = metadata_server_api.query_rows(sql, parameters).get('results', [])
|
||||||
|
query_result.extend(results)
|
||||||
|
sql = f'SELECT * FROM `{METADATA_TABLE.name}` WHERE `{METADATA_TABLE.columns.id.name}` IN ('
|
||||||
|
parameters = []
|
||||||
|
|
||||||
|
if parameters:
|
||||||
|
sql = sql.rstrip(', ') + ');'
|
||||||
|
results = metadata_server_api.query_rows(sql, parameters).get('results', [])
|
||||||
|
query_result.extend(results)
|
||||||
|
|
||||||
|
return query_result
|
||||||
|
|
||||||
|
|
||||||
def generator_base64_code(length=4):
|
def generator_base64_code(length=4):
|
||||||
possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123456789'
|
possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz0123456789'
|
||||||
ids = random.sample(possible, length)
|
ids = random.sample(possible, length)
|
||||||
@@ -57,6 +93,36 @@ def get_sys_columns():
|
|||||||
return columns
|
return columns
|
||||||
|
|
||||||
|
|
||||||
|
def get_link_column(face_table_id):
|
||||||
|
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
|
||||||
|
columns = [
|
||||||
|
METADATA_TABLE.columns.face_links.to_dict({
|
||||||
|
'link_id': FACES_TABLE.link_id,
|
||||||
|
'table_id': METADATA_TABLE.id,
|
||||||
|
'other_table_id': face_table_id,
|
||||||
|
'display_column_key': FACES_TABLE.columns.name.key,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
return columns
|
||||||
|
|
||||||
|
|
||||||
|
def get_face_columns(face_table_id):
|
||||||
|
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
|
||||||
|
columns = [
|
||||||
|
FACES_TABLE.columns.photo_links.to_dict({
|
||||||
|
'link_id': FACES_TABLE.link_id,
|
||||||
|
'table_id': METADATA_TABLE.id,
|
||||||
|
'other_table_id': face_table_id,
|
||||||
|
'display_column_key': METADATA_TABLE.columns.obj_id.key,
|
||||||
|
}),
|
||||||
|
FACES_TABLE.columns.vector.to_dict(),
|
||||||
|
FACES_TABLE.columns.name.to_dict(),
|
||||||
|
]
|
||||||
|
|
||||||
|
return columns
|
||||||
|
|
||||||
|
|
||||||
def get_unmodifiable_columns():
|
def get_unmodifiable_columns():
|
||||||
from seafevents.repo_metadata.utils import METADATA_TABLE
|
from seafevents.repo_metadata.utils import METADATA_TABLE
|
||||||
columns = [
|
columns = [
|
||||||
@@ -90,6 +156,20 @@ def init_metadata(metadata_server_api):
|
|||||||
metadata_server_api.add_columns(METADATA_TABLE.id, sys_columns)
|
metadata_server_api.add_columns(METADATA_TABLE.id, sys_columns)
|
||||||
|
|
||||||
|
|
||||||
|
def init_faces(metadata_server_api):
|
||||||
|
from seafevents.repo_metadata.utils import METADATA_TABLE, FACES_TABLE
|
||||||
|
|
||||||
|
resp = metadata_server_api.create_table(FACES_TABLE.name)
|
||||||
|
|
||||||
|
# init link column
|
||||||
|
link_column = get_link_column(resp['id'])
|
||||||
|
metadata_server_api.add_columns(METADATA_TABLE.id, link_column)
|
||||||
|
|
||||||
|
# init face column
|
||||||
|
face_columns = get_face_columns(resp['id'])
|
||||||
|
metadata_server_api.add_columns(resp['id'], face_columns)
|
||||||
|
|
||||||
|
|
||||||
def get_file_download_token(repo_id, file_id, username):
|
def get_file_download_token(repo_id, file_id, username):
|
||||||
return seafile_api.get_fileserver_access_token(repo_id, file_id, 'download', username, use_onetime=True)
|
return seafile_api.get_fileserver_access_token(repo_id, file_id, 'download', username, use_onetime=True)
|
||||||
|
|
||||||
|
@@ -211,7 +211,8 @@ from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView,
|
|||||||
Wiki2DuplicatePageView, WikiPageTrashView, Wiki2PublishView, Wiki2PublishConfigView, Wiki2PublishPageView
|
Wiki2DuplicatePageView, WikiPageTrashView, Wiki2PublishView, Wiki2PublishConfigView, Wiki2PublishPageView
|
||||||
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
|
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
|
||||||
from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
|
from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
|
||||||
MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView
|
MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView, FacesRecords, \
|
||||||
|
FaceRecognitionManage, FacesRecord
|
||||||
from seahub.api2.endpoints.user_list import UserListView
|
from seahub.api2.endpoints.user_list import UserListView
|
||||||
from seahub.api2.endpoints.seahub_io import SeahubIOStatus
|
from seahub.api2.endpoints.seahub_io import SeahubIOStatus
|
||||||
|
|
||||||
@@ -1052,6 +1053,9 @@ if settings.ENABLE_METADATA_MANAGEMENT:
|
|||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/views/(?P<view_id>[-0-9a-zA-Z]{4})/$', MetadataViewsDetailView.as_view(), name='api-v2.1-metadata-views-detail'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/views/(?P<view_id>[-0-9a-zA-Z]{4})/$', MetadataViewsDetailView.as_view(), name='api-v2.1-metadata-views-detail'),
|
||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/move-views/$', MetadataViewsMoveView.as_view(), name='api-v2.1-metadata-views-move'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/move-views/$', MetadataViewsMoveView.as_view(), name='api-v2.1-metadata-views-move'),
|
||||||
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/duplicate-view/$', MetadataViewsDuplicateView.as_view(), name='api-v2.1-metadata-view-duplicate'),
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/duplicate-view/$', MetadataViewsDuplicateView.as_view(), name='api-v2.1-metadata-view-duplicate'),
|
||||||
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-record/$', FacesRecord.as_view(), name='api-v2.1-metadata-face-record'),
|
||||||
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-records/$', FacesRecords.as_view(), name='api-v2.1-metadata-face-records'),
|
||||||
|
re_path(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/metadata/face-recognition/$', FaceRecognitionManage.as_view(), name='api-v2.1-metadata-face-recognition'),
|
||||||
]
|
]
|
||||||
|
|
||||||
# ai API
|
# ai API
|
||||||
|
Reference in New Issue
Block a user