diff --git a/seahub/api2/endpoints/query_zip_progress.py b/seahub/api2/endpoints/query_zip_progress.py new file mode 100644 index 0000000000..af63340581 --- /dev/null +++ b/seahub/api2/endpoints/query_zip_progress.py @@ -0,0 +1,37 @@ +import logging +import json + +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status + +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error + +from seaserv import seafile_api + +logger = logging.getLogger(__name__) + +class QueryZipProgressView(APIView): + + throttle_classes = (UserRateThrottle, ) + + def get(self, request, format=None): + """ check progress when download dir/multi. + + Permission checking: + """ + + token = request.GET.get('token', None) + if not token: + error_msg = 'token invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + progress = seafile_api.query_zip_progress(token) + 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(json.loads(progress)) diff --git a/seahub/api2/endpoints/share_link_zip_task.py b/seahub/api2/endpoints/share_link_zip_task.py new file mode 100644 index 0000000000..be11d9666e --- /dev/null +++ b/seahub/api2/endpoints/share_link_zip_task.py @@ -0,0 +1,120 @@ +import logging +import os +import json +import posixpath + +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status + +from django.conf import settings + +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error + +from seahub.views.file import send_file_access_msg +from seahub.share.models import FileShare +from seahub.utils import is_windows_operating_system, \ + is_pro_version + +import seaserv +from seaserv import seafile_api + +logger = logging.getLogger(__name__) + +class ShareLinkZipTaskView(APIView): + + throttle_classes = (UserRateThrottle,) + + def get(self, request, format=None): + """ Only used for download dir when view dir share link from web. + + + Permission checking: + 1. authenticated user OR anonymous user has passed email code check(if necessary); + """ + + # permission check + if is_pro_version() and settings.ENABLE_SHARE_LINK_AUDIT: + if not request.user.is_authenticated() and \ + not request.session.get('anonymous_email'): + # if anonymous user has passed email code check, + # then his/her email info will be in session. + + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # argument check + share_link_token = request.GET.get('share_link_token', None) + if not share_link_token: + error_msg = 'share_link_token invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + req_path = request.GET.get('path', None) + if not req_path: + error_msg = 'path invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # recourse check + fileshare = FileShare.objects.get_valid_dir_link_by_token(share_link_token) + if not fileshare: + error_msg = 'share_link_token %s not found.' % share_link_token + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if req_path[-1] != '/': + req_path += '/' + + if req_path == '/': + real_path = fileshare.path + else: + real_path = posixpath.join(fileshare.path, req_path.lstrip('/')) + + if real_path[-1] != '/': + real_path += '/' + + repo_id = fileshare.repo_id + 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) + + dir_id = seafile_api.get_dir_id_by_path(repo_id, real_path) + if not dir_id: + error_msg = 'Folder %s not found.' % real_path + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # get file server access token + dir_name = repo.name if real_path == '/' else \ + os.path.basename(real_path.rstrip('/')) + + dir_size = seafile_api.get_dir_size( + repo.store_id, repo.version, dir_id) + if dir_size > seaserv.MAX_DOWNLOAD_DIR_SIZE: + error_msg = 'Unable to download directory "%s": size is too large.' % dir_name + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + is_windows = 0 + if is_windows_operating_system(request): + is_windows = 1 + + fake_obj_id = { + 'obj_id': dir_id, + 'dir_name': dir_name, + 'is_windows': is_windows + } + + username = request.user.username + try: + zip_token = seafile_api.get_fileserver_access_token( + repo_id, json.dumps(fake_obj_id), 'download-dir', username) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + if request.session.get('anonymous_email'): + request.user.username = request.session.get('anonymous_email') + + send_file_access_msg(request, repo, real_path, 'share-link') + + return Response({'zip_token': zip_token}) diff --git a/seahub/api2/endpoints/zip_task.py b/seahub/api2/endpoints/zip_task.py new file mode 100644 index 0000000000..17115f259d --- /dev/null +++ b/seahub/api2/endpoints/zip_task.py @@ -0,0 +1,144 @@ +import logging +import json +import stat +import posixpath + +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status +from django.utils.translation import ugettext as _ + +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.utils import api_error + +from seahub.views import check_folder_permission +from seahub.views.file import send_file_access_msg +from seahub.utils import is_windows_operating_system + +import seaserv +from seaserv import seafile_api + +logger = logging.getLogger(__name__) + +class ZipTaskView(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def get(self, request, repo_id, format=None): + """ Get file server token for download-dir and download-multi. + + Permission checking: + 1. user with 'r' or 'rw' permission; + """ + + # argument check + parent_dir = request.GET.get('parent_dir', None) + if not parent_dir: + error_msg = 'parent_dir invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + dirent_name_list = request.GET.getlist('dirents', None) + if not dirent_name_list: + error_msg = 'dirents invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if len(dirent_name_list) == 1: + download_type = 'download-dir' + elif len(dirent_name_list) > 1: + download_type = 'download-multi' + else: + error_msg = 'dirents invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # recourse 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 seafile_api.get_dir_id_by_path(repo_id, parent_dir): + error_msg = 'Folder %s not found.' % parent_dir + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + if not check_folder_permission(request, repo_id, parent_dir): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # get file server access token + is_windows = 0 + if is_windows_operating_system(request): + is_windows = 1 + + if download_type == 'download-dir': + dir_name = dirent_name_list[0].strip('/') + full_dir_path = posixpath.join(parent_dir, dir_name) + + dir_id = seafile_api.get_dir_id_by_path(repo_id, full_dir_path) + if not dir_id: + error_msg = 'Folder %s not found.' % full_dir_path + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + dir_size = seafile_api.get_dir_size( + repo.store_id, repo.version, dir_id) + + if dir_size > seaserv.MAX_DOWNLOAD_DIR_SIZE: + error_msg = 'Unable to download directory "%s": size is too large.' % dir_name + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + fake_obj_id = { + 'obj_id': dir_id, + 'dir_name': dir_name, + 'is_windows': is_windows + } + + if download_type == 'download-multi': + dirent_list = [] + total_size = 0 + for dirent_name in dirent_name_list: + dirent_name = dirent_name.strip('/') + dirent_list.append(dirent_name) + + full_dirent_path = posixpath.join(parent_dir, dirent_name) + current_dirent = seafile_api.get_dirent_by_path(repo_id, full_dirent_path) + if not current_dirent: + continue + + if stat.S_ISDIR(current_dirent.mode): + total_size += seafile_api.get_dir_size(repo.store_id, + repo.version, current_dirent.obj_id) + else: + total_size += current_dirent.size + + if total_size > seaserv.MAX_DOWNLOAD_DIR_SIZE: + error_msg = _('Total size exceeds limit.') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + fake_obj_id = { + 'parent_dir': parent_dir, + 'file_list': dirent_list, + 'is_windows': is_windows + } + + username = request.user.username + try: + zip_token = seafile_api.get_fileserver_access_token( + repo_id, json.dumps(fake_obj_id), download_type, username) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + if len(dirent_name_list) > 10: + send_file_access_msg(request, repo, parent_dir, 'web') + else: + for dirent_name in dirent_name_list: + full_dirent_path = posixpath.join(parent_dir, dirent_name) + send_file_access_msg(request, repo, full_dirent_path, 'web') + + return Response({'zip_token': zip_token}) diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py index 98aeee6994..f4650bc866 100644 --- a/seahub/base/context_processors.py +++ b/seahub/base/context_processors.py @@ -61,6 +61,9 @@ def base(request): repo_id_patt = r".*/([a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})/.*" m = re.match(repo_id_patt, request.get_full_path()) search_repo_id = m.group(1) if m is not None else None + file_server_root = config.FILE_SERVER_ROOT + if not file_server_root.endswith('/'): + file_server_root += '/' return { 'seafile_version': SEAFILE_VERSION, @@ -90,4 +93,5 @@ def base(request): 'search_repo_id': search_repo_id, 'SITE_ROOT': SITE_ROOT, 'constance_enabled': dj_settings.CONSTANCE_ENABLED, + 'FILE_SERVER_ROOT': file_server_root, } diff --git a/seahub/templates/base_for_backbone.html b/seahub/templates/base_for_backbone.html index d73257736a..0f6de3cbc3 100644 --- a/seahub/templates/base_for_backbone.html +++ b/seahub/templates/base_for_backbone.html @@ -112,7 +112,8 @@ var app = { config: { mediaUrl: '{{ MEDIA_URL }}', - siteRoot: '{{ SITE_ROOT }}' + siteRoot: '{{ SITE_ROOT }}', + fileServerRoot: '{{ FILE_SERVER_ROOT }}' } }; diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html index 45a5b2dd33..eccf355a30 100644 --- a/seahub/templates/js/templates.html +++ b/seahub/templates/js/templates.html @@ -105,6 +105,7 @@ <% if (!encrypted && user_perm == 'r') { %>
+
<% } %> @@ -206,7 +207,9 @@
- + + + <% if (!repo_encrypted && can_generate_shared_link) { %> <% } %> diff --git a/seahub/templates/view_shared_dir.html b/seahub/templates/view_shared_dir.html index eebcf67ba5..f66676c4ff 100644 --- a/seahub/templates/view_shared_dir.html +++ b/seahub/templates/view_shared_dir.html @@ -42,7 +42,7 @@ {% endif %} {% if not traffic_over_limit %} - {% trans "ZIP"%} + {% trans "ZIP"%} {% endif %}
@@ -60,7 +60,7 @@ {% for dirent in dir_list %} - + {% trans {{ dirent.obj_name }} @@ -69,7 +69,7 @@ {{ dirent.last_modified|translate_seahub_time }} {% if not traffic_over_limit %} - + {% trans 'Download' %} {% endif %} @@ -119,7 +119,7 @@ {{ dirent.obj_name }} {% if not traffic_over_limit %} - + {% endif %} @@ -185,9 +185,73 @@ var magnificOptions = { $('.img-name-link').magnificPopup(magnificOptions); $('.img-img-link').magnificPopup(magnificOptions); +var cur_path = '{{path|escapejs}}'; +var share_link_token = '{{token}}'; +var file_server_root = '{{FILE_SERVER_ROOT}}'; + +$('.shared-dir-zip, .download-dir').click(function (e) { + var cur_download_dir_path, + cur_download_dir_name, + zip_token; + + var target = e.target || event.srcElement; + + if ($(target).hasClass("shared-dir-zip")) { + cur_download_dir_path = cur_path; + } else { + cur_download_dir_name = $(target).closest('a').attr('data-name'); + if (cur_path.lastIndexOf('/') != cur_path.length - 1) { + // cur_path NOT end with '/' + cur_download_dir_path = cur_path.substring(0, cur_path.lastIndexOf('/')) + '/' + cur_download_dir_name; + } else { + cur_download_dir_path = cur_path + cur_download_dir_name; + } + } + + var queryZipProgress = function() { + $.ajax({ + url: '{% url 'api-v2.1-query-zip-progress' %}' + '?token=' + zip_token, + dataType: 'json', + cache: false, + success: function (data) { + if (data['total'] == data['zipped']) { + clearInterval(interval); + location.href = file_server_root + 'zip/' + zip_token; + } + }, + error: function (xhr) { + if (xhr.responseText) { + feedback($.parseJSON(xhr.responseText).error_msg, 'error'); + } else { + feedback("{% trans "Failed. Please check the network." %}", 'error'); + } + clearInterval(interval); + } + }); + }; + + $.ajax({ + url: '{% url 'api-v2.1-share-link-zip-task' %}' + '?share_link_token=' + share_link_token + '&path=' + encodeURIComponent(cur_download_dir_path), + dataType: 'json', + cache: false, + success: function(data) { + zip_token = data['zip_token']; + queryZipProgress(); + interval = setInterval(queryZipProgress, 1000); + }, + error: function (xhr) { + if (xhr.responseText) { + feedback($.parseJSON(xhr.responseText).error_msg, 'error'); + } else { + feedback("{% trans "Failed. Please check the network." %}", 'error'); + } + } + }); +}); + {% if not repo.encrypted and ENABLE_THUMBNAIL %} // get thumbnails for image files -var cur_path = "{{path|escapejs}}"; + $(function() { var img_icons = $('.not-thumbnail'); if (img_icons.length == 0) { diff --git a/seahub/urls.py b/seahub/urls.py index 053c6a2816..448affccc9 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -27,6 +27,9 @@ from seahub.api2.endpoints.file import FileView from seahub.api2.endpoints.dir import DirView from seahub.api2.endpoints.repo_set_password import RepoSetPassword from seahub.api2.endpoints.dirents_download_link import DirentsDownloadLinkView +from seahub.api2.endpoints.zip_task import ZipTaskView +from seahub.api2.endpoints.share_link_zip_task import ShareLinkZipTaskView +from seahub.api2.endpoints.query_zip_progress import QueryZipProgressView from seahub.api2.endpoints.admin.login import Login from seahub.api2.endpoints.admin.file_audit import FileAudit from seahub.api2.endpoints.admin.file_update import FileUpdate @@ -187,6 +190,9 @@ urlpatterns = patterns( url(r'^api/v2.1/upload-links/(?P[a-f0-9]{10})/$', UploadLink.as_view(), name='api-v2.1-upload-link'), 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})/dirents/download-link/$', DirentsDownloadLinkView.as_view(), name='api-v2.1-dirents-download-link-view'), + url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/zip-task/$', ZipTaskView.as_view(), name='api-v2.1-zip-task'), + url(r'^api/v2.1/share-link-zip-task/$', ShareLinkZipTaskView.as_view(), name='api-v2.1-share-link-zip-task'), + url(r'^api/v2.1/query-zip-progress/$', QueryZipProgressView.as_view(), name='api-v2.1-query-zip-progress'), 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})/set-password/$', RepoSetPassword.as_view(), name="api-v2.1-repo-set-password"), url(r'^api/v2.1/admin/sysinfo/$', SysInfo.as_view(), name='api-v2.1-sysinfo'), diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py index c71593caf8..c39cad76a5 100644 --- a/seahub/utils/__init__.py +++ b/seahub/utils/__init__.py @@ -1298,3 +1298,12 @@ def get_system_admins(): admins.append(user) return admins + +def is_windows_operating_system(request): + if not request.META.has_key('HTTP_USER_AGENT'): + return False + + if 'windows' in request.META['HTTP_USER_AGENT'].lower(): + return True + else: + return False diff --git a/seahub/views/repo.py b/seahub/views/repo.py index f69b4d795f..5b0076b341 100644 --- a/seahub/views/repo.py +++ b/seahub/views/repo.py @@ -21,7 +21,6 @@ from seahub.share.models import FileShare, UploadLinkShare, \ check_share_link_common from seahub.views import gen_path_link, get_repo_dirents, \ check_folder_permission -from seahub.views.file import send_file_access_msg from seahub.utils import gen_file_upload_url, gen_dir_share_link, \ gen_shared_upload_link, user_traffic_over_limit, render_error, \ @@ -29,7 +28,6 @@ from seahub.utils import gen_file_upload_url, gen_dir_share_link, \ from seahub.settings import ENABLE_UPLOAD_FOLDER, \ ENABLE_RESUMABLE_FILEUPLOAD, ENABLE_THUMBNAIL, \ THUMBNAIL_ROOT, THUMBNAIL_DEFAULT_SIZE, THUMBNAIL_SIZE_FOR_GRID -from seahub.utils import gen_file_get_url from seahub.utils.file_types import IMAGE from seahub.thumbnail.utils import get_share_link_thumbnail_src @@ -158,47 +156,6 @@ def repo_history_view(request, repo_id): }, context_instance=RequestContext(request)) ########## shared dir/uploadlink -def _download_dir_from_share_link(request, fileshare, repo, real_path): - # check whether owner's traffic over the limit - if user_traffic_over_limit(fileshare.username): - return render_error( - request, _(u'Unable to access file: share link traffic is used up.')) - - shared_by = fileshare.username - if real_path == '/': - dirname = repo.name - else: - dirname = os.path.basename(real_path.rstrip('/')) - - dir_id = seafile_api.get_dir_id_by_path(repo.id, real_path) - if not dir_id: - return render_error( - request, _(u'Unable to download: folder not found.')) - - try: - total_size = seaserv.seafserv_threaded_rpc.get_dir_size( - repo.store_id, repo.version, dir_id) - except Exception as e: - logger.error(str(e)) - return render_error(request, _(u'Internal Error')) - - if total_size > seaserv.MAX_DOWNLOAD_DIR_SIZE: - return render_error(request, _(u'Unable to download directory "%s": size is too large.') % dirname) - - token = seafile_api.get_fileserver_access_token(repo.id, - dir_id, - 'download-dir', - request.user.username) - - try: - seaserv.send_message('seahub.stats', 'dir-download\t%s\t%s\t%s\t%s' % - (repo.id, shared_by, dir_id, total_size)) - send_file_access_msg(request, repo, real_path, 'web') - except Exception as e: - logger.error('Error when sending dir-download message: %s' % str(e)) - - return HttpResponseRedirect(gen_file_get_url(token, dirname)) - @share_link_audit def view_shared_dir(request, fileshare): token = fileshare.token @@ -233,11 +190,6 @@ def view_shared_dir(request, fileshare): if not seafile_api.get_dir_id_by_path(repo.id, fileshare.path): return render_error(request, _('"%s" does not exist.') % fileshare.path) - # download shared dir - if request.GET.get('dl', '') == '1': - return _download_dir_from_share_link(request, fileshare, repo, - real_path) - if fileshare.path == '/': # use repo name as dir name if share whole library dir_name = repo.name diff --git a/static/scripts/app/views/device.js b/static/scripts/app/views/device.js index 0a61632bd0..f806b04fff 100644 --- a/static/scripts/app/views/device.js +++ b/static/scripts/app/views/device.js @@ -67,7 +67,7 @@ define([ _this.remove(); var msg = gettext("Successfully unlink %(name)s.") - .replace('%(name)s', Common.HTMLescape(device_name)); + .replace('%(name)s', device_name); Common.feedback(msg, 'success'); }, error: function(xhr) { @@ -87,7 +87,7 @@ define([ _this.remove(); var msg = gettext("Successfully unlink %(name)s.") - .replace('%(name)s', Common.HTMLescape(device_name)); + .replace('%(name)s', device_name); Common.feedback(msg, 'success'); }, error: function(xhr) { diff --git a/static/scripts/app/views/dialogs/dirent-mvcp.js b/static/scripts/app/views/dialogs/dirent-mvcp.js index b550702b54..3ed56449b6 100644 --- a/static/scripts/app/views/dialogs/dirent-mvcp.js +++ b/static/scripts/app/views/dialogs/dirent-mvcp.js @@ -197,10 +197,10 @@ define([ if (_this.op_type == 'mv') { msg = gettext("Successfully moved %(name)s") - .replace('%(name)s', Common.HTMLescape(obj_name)); + .replace('%(name)s', obj_name); } else { msg = gettext("Successfully copied %(name)s") - .replace('%(name)s', Common.HTMLescape(obj_name)); + .replace('%(name)s', obj_name); } if (!data['task_id']) { // no progress diff --git a/static/scripts/app/views/dir.js b/static/scripts/app/views/dir.js index 85f0654b4d..b8bdbabbce 100644 --- a/static/scripts/app/views/dir.js +++ b/static/scripts/app/views/dir.js @@ -807,22 +807,53 @@ define([ }, download: function () { - var dirents = this.dir, - selected_dirents = dirents.where({'selected':true}), - selected_names = ''; + var dirents = this.dir; + var parent_dir = dirents.path; + var selected_dirents = dirents.where({'selected':true}); + var selected_names = []; + var interval; + var zip_token; + var queryZipProgress = function() { + $.ajax({ + url: Common.getUrl({name: 'query_zip_progress'}) + '?token=' + zip_token, + dataType: 'json', + cache: false, + success: function (data) { + if (data['total'] == data['zipped']) { + clearInterval(interval); + location.href = Common.getUrl({name: 'download_dir_zip_url', zip_token: zip_token}); + } + }, + error: function (xhr) { + Common.ajaxErrorHandler(xhr); + clearInterval(interval); + } + }); + }; + + if (selected_dirents.length == 1 && selected_dirents[0].get('is_file')) { + // only select one file + var file_path = parent_dir + '/' + selected_dirents[0].get('obj_name'); + location.href = Common.getUrl({name: 'get_file_download_url', repo_id: dirents.repo_id, file_path: encodeURIComponent(file_path)}); + return false + } $(selected_dirents).each(function() { - selected_names += this.get('obj_name') + ','; + selected_names.push(this.get('obj_name')); }); $.ajax({ - url: Common.getUrl({ - name: 'download_dirents', - repo_id: dirents.repo_id - }) + '?parent_dir=' + encodeURIComponent(dirents.path) + '&dirents=' + encodeURIComponent(selected_names), + url: Common.getUrl({name: 'zip_task', repo_id: dirents.repo_id}), + data: { + 'parent_dir': parent_dir, + 'dirents': selected_names + }, dataType: 'json', + traditional: true, success: function(data) { - location.href = data['url']; + zip_token = data['zip_token']; + queryZipProgress(); + interval = setInterval(queryZipProgress, 1000); }, error: function (xhr) { Common.ajaxErrorHandler(xhr); @@ -877,7 +908,7 @@ define([ } else { msg_s = gettext("Successfully deleted %(name)s and %(amount)s other items."); } - msg_s = msg_s.replace('%(name)s', Common.HTMLescape(data['deleted'][0])).replace('%(amount)s', del_len - 1); + msg_s = msg_s.replace('%(name)s', data['deleted'][0]).replace('%(amount)s', del_len - 1); Common.feedback(msg_s, 'success'); } if (not_del_len > 0) { @@ -888,7 +919,7 @@ define([ } else { msg_f = gettext("Failed to delete %(name)s and %(amount)s other items."); } - msg_f = msg_f.replace('%(name)s', Common.HTMLescape(data['undeleted'][0])).replace('%(amount)s', not_del_len - 1); + msg_f = msg_f.replace('%(name)s', data['undeleted'][0]).replace('%(amount)s', not_del_len - 1); Common.feedback(msg_f, 'error'); } $.modal.close(); @@ -1033,7 +1064,7 @@ define([ } } - msg_s = msg_s.replace('%(name)s', Common.HTMLescape(data['success'][0])).replace('%(amount)s', success_len - 1); + msg_s = msg_s.replace('%(name)s', data['success'][0]).replace('%(amount)s', success_len - 1); //msg_s += ' ' + "View" + ''; Common.feedback(msg_s, 'success'); } @@ -1052,7 +1083,7 @@ define([ msg_f = gettext("Internal error. Failed to copy %(name)s."); } } - msg_f = msg_f.replace('%(name)s', Common.HTMLescape(data['failed'][0])).replace('%(amount)s', data['failed'].length - 1); + msg_f = msg_f.replace('%(name)s', data['failed'][0]).replace('%(amount)s', data['failed'].length - 1); Common.feedback(msg_f, 'error'); } }, diff --git a/static/scripts/app/views/dirent-grid.js b/static/scripts/app/views/dirent-grid.js index f13e850a09..aa5920e4a8 100644 --- a/static/scripts/app/views/dirent-grid.js +++ b/static/scripts/app/views/dirent-grid.js @@ -152,7 +152,7 @@ define([ this.model.deleteFromServer({ success: function(data) { var msg = gettext("Successfully deleted %(name)s") - .replace('%(name)s', Common.HTMLescape(dirent_name)); + .replace('%(name)s', dirent_name); Common.feedback(msg, 'success'); }, error: function(xhr) { diff --git a/static/scripts/app/views/dirent.js b/static/scripts/app/views/dirent.js index 7ffbbfa4a3..4979121db9 100644 --- a/static/scripts/app/views/dirent.js +++ b/static/scripts/app/views/dirent.js @@ -69,6 +69,7 @@ define([ events: { 'click .select': 'select', 'click .file-star': 'starFile', + 'click .download-dir': 'downloadDir', 'click .share': 'share', 'click .delete': 'del', // 'delete' is a preserve word 'click .rename': 'rename', @@ -118,6 +119,48 @@ define([ } }, + downloadDir: function() { + var dir = this.dirView.dir; + var obj_name = this.model.get('obj_name'); + var interval; + var zip_token; + var queryZipProgress = function() { + $.ajax({ + url: Common.getUrl({name: 'query_zip_progress'}) + '?token=' + zip_token, + dataType: 'json', + cache: false, + success: function (data) { + if (data['total'] == data['zipped']) { + clearInterval(interval); + location.href = Common.getUrl({name: 'download_dir_zip_url', zip_token: zip_token}); + } + }, + error: function (xhr) { + Common.ajaxErrorHandler(xhr); + clearInterval(interval); + } + }); + }; + + $.ajax({ + url: Common.getUrl({ + name: 'zip_task', + repo_id: dir.repo_id + }) + '?parent_dir=' + encodeURIComponent(dir.path) + '&dirents=' + encodeURIComponent(obj_name), + dataType: 'json', + success: function(data) { + zip_token = data['zip_token']; + queryZipProgress(); + interval = setInterval(queryZipProgress, 1000); + }, + error: function (xhr) { + Common.ajaxErrorHandler(xhr); + } + }); + + return false; + }, + starFile: function() { var _this = this; var dir = this.dirView.dir; @@ -185,7 +228,7 @@ define([ this.model.deleteFromServer({ success: function(data) { var msg = gettext("Successfully deleted %(name)s") - .replace('%(name)s', Common.HTMLescape(dirent_name)); + .replace('%(name)s', dirent_name); Common.feedback(msg, 'success'); }, error: function(xhr) { diff --git a/static/scripts/app/views/group-repo.js b/static/scripts/app/views/group-repo.js index e61c91df4c..b42082d444 100644 --- a/static/scripts/app/views/group-repo.js +++ b/static/scripts/app/views/group-repo.js @@ -54,7 +54,7 @@ define([ this.model.destroy({ wait: true, success: function() { - var msg = gettext('Successfully unshared {placeholder}').replace('{placeholder}', '' + Common.HTMLescape(lib_name) + ''); + var msg = gettext('Successfully unshared 1 item.'); Common.feedback(msg, 'success', Common.SUCCESS_TIMOUT); }, error: function(model, response) { diff --git a/static/scripts/app/views/organization-repo.js b/static/scripts/app/views/organization-repo.js index 765d4a2506..7b3d1d3968 100644 --- a/static/scripts/app/views/organization-repo.js +++ b/static/scripts/app/views/organization-repo.js @@ -45,7 +45,7 @@ define([ dataType: 'json', success: function () { el.remove(); - var msg = gettext('Successfully unshared {placeholder}').replace('{placeholder}', '' + Common.HTMLescape(lib_name) + ''); + var msg = gettext('Successfully unshared 1 item.'); Common.feedback(msg, 'success', Common.SUCCESS_TIMOUT); }, error: function(xhr) { diff --git a/static/scripts/app/views/share.js b/static/scripts/app/views/share.js index a3e01d0a59..e9957535b3 100644 --- a/static/scripts/app/views/share.js +++ b/static/scripts/app/views/share.js @@ -328,11 +328,11 @@ define([ var after_op_success = function(data) { $.modal.close(); var msg = gettext("Successfully sent to {placeholder}") - .replace('{placeholder}', Common.HTMLescape(data['send_success'].join(', '))); + .replace('{placeholder}', data['send_success'].join(', ')); Common.feedback(msg, 'success'); if (data['send_failed'].length > 0) { msg += '
' + gettext("Failed to send to {placeholder}") - .replace('{placeholder}', Common.HTMLescape(data['send_failed'].join(', '))); + .replace('{placeholder}', data['send_failed'].join(', ')); Common.feedback(msg, 'info'); } }; diff --git a/static/scripts/app/views/starred-file-item.js b/static/scripts/app/views/starred-file-item.js index 8621288947..cfc4194daf 100644 --- a/static/scripts/app/views/starred-file-item.js +++ b/static/scripts/app/views/starred-file-item.js @@ -42,7 +42,7 @@ define([ beforeSend: Common.prepareCSRFToken, success: function() { _this.remove(); - Common.feedback(gettext("Successfully unstared {placeholder}").replace('{placeholder}', Common.HTMLescape(file_name)), 'success'); + Common.feedback(gettext("Successfully unstared {placeholder}").replace('{placeholder}', file_name), 'success'); }, error: function (xhr) { Common.ajaxErrorHandler(xhr); diff --git a/static/scripts/common.js b/static/scripts/common.js index b6e3e89790..d383710986 100644 --- a/static/scripts/common.js +++ b/static/scripts/common.js @@ -73,12 +73,15 @@ define([ getUrl: function(options) { var siteRoot = app.config.siteRoot; + var fileServerRoot = app.config.fileServerRoot; switch (options.name) { // File Operations case 'list_lib_dir': return siteRoot + 'ajax/lib/' + options.repo_id + '/dir/'; case 'del_dir': return siteRoot + 'api/v2.1/repos/' + options.repo_id + '/dir/'; case 'del_file': return siteRoot + 'api/v2.1/repos/' + options.repo_id + '/file/'; - case 'download_dirents': return siteRoot + 'api/v2.1/repos/' + options.repo_id + '/dirents/download-link/'; + case 'download_dir_zip_url': return fileServerRoot + 'zip/' + options.zip_token; + case 'zip_task': return siteRoot + 'api/v2.1/repos/' + options.repo_id + '/zip-task/'; + case 'query_zip_progress': return siteRoot + 'api/v2.1/query-zip-progress/'; case 'rename_dir': return siteRoot + 'api/v2.1/repos/' + options.repo_id + '/dir/'; case 'rename_file': return siteRoot + 'api/v2.1/repos/' + options.repo_id + '/file/'; case 'mv_dir': return siteRoot + 'ajax/repo/' + options.repo_id + '/dir/mv/'; @@ -94,6 +97,7 @@ define([ case 'get_cp_progress': return siteRoot + 'ajax/cp_progress/'; case 'cancel_cp': return siteRoot + 'ajax/cancel_cp/'; case 'get_file_op_url': return siteRoot + 'ajax/repo/' + options.repo_id + '/file_op_url/'; + case 'get_file_download_url': return siteRoot + 'lib/' + options.repo_id + '/file' + options.file_path + '?dl=1'; case 'get_file_uploaded_bytes': return siteRoot + 'ajax/repo/' + options.repo_id + '/get-file-uploaded-bytes/'; case 'get_dirents': return siteRoot + 'ajax/repo/' + options.repo_id + '/dirents/'; @@ -327,9 +331,9 @@ define([ feedback: function(con, type, time) { var time = time || 5000; if ($('.messages').length > 0) { - $('.messages').html('
  • ' + con + '
  • '); + $('.messages').html('
  • ' + this.HTMLescape(con) + '
  • '); } else { - var html = '
    • ' + con + '
    '; + var html = '
    • ' + this.HTMLescape(con) + '
    '; $('#main').append(html); } $('.messages').css({'left':($(window).width() - $('.messages').width())/2, 'top':10}).removeClass('hide'); diff --git a/static/scripts/sysadmin-app/views/device.js b/static/scripts/sysadmin-app/views/device.js index a2ff6cd588..99cb2073a8 100644 --- a/static/scripts/sysadmin-app/views/device.js +++ b/static/scripts/sysadmin-app/views/device.js @@ -42,7 +42,7 @@ define([ _this.remove(); var msg = gettext("Successfully unlink %(name)s.") - .replace('%(name)s', Common.HTMLescape(device_name)); + .replace('%(name)s', device_name); Common.feedback(msg, 'success'); }, error: function(xhr) { diff --git a/tests/api/endpoints/test_dirents_download_link.py b/tests/api/endpoints/test_dirents_download_link.py index f69a9b77a7..0cbe7329fa 100644 --- a/tests/api/endpoints/test_dirents_download_link.py +++ b/tests/api/endpoints/test_dirents_download_link.py @@ -6,11 +6,6 @@ from django.core.urlresolvers import reverse from seahub.test_utils import BaseTestCase -try: - from seahub.settings import LOCAL_PRO_DEV_ENV -except ImportError: - LOCAL_PRO_DEV_ENV = False - class DirentsDownloadLinkViewTest(BaseTestCase): diff --git a/tests/api/endpoints/test_query_zip_progress.py b/tests/api/endpoints/test_query_zip_progress.py new file mode 100644 index 0000000000..14e5bb4e5d --- /dev/null +++ b/tests/api/endpoints/test_query_zip_progress.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +import json + +from django.core.urlresolvers import reverse + +from seahub.test_utils import BaseTestCase + +class QueryZipProgressViewTest(BaseTestCase): + + def _get_zip_token(self): + self.login_as(self.user) + + parent_dir = '/' + folder_name = self.folder + args = '?parent_dir=%s&dirents=%s' % (parent_dir, folder_name) + url = reverse('api-v2.1-zip-task', args=[self.repo.id]) + args + + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + json_resp = json.loads(resp.content) + return json_resp['zip_token'] + + def setUp(self): + self.url = reverse('api-v2.1-query-zip-progress') + + def tearDown(self): + self.remove_repo() + + def test_can_get_progress(self): + + token = self._get_zip_token() + + url = self.url + '?token=%s' % token + + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert json_resp['total'] is not None + assert json_resp['zipped'] is not None diff --git a/tests/api/endpoints/test_share_link_zip_task.py b/tests/api/endpoints/test_share_link_zip_task.py new file mode 100644 index 0000000000..27a78fd15b --- /dev/null +++ b/tests/api/endpoints/test_share_link_zip_task.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +import json +from mock import patch + +from django.core.urlresolvers import reverse + +from seahub.test_utils import BaseTestCase +from seahub.share.models import FileShare + +class ShareLinkZipTaskViewTest(BaseTestCase): + + def _add_dir_share_link(self): + fs = FileShare.objects.create_dir_link(self.user.username, + self.repo.id, self.folder, None, None) + + return fs.token + + def setUp(self): + self.url = reverse('api-v2.1-share-link-zip-task') + + def tearDown(self): + self.remove_repo() + + def test_can_get_share_link_zip_task(self): + + share_link_token = self._add_dir_share_link() + + url = self.url + '?share_link_token=%s&path=/' % share_link_token + + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert len(json_resp['zip_token']) == 36 + + @patch('seahub.api2.endpoints.share_link_zip_task.settings.ENABLE_SHARE_LINK_AUDIT') + @patch('seahub.api2.endpoints.share_link_zip_task.is_pro_version') + def test_get_zip_token_with_unauthenticated_user(self, + mock_is_pro_version, mock_enable_share_link_audit): + + mock_is_pro_version.return_value = True + mock_enable_share_link_audit = True + + share_link_token = self._add_dir_share_link() + + url = self.url + '?share_link_token=%s&path=/' % share_link_token + + # user neither login in nor passed code check + resp = self.client.get(url) + self.assertEqual(403, resp.status_code) + + @patch('seahub.api2.endpoints.share_link_zip_task.settings.ENABLE_SHARE_LINK_AUDIT') + @patch('seahub.api2.endpoints.share_link_zip_task.is_pro_version') + def test_get_zip_token_with_authenticated_user(self, + mock_is_pro_version, mock_enable_share_link_audit): + + mock_is_pro_version.return_value = True + mock_enable_share_link_audit = True + + share_link_token = self._add_dir_share_link() + + # user login in + self.login_as(self.admin) + url = self.url + '?share_link_token=%s&path=/' % share_link_token + + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert len(json_resp['zip_token']) == 36 + + @patch('seahub.api2.endpoints.share_link_zip_task.settings.ENABLE_SHARE_LINK_AUDIT') + @patch('seahub.api2.endpoints.share_link_zip_task.is_pro_version') + def test_get_zip_token_with_anonymous_user_passed_code_check(self, + mock_is_pro_version, mock_enable_share_link_audit): + + mock_is_pro_version.return_value = True + mock_enable_share_link_audit = True + + share_link_token = self._add_dir_share_link() + + url = self.url + '?share_link_token=%s&path=/' % share_link_token + + # user pass code check + session = self.client.session + session['anonymous_email'] = 'anonymous@email.com' + session.save() + + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert len(json_resp['zip_token']) == 36 diff --git a/tests/api/endpoints/test_zip_task.py b/tests/api/endpoints/test_zip_task.py new file mode 100644 index 0000000000..b5c43e75f3 --- /dev/null +++ b/tests/api/endpoints/test_zip_task.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +import os +import json + +from django.core.urlresolvers import reverse + +from seahub.test_utils import BaseTestCase + +from seaserv import seafile_api + +try: + from seahub.settings import LOCAL_PRO_DEV_ENV +except ImportError: + LOCAL_PRO_DEV_ENV = False + + +class ZipTaskViewTest(BaseTestCase): + + def setUp(self): + self.repo_id = self.repo.id + self.folder_path = self.folder + self.folder_name = os.path.basename(self.folder_path) + + self.url = reverse('api-v2.1-zip-task', args=[self.repo_id]) + + def tearDown(self): + self.remove_repo() + + def test_can_get_download_dir_zip_token(self): + self.login_as(self.user) + + parent_dir = '/' + folder_name = self.folder_name + url = self.url + '?parent_dir=%s&dirents=%s' % (parent_dir, folder_name) + + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert len(json_resp['zip_token']) == 36 + + def test_can_get_download_multi_zip_token(self): + + # create another folder for download multi + another_folder_name = 'another_folder_name' + seafile_api.post_dir(repo_id=self.repo.id, + parent_dir='/', dirname=another_folder_name, + username=self.user.username) + + self.login_as(self.user) + + parent_dir = '/' + folder_name = self.folder_name + url = self.url + '?parent_dir=%s&dirents=%s&dirents=%s' % (parent_dir, + folder_name, another_folder_name) + + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert len(json_resp['zip_token']) == 36 + + def test_can_get_zip_token_with_invalid_repo_permission(self): + self.login_as(self.admin) + + parent_dir = '/' + folder_name = self.folder_name + url = self.url + '?parent_dir=%s&dirents=%s' % (parent_dir, folder_name) + + resp = self.client.get(url) + self.assertEqual(403, resp.status_code) + + def test_can_get_zip_token_for_r_permission_folder(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.set_user_folder_r_permission_to_admin() + + self.login_as(self.admin) + + parent_dir = '/' + folder_name = self.folder_name + url = self.url + '?parent_dir=%s&dirents=%s' % (parent_dir, folder_name) + + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert len(json_resp['zip_token']) == 36 + + def test_can_get_zip_token_for_rw_permission_folder(self): + + if not LOCAL_PRO_DEV_ENV: + return + + self.set_user_folder_rw_permission_to_admin() + + self.login_as(self.admin) + + parent_dir = '/' + folder_name = self.folder_name + url = self.url + '?parent_dir=%s&dirents=%s' % (parent_dir, folder_name) + + resp = self.client.get(url) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert len(json_resp['zip_token']) == 36 diff --git a/tests/seahub/views/repo/test_shared_dir.py b/tests/seahub/views/repo/test_shared_dir.py index e278c3d33e..6f978b4568 100644 --- a/tests/seahub/views/repo/test_shared_dir.py +++ b/tests/seahub/views/repo/test_shared_dir.py @@ -28,16 +28,6 @@ class SharedDirTest(TestCase, Fixtures): self.assertContains(resp, '

    %s

    ' % self.repo.name) - zip_url = 'href="?p=/&dl=1"' - self.assertContains(resp, zip_url) - - def test_can_download(self): - dl_url = reverse('view_shared_dir', args=[self.fs.token]) + \ - '?p=/&dl=1' - resp = self.client.get(dl_url) - self.assertEqual(302, resp.status_code) - assert '8082/files/' in resp.get('location') - def test_view_raw_file_via_shared_dir(self): resp = self.client.get( reverse('view_file_via_shared_dir', args=[self.fs.token]) + '?p=' + self.file + '&raw=1'