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;