From 0981a0dc99699ae9d538d23cfdba5f6a036698fb Mon Sep 17 00:00:00 2001 From: awu0403 <76416779+awu0403@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:44:41 +0800 Subject: [PATCH] update repo trash (#6148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update repo trash * update code * select trash * update * update * merge clean trash * fix-uni-test-and-code-optimize * Update mysql.sql * code-optimize * update select * update sql * update UI * change trash dialog style * optimize code * fix code format * Update repo_trash.py * update * add clean trash Command * update * optimize code * support page * support frontend page * update * Update __init__.py * Update clean_repo_trash.py * Update clean_repo_trash.py * Update clean_repo_trash.py * Update trash-dialog.js * Update clean_repo_trash.py * set default by 90 * Update clean_repo_trash.py * update --------- Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com> Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com> Co-authored-by: Michael An <2331806369@qq.com> --- .../src/components/dialog/trash-dialog.js | 432 ++++++++++++++++++ .../dir-view-mode/dir-column-nav.js | 1 + .../components/dir-view-mode/dir-others.js | 22 +- frontend/src/css/trash-dialog.css | 44 ++ frontend/src/repo-folder-trash.js | 6 +- frontend/src/utils/constants.js | 1 + frontend/src/utils/repo-trash-api.js | 51 +++ seahub/api2/endpoints/repo_trash.py | 90 +++- .../management/commands/clean_repo_trash.py | 41 ++ seahub/handlers.py | 7 +- seahub/templates/base_for_react.html | 1 + seahub/templates/repo_folder_trash_react.html | 2 +- seahub/urls.py | 3 +- seahub/utils/__init__.py | 10 +- seahub/views/__init__.py | 3 +- sql/mysql.sql | 15 + 16 files changed, 714 insertions(+), 15 deletions(-) create mode 100644 frontend/src/components/dialog/trash-dialog.js create mode 100644 frontend/src/css/trash-dialog.css create mode 100644 frontend/src/utils/repo-trash-api.js create mode 100644 seahub/base/management/commands/clean_repo_trash.py diff --git a/frontend/src/components/dialog/trash-dialog.js b/frontend/src/components/dialog/trash-dialog.js new file mode 100644 index 0000000000..f93772cd7d --- /dev/null +++ b/frontend/src/components/dialog/trash-dialog.js @@ -0,0 +1,432 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { navigate } from '@gatsbyjs/reach-router'; +import { Modal, ModalHeader, ModalBody } from 'reactstrap'; +import moment from 'moment'; +import { Utils } from '../../utils/utils'; +import {gettext, siteRoot, enableUserCleanTrash, username} from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import { repotrashAPI } from '../../utils/repo-trash-api'; +import ModalPortal from '../../components/modal-portal'; +import toaster from '../../components/toast'; +import CleanTrash from '../../components/dialog/clean-trash'; +import Paginator from '../paginator'; + +import '../../css/toolbar.css'; +import '../../css/search.css'; +import '../../css/trash-dialog.css'; + +const propTypes = { + repoID: PropTypes.string.isRequired, + currentRepoInfo: PropTypes.object.isRequired, + showTrashDialog: PropTypes.bool.isRequired, + toggleTrashDialog: PropTypes.func.isRequired +}; + +class TrashDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + isLoading: true, + errorMsg: '', + items: [], + scanStat: null, + more: false, + isCleanTrashDialogOpen: false, + trashType: 0, + isOldTrashDialogOpen: false, + currentPage: 1, + perPage: 100, + hasNextPage: false + }; + } + + componentDidMount() { + this.getItems2(); + } + + getItems2 = (page) => { + repotrashAPI.getRepoFolderTrash2(this.props.repoID, page, this.state.perPage).then((res) => { + const { items, total_count } = res.data; + if (!page){ + page = 1; + } + this.setState({ + currentPage: page, + hasNextPage: total_count - page * this.state.perPage > 0, + isLoading: false, + items: items, + more: false + }); + }); + }; + + onSearchedClick = (selectedItem) => { + if (selectedItem.is_dir === true) { + let url = siteRoot + 'library/' + selectedItem.repo_id + '/' + selectedItem.repo_name + selectedItem.path; + navigate(url, {repalce: true}); + } else { + let url = siteRoot + 'lib/' + selectedItem.repo_id + '/file' + Utils.encodePath(selectedItem.path); + let newWindow = window.open('about:blank'); + newWindow.location.href = url; + } + }; + + resetPerPage = (perPage) => { + this.setState({ + perPage: perPage + }, () => { + this.getItems2(1); + }); + }; + cleanTrash = () => { + this.toggleCleanTrashDialog(); + }; + + toggleCleanTrashDialog = () => { + this.setState({ + isCleanTrashDialogOpen: !this.state.isCleanTrashDialogOpen + }); + }; + + refreshTrash2 = () => { + this.setState({ + isLoading: true, + errorMsg: '', + items: [], + scanStat: null, + more: false, + showFolder: false + }); + this.getItems2(); + }; + + renderFolder = (commitID, baseDir, folderPath) => { + this.setState({ + showFolder: true, + commitID: commitID, + baseDir: baseDir, + folderPath: folderPath, + folderItems: [], + isLoading: true + }); + + seafileAPI.listCommitDir(this.props.repoID, commitID, `${baseDir.substr(0, baseDir.length - 1)}${folderPath}`).then((res) => { + this.setState({ + isLoading: false, + folderItems: res.data.dirent_list + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + isLoading: false, + errorMsg: gettext('Permission denied') + }); + } else { + this.setState({ + isLoading: false, + errorMsg: gettext('Error') + }); + } + } else { + this.setState({ + isLoading: false, + errorMsg: gettext('Please check the network.') + }); + } + }); + }; + + render() { + const { showTrashDialog, toggleTrashDialog } = this.props; + const { isCleanTrashDialogOpen, showFolder } = this.state; + const isRepoAdmin = this.props.currentRepoInfo.owner_email === username || this.props.currentRepoInfo.is_admin; + const repoFolderName = this.props.currentRepoInfo.repo_name; + const oldTrashUrl = siteRoot + 'repo/' + this.props.repoID + '/trash/'; + let title = gettext('{placeholder} Trash'); + title = title.replace('{placeholder}', '' + Utils.HTMLescape(repoFolderName) + ''); + + return ( + + + {gettext('Visit old version page')} + {(enableUserCleanTrash && !showFolder && isRepoAdmin) && + + } + + + } + > +
+
+ + + {isCleanTrashDialogOpen && + + + + } + +
+ ); + } +} + +class Content extends React.Component { + + constructor(props) { + super(props); + this.theadData = [ + {width: '5%', text: ''}, + {width: '20%', text: gettext('Name')}, + {width: '40%', text: gettext('Original path')}, + {width: '12%', text: gettext('Delete Time')}, + {width: '13%', text: gettext('Size')}, + {width: '10%', text: ''} + ]; + } + + getPreviousPage = () => { + this.props.getListByPage(this.props.currentPage - 1); + }; + + getNextPage = () => { + this.props.getListByPage(this.props.currentPage + 1); + }; + + render() { + const { items, showFolder, commitID, baseDir, folderPath, folderItems } = this.props.data; + const { + curPerPage, currentPage, hasNextPage + } = this.props; + return ( + + + + + {this.theadData.map((item, index) => { + return ; + })} + + + + {showFolder ? + folderItems.map((item, index) => { + return ; + }) : + items.map((item, index) => { + return ; + })} + +
{item.text}
+ +
+ ); + } +} + +Content.propTypes = { + data: PropTypes.object.isRequired, + getMore: PropTypes.func, + renderFolder: PropTypes.func.isRequired, + repoID: PropTypes.string.isRequired, + getListByPage: PropTypes.func.isRequired, + resetPerPage: PropTypes.func.isRequired, + currentPage: PropTypes.number.isRequired, + curPerPage: PropTypes.number.isRequired, + hasNextPage: PropTypes.bool.isRequired, +}; + + +class Item extends React.Component { + + constructor(props) { + super(props); + this.state = { + restored: false, + isIconShown: false + }; + } + + handleMouseOver = () => { + this.setState({isIconShown: true}); + }; + + handleMouseOut = () => { + this.setState({isIconShown: false}); + }; + + restoreItem = (e) => { + e.preventDefault(); + const item = this.props.item; + const { commit_id, parent_dir, obj_name } = item; + const path = parent_dir + obj_name; + const request = item.is_dir ? + seafileAPI.restoreFolder(this.props.repoID, commit_id, path) : + seafileAPI.restoreFile(this.props.repoID, commit_id, path); + request.then((res) => { + this.setState({ + restored: true + }); + toaster.success(gettext('Successfully restored 1 item.')); + }).catch((error) => { + let errorMsg = ''; + if (error.response) { + errorMsg = error.response.data.error_msg || gettext('Error'); + } else { + errorMsg = gettext('Please check the network.'); + } + toaster.danger(errorMsg); + }); + }; + + renderFolder = (e) => { + e.preventDefault(); + const item = this.props.item; + this.props.renderFolder(item.commit_id, item.parent_dir, Utils.joinPath('/', item.obj_name)); + }; + + render() { + const item = this.props.item; + const { restored, isIconShown } = this.state; + + if (restored) { + return null; + } + + return item.is_dir ? ( + + {gettext('Directory')} + {item.obj_name} + {item.parent_dir} + {moment(item.deleted_time).format('YYYY-MM-DD')} + + + {gettext('Restore')} + + + ) : ( + + {gettext('File')} + {item.obj_name} + {item.parent_dir} + {moment(item.deleted_time).format('YYYY-MM-DD')} + {Utils.bytesToSize(item.size)} + + {gettext('Restore')} + + + ); + } +} + +Item.propTypes = { + item: PropTypes.object.isRequired, + renderFolder: PropTypes.func.isRequired, + repoID: PropTypes.string.isRequired +}; + +class FolderItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + isIconShown: false + }; + } + + handleMouseOver = () => { + this.setState({isIconShown: true}); + }; + + handleMouseOut = () => { + this.setState({isIconShown: false}); + }; + + renderFolder = (e) => { + e.preventDefault(); + const item = this.props.item; + const { commitID, baseDir, folderPath } = this.props; + this.props.renderFolder(commitID, baseDir, Utils.joinPath(folderPath, item.name)); + }; + + render() { + const item = this.props.item; + const { commitID, baseDir, folderPath } = this.props; + + return item.type == 'dir' ? ( + + {gettext('Directory')} + {item.name} + {item.parent_dir} + + + + + ) : ( + + + {gettext('File')} + + + {item.name} + + {item.parent_dir} + + {Utils.bytesToSize(item.size)} + + + ); + } +} + +FolderItem.propTypes = { + item: PropTypes.object.isRequired, + commitID: PropTypes.string.isRequired, + repoID: PropTypes.string.isRequired, + baseDir: PropTypes.string.isRequired, + folderPath: PropTypes.string.isRequired, + renderFolder: PropTypes.func.isRequired, +}; + +TrashDialog.propTypes = propTypes; + +export default TrashDialog; diff --git a/frontend/src/components/dir-view-mode/dir-column-nav.js b/frontend/src/components/dir-view-mode/dir-column-nav.js index 12aaf61dd4..295802cab6 100644 --- a/frontend/src/components/dir-view-mode/dir-column-nav.js +++ b/frontend/src/components/dir-view-mode/dir-column-nav.js @@ -281,6 +281,7 @@ class DirColumnNav extends React.Component { ); diff --git a/frontend/src/components/dir-view-mode/dir-others.js b/frontend/src/components/dir-view-mode/dir-others.js index 31d3210b51..9baa2fba6c 100644 --- a/frontend/src/components/dir-view-mode/dir-others.js +++ b/frontend/src/components/dir-view-mode/dir-others.js @@ -1,20 +1,23 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { gettext, siteRoot } from '../../utils/constants'; import TreeSection from '../tree-section'; +import TrashDialog from '../dialog/trash-dialog'; -const DirOthers = ({ userPerm, repoID }) => { - +const DirOthers = ({ userPerm, repoID, currentRepoInfo }) => { + const [showTrashDialog, setShowTrashDialog] = useState(false); let trashUrl = null; const historyUrl = siteRoot + 'repo/history/' + repoID + '/'; if (userPerm === 'rw') { trashUrl = siteRoot + 'repo/' + repoID + '/trash/'; } - + const toggleTrashDialog = () => { + setShowTrashDialog(!showTrashDialog); + }; return ( {trashUrl && -
location.href = trashUrl}> +
{gettext('Trash')}
@@ -31,6 +34,14 @@ const DirOthers = ({ userPerm, repoID }) => {
+ {showTrashDialog && ( + + )} ); }; @@ -38,6 +49,7 @@ const DirOthers = ({ userPerm, repoID }) => { DirOthers.propTypes = { userPerm: PropTypes.string, repoID: PropTypes.string, + currentRepoInfo: PropTypes.object.isRequired, }; export default DirOthers; diff --git a/frontend/src/css/trash-dialog.css b/frontend/src/css/trash-dialog.css new file mode 100644 index 0000000000..6b308bcbd8 --- /dev/null +++ b/frontend/src/css/trash-dialog.css @@ -0,0 +1,44 @@ +.trash-dialog { + max-width: 1100px; +} + +.trash-dialog .modal-header { + align-items: center; +} + +.trash-dialog .modal-header .trash-dialog-old-page { + margin-left: auto; +} + +.trash-dialog .modal-header .trash-dialog-close-icon { + color: #000; + opacity: 0.5; + font-weight: 700; + cursor: pointer; +} + +.trash-dialog .modal-header .trash-dialog-close-icon:hover { + opacity: 0.75; +} + +.trash-dialog .modal-header .clean { + height: 30px; + line-height: 28px; + padding: 0 0.5rem; +} + +.trash-dialog .modal-body { + height: 500px; + overflow-y: auto; +} + +.trash-dialog .modal-body .more { + background: #efefef; + border: 0; + color: #777; +} + +.trash-dialog .modal-body .more:hover { + color: #000; + background: #dfdfdf; +} diff --git a/frontend/src/repo-folder-trash.js b/frontend/src/repo-folder-trash.js index 4cf9c22786..d1ef7d6887 100644 --- a/frontend/src/repo-folder-trash.js +++ b/frontend/src/repo-folder-trash.js @@ -21,7 +21,7 @@ const { repoID, repoFolderName, path, - enableClean, + enableUserCleanTrash, isRepoAdmin } = window.app.pageOptions; @@ -35,7 +35,7 @@ class RepoFolderTrash extends React.Component { items: [], scanStat: null, more: false, - isCleanTrashDialogOpen: false + isCleanTrashDialogOpen: false, }; } @@ -204,7 +204,7 @@ class RepoFolderTrash extends React.Component {

{gettext('Current path: ')}{showFolder ? this.renderFolderPath() : {repoFolderName}}

- {(path == '/' && enableClean && !showFolder && isRepoAdmin) && + {(path == '/' && enableUserCleanTrash && !showFolder && isRepoAdmin) && }
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 7502a9823d..0e4bb2bf02 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -70,6 +70,7 @@ export const maxFileName = window.app.pageOptions.maxFileName; export const canPublishRepo = window.app.pageOptions.canPublishRepo; export const enableEncryptedLibrary = window.app.pageOptions.enableEncryptedLibrary; export const enableRepoHistorySetting = window.app.pageOptions.enableRepoHistorySetting; +export const enableUserCleanTrash = window.app.pageOptions.enableUserCleanTrash; export const isSystemStaff = window.app.pageOptions.isSystemStaff; export const thumbnailSizeForOriginal = window.app.pageOptions.thumbnailSizeForOriginal; export const repoPasswordMinLength = window.app.pageOptions.repoPasswordMinLength; diff --git a/frontend/src/utils/repo-trash-api.js b/frontend/src/utils/repo-trash-api.js new file mode 100644 index 0000000000..bec8468cff --- /dev/null +++ b/frontend/src/utils/repo-trash-api.js @@ -0,0 +1,51 @@ +import axios from 'axios'; +import cookie from 'react-cookies'; +import { siteRoot } from './constants'; + +class RepotrashAPI { + + init({ server, username, password, token }) { + this.server = server; + this.username = username; + this.password = password; + this.token = token; //none + if (this.token && this.server) { + this.req = axios.create({ + baseURL: this.server, + headers: { 'Authorization': 'Token ' + this.token }, + }); + } + return this; + } + + initForSeahubUsage({ siteRoot, xcsrfHeaders }) { + if (siteRoot && siteRoot.charAt(siteRoot.length - 1) === '/') { + var server = siteRoot.substring(0, siteRoot.length - 1); + this.server = server; + } else { + this.server = siteRoot; + } + + this.req = axios.create({ + headers: { + 'X-CSRFToken': xcsrfHeaders, + } + }); + return this; + } + + getRepoFolderTrash2(repoID, page, per_page) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/trash2/'; + let params = { + page: page || 1, + per_page: per_page + }; + return this.req.get(url, {params: params}); + } +} + +let repotrashAPI = new RepotrashAPI(); +let xcsrfHeaders = cookie.load('sfcsrftoken'); +repotrashAPI.initForSeahubUsage({ siteRoot, xcsrfHeaders }); + +export { repotrashAPI }; diff --git a/seahub/api2/endpoints/repo_trash.py b/seahub/api2/endpoints/repo_trash.py index f7777d1983..a21f93dc53 100644 --- a/seahub/api2/endpoints/repo_trash.py +++ b/seahub/api2/endpoints/repo_trash.py @@ -1,6 +1,8 @@ # Copyright (c) 2012-2016 Seafile Ltd. import stat import logging +import os +from datetime import datetime from rest_framework.authentication import SessionAuthentication from rest_framework.permissions import IsAuthenticated @@ -13,6 +15,7 @@ from seahub.api2.authentication import TokenAuthentication from seahub.api2.utils import api_error from seahub.signals import clean_up_repo_trash +from seahub.utils import get_trash_records from seahub.utils.timeutils import timestamp_to_isoformat_timestr from seahub.utils.repo import get_repo_owner, is_repo_admin from seahub.views import check_folder_permission @@ -24,7 +27,7 @@ from pysearpc import SearpcError from constance import config logger = logging.getLogger(__name__) - +SHOW_REPO_TRASH_DAYS = 90 class RepoTrash(APIView): @@ -303,3 +306,88 @@ class RepoTrashRevertDirents(APIView): }) return Response(result) + + +class RepoTrash2(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def get_item_info(self, trash_item): + + item_info = { + 'parent_dir': '/' if trash_item.path == '/' else trash_item.path, + 'obj_name': trash_item.obj_name, + 'deleted_time': timestamp_to_isoformat_timestr(int(trash_item.delete_time.timestamp())), + 'commit_id': trash_item.commit_id, + } + + if trash_item.obj_type == 'dir': + is_dir = True + else: + is_dir = False + + item_info['is_dir'] = is_dir + item_info['size'] = trash_item.size if not is_dir else '' + item_info['obj_id'] = trash_item.obj_id if not is_dir else '' + + return item_info + + def get(self, request, repo_id): + """ Return deleted files/dirs of a repo/folder + + Permission checking: + 1. all authenticated user can perform this action. + """ + + path = '/' + # 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) + + try: + current_page = int(request.GET.get('page', '1')) + per_page = int(request.GET.get('per_page', '100')) + except ValueError: + current_page = 1 + per_page = 100 + start = (current_page - 1) * per_page + limit = per_page + try: + dir_id = seafile_api.get_dir_id_by_path(repo_id, path) + except SearpcError as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + if not dir_id: + error_msg = 'Folder %s not found.' % path + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + if check_folder_permission(request, repo_id, path) is None: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + deleted_entries, total_count = get_trash_records(repo_id, SHOW_REPO_TRASH_DAYS, start, limit) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + items = [] + if len(deleted_entries) >= 1: + for item in deleted_entries: + item_info = self.get_item_info(item) + items.append(item_info) + + result = { + 'items': items, + 'total_count': total_count + } + + return Response(result) diff --git a/seahub/base/management/commands/clean_repo_trash.py b/seahub/base/management/commands/clean_repo_trash.py new file mode 100644 index 0000000000..9358e4c5c0 --- /dev/null +++ b/seahub/base/management/commands/clean_repo_trash.py @@ -0,0 +1,41 @@ +import logging +from datetime import datetime +from seahub.utils import SeafEventsSession +from seafevents import seafevents_api +from django.core.management.base import BaseCommand + + +logger = logging.getLogger(__name__) + +class Command(BaseCommand): + help = 'Clear repo trash within the specified time' + label = 'clean_repo_trash' + + def print_msg(self, msg): + self.stdout.write('[%s] %s\n' % (datetime.now(), msg)) + + def add_arguments(self, parser): + parser.add_argument('--keep-days', help='keep days', type=int, default=90) + + def handle(self, *args, **options): + days = options.get('keep_days') + if days < 0: + self.print_msg('keep-days cannot be set to nagative number') + return + logger.info('Start clean repo trash...') + self.print_msg('Start clean repo trash...') + self.do_action(days) + self.print_msg('Finish clean repo trash.\n') + logger.info('Finish clean repo trash.\n') + + def do_action(self, days): + try: + session = SeafEventsSession() + seafevents_api.clean_up_all_repo_trash(session, days) + except Exception as e: + logger.debug('Clean up repo trash error: %s' % e) + self.print_msg('Clean up repo trash error: %s' % e) + return + + logger.info('Successfully cleared repo trash older than %s days' % days) + self.print_msg('Successfully cleared repo trash older than %s days' % days) diff --git a/seahub/handlers.py b/seahub/handlers.py index 20a5d2c7ba..b20ae04cb1 100644 --- a/seahub/handlers.py +++ b/seahub/handlers.py @@ -129,7 +129,12 @@ try: from .utils import SeafEventsSession session = SeafEventsSession() - seafevents_api.save_user_activity(session, record) + try: + seafevents_api.save_user_activity(session, record) + seafevents_api.clean_up_repo_trash(session, repo_id, days) + except Exception as e: + logger.error(e) + session.close() def repo_restored_cb(sender, **kwargs): diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index 287a7cefbe..ccd0365f64 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -109,6 +109,7 @@ canPublishRepo: {% if user.permissions.can_publish_repo %} true {% else %} false {% endif %}, enableEncryptedLibrary: {% if enable_encrypted_library %} true {% else %} false {% endif %}, enableRepoHistorySetting: {% if enable_repo_history_setting %} true {% else %} false {% endif %}, + enableUserCleanTrash: {% if enable_user_clean_trash %} true {% else %} false {% endif %}, isSystemStaff: {% if request.user.is_staff %} true {% else %} false {% endif %}, thumbnailSizeForOriginal: {{ thumbnail_size_for_original }}, repoPasswordMinLength: {{repo_password_min_length}}, diff --git a/seahub/templates/repo_folder_trash_react.html b/seahub/templates/repo_folder_trash_react.html index 5c2e9e2106..f13cbedfb2 100644 --- a/seahub/templates/repo_folder_trash_react.html +++ b/seahub/templates/repo_folder_trash_react.html @@ -15,7 +15,7 @@ window.app.pageOptions = { repoID: '{{repo.id}}', repoFolderName: '{{repo_folder_name|escapejs}}', path: '{{path|escapejs}}', - enableClean: {% if enable_clean %} true {% else %} false {% endif %}, + enableUserCleanTrash: {% if enable_user_clean_trash %} true {% else %} false {% endif %}, isRepoAdmin: {% if is_repo_admin %} true {% else %} false {% endif %} }; diff --git a/seahub/urls.py b/seahub/urls.py index 21b147cf02..8da90aa96d 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -64,7 +64,7 @@ from seahub.api2.endpoints.file_history import FileHistoryView, NewFileHistoryVi from seahub.api2.endpoints.dir import DirView, DirDetailView from seahub.api2.endpoints.file_tag import FileTagView from seahub.api2.endpoints.file_tag import FileTagsView -from seahub.api2.endpoints.repo_trash import RepoTrash, RepoTrashRevertDirents +from seahub.api2.endpoints.repo_trash import RepoTrash, RepoTrashRevertDirents, RepoTrash2 from seahub.api2.endpoints.repo_commit import RepoCommitView from seahub.api2.endpoints.repo_commit_dir import RepoCommitDirView from seahub.api2.endpoints.repo_commit_revert import RepoCommitRevertView @@ -430,6 +430,7 @@ urlpatterns = [ re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/commits/(?P[0-9a-f]{40})/revert/$', RepoCommitRevertView.as_view(), name='api-v2.1-repo-commit-revert'), re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/dir/detail/$', DirDetailView.as_view(), name='api-v2.1-dir-detail-view'), re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/trash/$', RepoTrash.as_view(), name='api-v2.1-repo-trash'), + re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/trash2/$', RepoTrash2.as_view(), name='api-v2.1-repo-trash2'), re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/trash/revert-dirents/$', RepoTrashRevertDirents.as_view(), name='api-v2.1-repo-trash-revert-dirents'), re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/history/$', RepoHistory.as_view(), name='api-v2.1-repo-history'), re_path(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/set-password/$', RepoSetPassword.as_view(), name="api-v2.1-repo-set-password"), diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py index a958bbb2c5..78926136a9 100644 --- a/seahub/utils/__init__.py +++ b/seahub/utils/__init__.py @@ -397,7 +397,7 @@ def get_user_repos(username, org_id=None): r.id = r.repo_id r.name = r.repo_name r.last_modify = r.last_modified - + return (owned_repos, shared_repos, groups_repos, public_repos) def get_conf_text_ext(): @@ -812,6 +812,11 @@ if EVENTS_CONFIG_FILE: def get_file_history_suffix(): return seafevents_api.get_file_history_suffix(parsed_events_conf) + + def get_trash_records(repo_id, show_time, start, limit): + with _get_seafevents_session() as session: + res, total_count = seafevents_api.get_delete_records(session, repo_id, show_time, start, limit) + return res, total_count else: parsed_events_conf = None @@ -874,6 +879,8 @@ else: pass def get_user_activities_by_timestamp(): pass + def get_trash_records(): + pass def calc_file_path_hash(path, bits=12): @@ -881,7 +888,6 @@ def calc_file_path_hash(path, bits=12): path = path.encode('UTF-8') path_hash = hashlib.md5(urllib.parse.quote(path)).hexdigest()[:bits] - return path_hash def get_service_url(): diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py index 7d750e4f97..a59b4fd880 100644 --- a/seahub/views/__init__.py +++ b/seahub/views/__init__.py @@ -317,7 +317,7 @@ def repo_folder_trash(request, repo_id): 'repo': repo, 'repo_folder_name': name, 'path': path, - 'enable_clean': config.ENABLE_USER_CLEAN_TRASH, + 'enable_user_clean_trash': config.ENABLE_USER_CLEAN_TRASH, 'is_repo_admin': repo_admin }) @@ -1095,6 +1095,7 @@ def react_fake_view(request, **kwargs): 'upload_link_expire_days_max': UPLOAD_LINK_EXPIRE_DAYS_MAX, 'enable_encrypted_library': config.ENABLE_ENCRYPTED_LIBRARY, 'enable_repo_history_setting': config.ENABLE_REPO_HISTORY_SETTING, + 'enable_user_clean_trash': config.ENABLE_USER_CLEAN_TRASH, 'enable_reset_encrypted_repo_password': ENABLE_RESET_ENCRYPTED_REPO_PASSWORD, 'is_email_configured': IS_EMAIL_CONFIGURED, 'can_add_public_repo': request.user.permissions.can_add_public_repo(), diff --git a/sql/mysql.sql b/sql/mysql.sql index 29053c11d9..9e302db383 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -1479,3 +1479,18 @@ CREATE TABLE `base_clientssotoken` ( KEY `base_clientssotoken_updated_at_591fc2cd` (`updated_at`), KEY `base_clientssotoken_accessed_at_cdc66bf3` (`accessed_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +CREATE TABLE IF NOT EXISTS `FileTrash` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user` varchar(255) NOT NULL, + `obj_type` varchar(10) NOT NULL, + `obj_id` varchar(40) NOT NULL, + `obj_name` varchar(255) NOT NULL, + `delete_time` datetime NOT NULL, + `repo_id` varchar(36) NOT NULL, + `commit_id` varchar(40) DEFAULT NULL, + `path` text NOT NULL, + `size` bigint(20) NOT NULL, + PRIMARY KEY (`id`), + KEY `ix_FileTrash_repo_id` (`repo_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8_general_ci;