diff --git a/frontend/src/app.js b/frontend/src/app.js index 084e4658a9..fd86c7261e 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -45,8 +45,6 @@ const SharedWithOCMWrapper = MainContentWrapper(ShareWithOCM); const OCMViaWebdavWrapper = MainContentWrapper(OCMViaWebdav); const ShareAdminLibrariesWrapper = MainContentWrapper(ShareAdminLibraries); const ShareAdminFoldersWrapper = MainContentWrapper(ShareAdminFolders); -const ShareAdminShareLinksWrapper = MainContentWrapper(ShareAdminShareLinks); -const ShareAdminUploadLinksWrapper = MainContentWrapper(ShareAdminUploadLinks); class App extends Component { @@ -259,8 +257,8 @@ class App extends Component { - - + + diff --git a/frontend/src/components/toolbar/top-toolbar.js b/frontend/src/components/toolbar/top-toolbar.js new file mode 100644 index 0000000000..99e42ff584 --- /dev/null +++ b/frontend/src/components/toolbar/top-toolbar.js @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import CommonToolbar from './common-toolbar'; + +const propTypes = { + onShowSidePanel: PropTypes.func.isRequired, + onSearchedClick: PropTypes.func.isRequired, + searchPlaceholder: PropTypes.string, + children: PropTypes.object +}; + +class TopToolbar extends React.Component { + + render() { + const { onShowSidePanel, onSearchedClick } = this.props; + return ( +
+
+ + + {this.props.children} +
+ +
+ ); + } +} + +TopToolbar.propTypes = propTypes; + +export default TopToolbar; diff --git a/frontend/src/models/share-link.js b/frontend/src/models/share-link.js index 72b414ac40..80f2916d0f 100644 --- a/frontend/src/models/share-link.js +++ b/frontend/src/models/share-link.js @@ -5,6 +5,7 @@ class ShareLink { this.repo_name = object.repo_name; this.path = object.path; this.obj_name = object.obj_name; + this.obj_id = object.obj_id; this.is_dir = object.is_dir; this.can_edit = object.can_edit; this.repo_folder_permission = object.repo_folder_permission; diff --git a/frontend/src/models/upload-link.js b/frontend/src/models/upload-link.js index ea01cec8cf..0d23125089 100644 --- a/frontend/src/models/upload-link.js +++ b/frontend/src/models/upload-link.js @@ -6,6 +6,7 @@ class UploadLink { this.path = object.path; this.link = object.link; this.obj_name = object.obj_name; + this.obj_id = object.obj_id; this.username = object.username; this.ctime = object.ctime; this.token = object.token; diff --git a/frontend/src/pages/share-admin/share-links.js b/frontend/src/pages/share-admin/share-links.js index 0c43ef1f63..c61a20bca9 100644 --- a/frontend/src/pages/share-admin/share-links.js +++ b/frontend/src/pages/share-admin/share-links.js @@ -2,7 +2,7 @@ import React, { Component, Fragment } from 'react'; import PropTypes from 'prop-types'; import { Link } from '@gatsbyjs/reach-router'; import moment from 'moment'; -import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap'; +import { Dropdown, DropdownToggle, DropdownItem, Button } from 'reactstrap'; import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; import { isPro, gettext, siteRoot, canGenerateUploadLink } from '../../utils/constants'; @@ -14,6 +14,18 @@ import EmptyTip from '../../components/empty-tip'; import ShareLinkPermissionSelect from '../../components/dialog/share-link-permission-select'; import ShareAdminLink from '../../components/dialog/share-admin-link'; import SortOptionsDialog from '../../components/dialog/sort-options'; +import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog'; +import TopToolbar from '../../components/toolbar/top-toolbar'; + +const contentPropTypes = { + loading: PropTypes.bool.isRequired, + errorMsg: PropTypes.string.isRequired, + items: PropTypes.array.isRequired, + sortBy: PropTypes.string.isRequired, + sortOrder: PropTypes.string.isRequired, + sortItems: PropTypes.func.isRequired, + onRemoveLink: PropTypes.func.isRequired +}; class Content extends Component { @@ -88,6 +100,8 @@ class Content extends Component { } } +Content.propTypes = contentPropTypes; + const itemPropTypes = { item: PropTypes.object.isRequired, isDesktop: PropTypes.bool.isRequired, @@ -202,6 +216,7 @@ class Item extends Component { objUrl = `${siteRoot}lib/${item.repo_id}/file${Utils.encodePath(item.path)}`; } + const deletedTip = item.obj_id === '' ? {gettext('(deleted)')} : null; const desktopItem = ( @@ -210,6 +225,7 @@ class Item extends Component { {item.obj_name} : {item.obj_name} } + {deletedTip} {item.repo_name} {isPro && @@ -294,11 +310,17 @@ class Item extends Component { Item.propTypes = itemPropTypes; +const propTypes = { + onShowSidePanel: PropTypes.func.isRequired, + onSearchedClick: PropTypes.func.isRequired +}; + class ShareAdminShareLinks extends Component { constructor(props) { super(props); this.state = { + isCleanOrphanShareLinksDialogOpen: false, loading: true, errorMsg: '', items: [], @@ -366,6 +388,10 @@ class ShareAdminShareLinks extends Component { } componentDidMount() { + this.listUserShareLinks(); + } + + listUserShareLinks() { seafileAPI.listUserShareLinks().then((res) => { let items = res.data.map(item => { return new ShareLink(item); @@ -402,9 +428,31 @@ class ShareAdminShareLinks extends Component { }); } + toggleCleanOrphanShareLinksDialog = () => { + this.setState({isCleanOrphanShareLinksDialogOpen: !this.state.isCleanOrphanShareLinksDialogOpen}); + } + + cleanOrphanShareLinks = () => { + seafileAPI.cleanOrphanShareLinks().then(res => { + const newItems = this.state.items.filter(item => item.obj_id !== ''); + this.setState({items: newItems}); + toaster.success(gettext('Successfully cleaned orphan share links.')); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + render() { return ( + + + +
@@ -440,9 +488,20 @@ class ShareAdminShareLinks extends Component { sortItems={this.sortItems} /> } + {this.state.isCleanOrphanShareLinksDialogOpen && + + } ); } } +ShareAdminShareLinks.propTypes = propTypes; + export default ShareAdminShareLinks; diff --git a/frontend/src/pages/share-admin/upload-links.js b/frontend/src/pages/share-admin/upload-links.js index 1692d1da6e..b835f37048 100644 --- a/frontend/src/pages/share-admin/upload-links.js +++ b/frontend/src/pages/share-admin/upload-links.js @@ -1,7 +1,8 @@ import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; import { Link } from '@gatsbyjs/reach-router'; import moment from 'moment'; -import { Dropdown, DropdownToggle, DropdownItem } from 'reactstrap'; +import { Dropdown, DropdownToggle, DropdownItem, Button } from 'reactstrap'; import { gettext, siteRoot, canGenerateShareLink } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; import { Utils } from '../../utils/utils'; @@ -10,6 +11,15 @@ import Loading from '../../components/loading'; import EmptyTip from '../../components/empty-tip'; import UploadLink from '../../models/upload-link'; import ShareAdminLink from '../../components/dialog/share-admin-link'; +import CommonOperationConfirmationDialog from '../../components/dialog/common-operation-confirmation-dialog'; +import TopToolbar from '../../components/toolbar/top-toolbar'; + +const contentPropTypes = { + loading: PropTypes.bool.isRequired, + errorMsg: PropTypes.string.isRequired, + items: PropTypes.array.isRequired, + onRemoveLink: PropTypes.func.isRequired +}; class Content extends Component { @@ -63,6 +73,14 @@ class Content extends Component { } } +Content.propTypes = contentPropTypes; + +const itemPropTypes = { + isDesktop: PropTypes.bool.isRequired, + item: PropTypes.object.isRequired, + onRemoveLink: PropTypes.func.isRequired +}; + class Item extends Component { constructor(props) { @@ -122,10 +140,11 @@ class Item extends Component { const repoUrl = `${siteRoot}library/${item.repo_id}/${encodeURIComponent(item.repo_name)}`; const objUrl = `${repoUrl}${Utils.encodePath(item.path)}`; + const deletedTip = item.obj_id === '' ? {gettext('(deleted)')} : null; const desktopItem = ( - {item.obj_name} + {item.obj_name}{deletedTip} {item.repo_name} {item.view_cnt} {this.renderExpiration()} @@ -180,11 +199,19 @@ class Item extends Component { } } +Item.propTypes = itemPropTypes; + +const propTypes = { + onShowSidePanel: PropTypes.func.isRequired, + onSearchedClick: PropTypes.func.isRequired +}; + class ShareAdminUploadLinks extends Component { constructor(props) { super(props); this.state = { + isCleanOrphanUploadLinksDialogOpen: false, loading: true, errorMsg: '', items: [] @@ -192,6 +219,10 @@ class ShareAdminUploadLinks extends Component { } componentDidMount() { + this.listUserUploadLinks(); + } + + listUserUploadLinks() { seafileAPI.listUserUploadLinks().then((res) => { let items = res.data.map(item => { return new UploadLink(item); @@ -222,30 +253,64 @@ class ShareAdminUploadLinks extends Component { }); } + toggleCleanOrphanUploadLinksDialog = () => { + this.setState({isCleanOrphanUploadLinksDialogOpen: !this.state.isCleanOrphanUploadLinksDialogOpen}); + } + + cleanOrphanUploadLinks = () => { + seafileAPI.cleanOrphanUploadLinks().then(res => { + const newItems = this.state.items.filter(item => item.obj_id !== ''); + this.setState({items: newItems}); + toaster.success(gettext('Successfully cleaned orphan upload links.')); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + render() { return ( -
-
-
-
    - {canGenerateShareLink && ( -
  • {gettext('Share Links')}
  • - )} -
  • {gettext('Upload Links')}
  • -
-
-
- + + + + +
+
+
+
    + {canGenerateShareLink && ( +
  • {gettext('Share Links')}
  • + )} +
  • {gettext('Upload Links')}
  • +
+
+
+ +
-
+ {this.state.isCleanOrphanUploadLinksDialogOpen && + + } + ); } } +ShareAdminUploadLinks.propTypes = propTypes; + export default ShareAdminUploadLinks; diff --git a/seahub/api2/endpoints/share_links.py b/seahub/api2/endpoints/share_links.py index a763fde1c9..fb8c6ba2d7 100644 --- a/seahub/api2/endpoints/share_links.py +++ b/seahub/api2/endpoints/share_links.py @@ -73,6 +73,13 @@ def get_share_link_info(fileshare): else: obj_name = '' + if fileshare.s_type == 'd': + folder_path = normalize_dir_path(fileshare.path) + obj_id = seafile_api.get_dir_id_by_path(repo_id, folder_path) + else: + file_path = normalize_file_path(fileshare.path) + obj_id = seafile_api.get_file_id_by_path(repo_id, file_path) + if fileshare.expire_date: expire_date = datetime_to_isoformat_timestr(fileshare.expire_date) else: @@ -89,6 +96,7 @@ def get_share_link_info(fileshare): data['path'] = path data['obj_name'] = obj_name + data['obj_id'] = obj_id or "" data['is_dir'] = True if fileshare.s_type == 'd' else False data['token'] = token @@ -1327,3 +1335,37 @@ class ShareLinkRepoTagsTaggedFiles(APIView): filtered_tagged_files.append(tagged_file) return Response({'tagged_files': filtered_tagged_files}) + + +class ShareLinksCleanOrphan(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, CanGenerateShareLink) + throttle_classes = (UserRateThrottle,) + + def delete(self, request): + """ Clean orphan share links. + """ + + username = request.user.username + share_links = FileShare.objects.filter(username=username) + + for share_link in share_links: + + repo_id = share_link.repo_id + if not seafile_api.get_repo(repo_id): + share_link.delete() + continue + + if share_link.s_type == 'd': + folder_path = normalize_dir_path(share_link.path) + obj_id = seafile_api.get_dir_id_by_path(repo_id, folder_path) + else: + file_path = normalize_file_path(share_link.path) + obj_id = seafile_api.get_file_id_by_path(repo_id, file_path) + + if not obj_id: + share_link.delete() + continue + + return Response({'success': True}) diff --git a/seahub/api2/endpoints/upload_links.py b/seahub/api2/endpoints/upload_links.py index b196c51ddb..fbbfa32ddd 100644 --- a/seahub/api2/endpoints/upload_links.py +++ b/seahub/api2/endpoints/upload_links.py @@ -27,6 +27,7 @@ from seahub.api2.permissions import CanGenerateUploadLink from seahub.share.models import UploadLinkShare, check_share_link_common from seahub.utils import gen_shared_upload_link, gen_file_upload_url, \ is_pro_version, get_password_strength_level, is_valid_password + from seahub.views import check_folder_permission from seahub.utils.timeutils import datetime_to_isoformat_timestr @@ -54,6 +55,8 @@ def get_upload_link_info(uls): else: obj_name = '' + obj_id = seafile_api.get_dir_id_by_path(repo_id, path) + if uls.ctime: ctime = datetime_to_isoformat_timestr(uls.ctime) else: @@ -68,6 +71,7 @@ def get_upload_link_info(uls): data['repo_name'] = repo.repo_name if repo else '' data['path'] = path data['obj_name'] = obj_name + data['obj_id'] = obj_id or "" data['view_cnt'] = uls.view_cnt data['ctime'] = ctime data['link'] = gen_shared_upload_link(token) @@ -475,3 +479,30 @@ class UploadLinkUpload(APIView): result = {} result['upload_link'] = gen_file_upload_url(token, 'upload-api') return Response(result) + + +class UploadLinksCleanOrphan(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, CanGenerateUploadLink) + throttle_classes = (UserRateThrottle, ) + + def delete(self, request): + """ Clean orphan upload links. + """ + + username = request.user.username + upload_links = UploadLinkShare.objects.filter(username=username) + + for upload_link in upload_links: + + repo_id = upload_link.repo_id + if not seafile_api.get_repo(repo_id): + upload_link.delete() + continue + + obj_id = seafile_api.get_dir_id_by_path(repo_id, upload_link.path) + if not obj_id: + upload_link.delete() + continue + + return Response({'success': True}) diff --git a/seahub/urls.py b/seahub/urls.py index d68243ffaa..6e51ffc2ad 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -40,11 +40,11 @@ from seahub.api2.endpoints.search_group import SearchGroup from seahub.api2.endpoints.share_links import ShareLinks, ShareLink, \ ShareLinkOnlineOfficeLock, ShareLinkDirents, ShareLinkSaveFileToRepo, \ ShareLinkUpload, ShareLinkUploadDone, ShareLinkSaveItemsToRepo, \ - ShareLinkRepoTags, ShareLinkRepoTagsTaggedFiles + ShareLinkRepoTags, ShareLinkRepoTagsTaggedFiles, ShareLinksCleanOrphan from seahub.api2.endpoints.shared_folders import SharedFolders from seahub.api2.endpoints.shared_repos import SharedRepos, SharedRepo from seahub.api2.endpoints.upload_links import UploadLinks, UploadLink, \ - UploadLinkUpload + UploadLinkUpload, UploadLinksCleanOrphan from seahub.api2.endpoints.repos_batch import ReposBatchView, \ ReposBatchCopyDirView, ReposBatchCreateDirView, \ ReposBatchCopyItemView, ReposBatchMoveItemView, \ @@ -341,6 +341,7 @@ urlpatterns = [ ## user::shared-download-links url(r'^api/v2.1/share-links/$', ShareLinks.as_view(), name='api-v2.1-share-links'), + url(r'^api/v2.1/share-links/clean-orphan/$', ShareLinksCleanOrphan.as_view(), name='api-v2.1-share-links-clean-orphan'), url(r'^api/v2.1/share-links/(?P[a-f0-9]+)/$', ShareLink.as_view(), name='api-v2.1-share-link'), url(r'^api/v2.1/share-links/(?P[a-f0-9]+)/save-file-to-repo/$', ShareLinkSaveFileToRepo.as_view(), name='api-v2.1-share-link-save-file-to-repo'), url(r'^api/v2.1/share-links/(?P[a-f0-9]+)/save-items-to-repo/$', ShareLinkSaveItemsToRepo.as_view(), name='api-v2.1-share-link-save-items-to-repo'), @@ -355,6 +356,7 @@ urlpatterns = [ ## user::shared-upload-links url(r'^api/v2.1/upload-links/$', UploadLinks.as_view(), name='api-v2.1-upload-links'), + url(r'^api/v2.1/upload-links/clean-orphan/$', UploadLinksCleanOrphan.as_view(), name='api-v2.1-upload-links-clean-orphan'), url(r'^api/v2.1/upload-links/(?P[a-f0-9]+)/$', UploadLink.as_view(), name='api-v2.1-upload-link'), url(r'^api/v2.1/upload-links/(?P[a-f0-9]+)/upload/$', UploadLinkUpload.as_view(), name='api-v2.1-upload-link-upload'),