diff --git a/seahub/api2/endpoints/repos_batch.py b/seahub/api2/endpoints/repos_batch.py index 55e6ab944b..5db1e5cea9 100644 --- a/seahub/api2/endpoints/repos_batch.py +++ b/seahub/api2/endpoints/repos_batch.py @@ -1,4 +1,5 @@ # Copyright (c) 2012-2016 Seafile Ltd. +import os import logging from pysearpc import SearpcError @@ -13,13 +14,18 @@ from seaserv import seafile_api, ccnet_api from seahub.api2.authentication import TokenAuthentication from seahub.api2.throttling import UserRateThrottle from seahub.api2.utils import api_error + from seahub.base.accounts import User from seahub.share.signals import share_repo_to_user_successful, \ - share_repo_to_group_successful -from seahub.utils import (is_org_context, send_perm_audit_msg) + share_repo_to_group_successful +from seahub.utils import is_org_context, send_perm_audit_msg, \ + normalize_dir_path +from seahub.views import check_folder_permission +from seahub.settings import MAX_PATH logger = logging.getLogger(__name__) + class ReposBatchView(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) permission_classes = (IsAuthenticated,) @@ -268,3 +274,169 @@ class ReposBatchView(APIView): }) return Response(result) + + +class ReposBatchCopyDirView(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def post(self, request): + """ Multi copy folders. + + Permission checking: + 1. User must has `r/rw` permission for src folder. + 2. User must has `rw` permission for dst folder. + + Parameter: + { + "src_repo_id":"7460f7ac-a0ff-4585-8906-bb5a57d2e118", + "dst_repo_id":"a3fa768d-0f00-4343-8b8d-07b4077881db", + "path":[ + {"src_path":"/1/2/3/","dst_path":"/4/5/6/"}, + {"src_path":"/a/b/c/","dst_path":"/d/e/f/"}, + ] + } + """ + + # argument check + path_list = request.data.get('path', None) + if not path_list: + error_msg = 'path invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + src_repo_id = request.data.get('src_repo_id', None) + if not src_repo_id: + error_msg = 'src_repo_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + dst_repo_id = request.data.get('dst_repo_id', None) + if not dst_repo_id: + error_msg = 'dst_repo_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # permission check, user must has `r/rw` permission for src folder. + if check_folder_permission(request, src_repo_id, '/') is None: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # permission check, user must has `rw` permission for dst folder. + if check_folder_permission(request, dst_repo_id, '/') != 'rw': + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # resource check + src_repo = seafile_api.get_repo(src_repo_id) + if not src_repo: + error_msg = 'Library %s not found.' % src_repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + dst_repo = seafile_api.get_repo(dst_repo_id) + if not dst_repo: + error_msg = 'Library %s not found.' % dst_repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + result = {} + result['failed'] = [] + result['success'] = [] + username = request.user.username + + for path_item in path_list: + + src_path = path_item['src_path'] + src_path = normalize_dir_path(src_path) + src_parent_dir = os.path.dirname(src_path.rstrip('/')) + src_parent_dir = normalize_dir_path(src_parent_dir) + src_obj_name = os.path.basename(src_path.rstrip('/')) + + dst_path = path_item['dst_path'] + dst_path = normalize_dir_path(dst_path) + dst_parent_dir = dst_path + dst_obj_name = src_obj_name + + common_dict = { + 'src_repo_id': src_repo_id, + 'src_path': src_path, + 'dst_repo_id': dst_repo_id, + 'dst_path': dst_path, + } + + # src/dst parameter check + if src_repo_id == dst_repo_id and \ + dst_path.startswith(src_path): + error_dict = { + 'error_msg': "The destination directory is the same as the source, or is it's subfolder." + } + common_dict.update(error_dict) + result['failed'].append(common_dict) + continue + + if src_path == '/': + error_dict = { + 'error_msg': "The source path can not be '/'." + } + common_dict.update(error_dict) + result['failed'].append(common_dict) + continue + + if len(dst_parent_dir + dst_obj_name) > MAX_PATH: + error_dict = { + 'error_msg': "'Destination path is too long." + } + common_dict.update(error_dict) + result['failed'].append(common_dict) + continue + + # src resource check + if not seafile_api.get_dir_id_by_path(src_repo_id, src_path): + error_dict = { + 'error_msg': 'Folder %s not found.' % src_path + } + common_dict.update(error_dict) + result['failed'].append(common_dict) + continue + + # dst resource check + if not seafile_api.get_dir_id_by_path(dst_repo_id, dst_path): + error_dict = { + 'error_msg': 'Folder %s not found.' % dst_path + } + common_dict.update(error_dict) + result['failed'].append(common_dict) + continue + + # src path permission check, user must has `r/rw` permission for src folder. + if check_folder_permission(request, src_repo_id, src_parent_dir) is None: + error_dict = { + 'error_msg': 'Permission denied.' + } + common_dict.update(error_dict) + result['failed'].append(common_dict) + continue + + # dst path permission check, user must has `rw` permission for dst folder. + if check_folder_permission(request, dst_repo_id, dst_path) != 'rw': + error_dict = { + 'error_msg': 'Permission denied.' + } + common_dict.update(error_dict) + result['failed'].append(common_dict) + continue + + try: + # need_progress=0, synchronous=1 + seafile_api.copy_file(src_repo_id, src_parent_dir, src_obj_name, + dst_repo_id, dst_parent_dir, dst_obj_name, username, 0, 1) + except Exception as e: + logger.error(e) + error_dict = { + 'error_msg': 'Internal Server Error' + } + common_dict.update(error_dict) + result['failed'].append(common_dict) + continue + + result['success'].append(common_dict) + + return Response(result) diff --git a/seahub/urls.py b/seahub/urls.py index 1de55d580d..f286546326 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -5,6 +5,10 @@ from django.conf.urls import patterns, url, include from django.views.generic import TemplateView from seahub.views import * +from seahub.views.sysadmin import * +from seahub.views.ajax import * +from seahub.views.sso import * + from seahub.views.file import view_repo_file, view_history_file, view_trash_file,\ view_snapshot_file, file_edit, view_shared_file, view_file_via_shared_dir,\ text_diff, view_raw_file, view_raw_shared_file, \ @@ -15,9 +19,6 @@ from notifications.views import notification_list from seahub.views.wiki import personal_wiki, personal_wiki_pages, \ personal_wiki_create, personal_wiki_page_new, personal_wiki_page_edit, \ personal_wiki_page_delete, personal_wiki_use_lib -from seahub.views.sysadmin import * -from seahub.views.ajax import * -from seahub.views.sso import * from seahub.api2.endpoints.groups import Groups, Group from seahub.api2.endpoints.group_members import GroupMembers, GroupMembersBulk, GroupMember from seahub.api2.endpoints.search_group import SearchGroup @@ -25,7 +26,8 @@ from seahub.api2.endpoints.share_links import ShareLinks, ShareLink 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 -from seahub.api2.endpoints.repos_batch import ReposBatchView +from seahub.api2.endpoints.repos_batch import ReposBatchView, \ + ReposBatchCopyDirView from seahub.api2.endpoints.repos import RepoView from seahub.api2.endpoints.file import FileView from seahub.api2.endpoints.dir import DirView, DirDetailView @@ -204,16 +206,23 @@ urlpatterns = patterns( url(r'^api/v2.1/shared-repos/$', SharedRepos.as_view(), name='api-v2.1-shared-repos'), url(r'^api/v2.1/shared-repos/(?P[-0-9a-f]{36})/$', SharedRepo.as_view(), name='api-v2.1-shared-repo'), - ## user::share-links + ## 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/(?P[a-f0-9]+)/$', ShareLink.as_view(), name='api-v2.1-share-link'), + + ## 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/(?P[a-f0-9]+)/$', UploadLink.as_view(), name='api-v2.1-upload-link'), - ## user::repos + ## user::repos-batch-operate url(r'^api/v2.1/repos/batch/$', ReposBatchView.as_view(), name='api-v2.1-repos-batch'), - url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/$', RepoView.as_view(), name='api-v2.1-repo-view'), + url(r'^api/v2.1/repos/batch-copy-dir/$', ReposBatchCopyDirView.as_view(), name='api-v2.1-repos-batch-copy-dir'), + + ## user::deleted repos url(r'^api/v2.1/deleted-repos/$', DeletedRepos.as_view(), name='api2-v2.1-deleted-repos'), + + ## user::repos + url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/$', RepoView.as_view(), name='api-v2.1-repo-view'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/file/$', FileView.as_view(), name='api-v2.1-file-view'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/dir/$', DirView.as_view(), name='api-v2.1-dir-view'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/dir/detail/$', DirDetailView.as_view(), name='api-v2.1-dir-detail-view'), diff --git a/tests/api/endpoints/test_repos_batch.py b/tests/api/endpoints/test_repos_batch.py index f4d76ff0c6..cb4f32cb4a 100644 --- a/tests/api/endpoints/test_repos_batch.py +++ b/tests/api/endpoints/test_repos_batch.py @@ -1,8 +1,11 @@ +import os import json +import posixpath from seaserv import seafile_api, ccnet_api from django.core.urlresolvers import reverse from tests.common.utils import randstring from seahub.test_utils import BaseTestCase +from seahub.utils import normalize_dir_path class ReposBatchViewTest(BaseTestCase): @@ -193,3 +196,130 @@ class ReposBatchViewTest(BaseTestCase): } resp = self.client.post(self.url, data) self.assertEqual(403, resp.status_code) + + +class ReposBatchCopyDirView(BaseTestCase): + + def setUp(self): + self.user_name = self.user.username + self.admin_name = self.admin.username + self.repo_id = self.repo.id + self.url = reverse('api-v2.1-repos-batch-copy-dir') + + def tearDown(self): + self.remove_repo() + self.remove_group() + + def get_random_path(self): + return '/%s/%s/%s/' % (randstring(2), \ + randstring(2), randstring(2)) + + def create_new_repo(self, username): + new_repo_id = seafile_api.create_repo(name=randstring(10), + desc='', username=username, passwd=None) + + return new_repo_id + + def test_copy_dir(self): + + self.login_as(self.user) + + # create two folders in src repo + src_folder_1 = self.get_random_path() + src_folder_2 = self.get_random_path() + for path in [src_folder_1, src_folder_2]: + seafile_api.mkdir_with_parents(self.repo_id, + '/', path.strip('/'), self.user_name) + + # share admin's tmp repo to user + tmp_repo_id = self.create_new_repo(self.admin_name) + seafile_api.share_repo(tmp_repo_id, self.admin_name, + self.user_name, 'rw') + + # create two folders as parent dirs in dst repo for admin user + dst_folder_1 = self.get_random_path() + seafile_api.mkdir_with_parents(tmp_repo_id, + '/', dst_folder_1.strip('/'), self.admin_name) + + dst_folder_2 = '/' + + # copy folders + data = { + "src_repo_id": self.repo_id, + "dst_repo_id": tmp_repo_id, + "path": [ + {"src_path": src_folder_1, "dst_path": dst_folder_1}, + {"src_path": src_folder_2, "dst_path": dst_folder_2}, + ] + } + + resp = self.client.post(self.url, json.dumps(data), + 'application/json') + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert len(json_resp['success']) == 2 + assert len(json_resp['failed']) == 0 + + def folder_exist(src_folder, dst_repo_id, dst_folder): + src_obj_name = os.path.basename(src_folder.rstrip('/')) + full_dst_folder_path = posixpath.join(dst_folder.strip('/'), + src_obj_name.strip('/')) + full_dst_folder_path = normalize_dir_path(full_dst_folder_path) + return seafile_api.get_dir_id_by_path(dst_repo_id, + full_dst_folder_path) is not None + + assert folder_exist(src_folder_1, tmp_repo_id, dst_folder_1) + assert folder_exist(src_folder_2, tmp_repo_id, dst_folder_2) + + self.remove_repo(tmp_repo_id) + + def test_copy_dir_with_invalid_repo_permisson(self): + + self.login_as(self.user) + + # create two folders as parent dirs in dst repo for admin user + tmp_repo_id = self.create_new_repo(self.admin_name) + + # copy folders + data = { + "src_repo_id": self.repo_id, + "dst_repo_id": tmp_repo_id, + "path": [ + {"src_path": '/', "dst_path": '/'}, + {"src_path": '/', "dst_path": '/'}, + ] + } + + resp = self.client.post(self.url, json.dumps(data), + 'application/json') + self.assertEqual(403, resp.status_code) + + def test_copy_dir_with_src_path_is_root_folder(self): + + self.login_as(self.user) + + # create two folders as parent dirs in dst repo for admin user + tmp_repo_id = self.create_new_repo(self.admin_name) + seafile_api.share_repo(tmp_repo_id, self.admin_name, + self.user_name, 'rw') + + # copy folders + data = { + "src_repo_id": self.repo_id, + "dst_repo_id": tmp_repo_id, + "path": [ + {"src_path": '/', "dst_path": '/'}, + ] + } + + resp = self.client.post(self.url, json.dumps(data), + 'application/json') + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert len(json_resp['success']) == 0 + assert len(json_resp['failed']) == 1 + + assert json_resp['failed'][0]['error_msg'] == \ + "The source path can not be '/'."