diff --git a/seahub/api2/endpoints/file_history.py b/seahub/api2/endpoints/file_history.py new file mode 100644 index 0000000000..8ce901fe3f --- /dev/null +++ b/seahub/api2/endpoints/file_history.py @@ -0,0 +1,112 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +import logging + +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 seaserv import seafile_api + +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.utils import api_error +from seahub.utils.timeutils import timestamp_to_isoformat_timestr +from seahub.utils.file_revisions import get_file_revisions_within_limit +from seahub.views import check_folder_permission +from seahub.avatar.templatetags.avatar_tags import api_avatar_url +from seahub.base.templatetags.seahub_tags import email2nickname, \ + email2contact_email + +logger = logging.getLogger(__name__) + +def get_file_history_info(commit, avatar_size): + + info = {} + + creator_name = commit.creator_name + url, is_default, date_uploaded = api_avatar_url(creator_name, avatar_size) + + info['creator_avatar_url'] = url + info['creator_email'] = creator_name + info['creator_name'] = email2nickname(creator_name) + info['creator_contact_email'] = email2contact_email(creator_name) + info['ctime'] = timestamp_to_isoformat_timestr(commit.ctime) + info['description'] = commit.desc + info['commit_id'] = commit.id + info['size'] = commit.rev_file_size + info['rev_file_id'] = commit.rev_file_id + info['rev_renamed_old_path'] = commit.rev_renamed_old_path + + return info + + +class FileHistoryView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + + def get(self, request, repo_id): + """ Get file history within certain commits. + + Controlled by path(rev_renamed_old_path), commit_id and next_start_commit. + """ + # argument check + path = request.GET.get('path', '') + if not path: + error_msg = 'path invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # 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) + + commit_id = request.GET.get('commit_id', '') + if not commit_id: + commit_id = repo.head_cmmt_id + + try: + avatar_size = int(request.GET.get('avatar_size', 32)) + except ValueError: + avatar_size = 32 + + # Don't use seafile_api.get_file_id_by_path() + # if path parameter is `rev_renamed_old_path`. + # seafile_api.get_file_id_by_path() will return None. + file_id = seafile_api.get_file_id_by_commit_and_path(repo_id, + commit_id, path) + if not file_id: + error_msg = 'File %s not found.' % path + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + if not check_folder_permission(request, repo_id, '/'): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # get file history + limit = request.GET.get('limit', 15) + try: + limit = 100 if int(limit) < 1 else int(limit) + except ValueError: + limit = 100 + + try: + file_revisions, next_start_commit = get_file_revisions_within_limit( + repo_id, path, commit_id, limit) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + result = [] + for commit in file_revisions: + info = get_file_history_info(commit, avatar_size) + info['path'] = path + result.append(info) + + return Response({"data": result, \ + "next_start_commit": next_start_commit or False}) diff --git a/seahub/api2/views.py b/seahub/api2/views.py index 97e4a410a3..b8ab1b9dfb 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -70,6 +70,8 @@ from seahub.utils import gen_file_get_url, gen_token, gen_file_upload_url, \ gen_shared_upload_link, convert_cmmt_desc_link, is_valid_dirent_name, \ is_org_repo_creation_allowed, is_windows_operating_system, \ get_no_duplicate_obj_name + +from seahub.utils.file_revisions import get_file_revisions_after_renamed from seahub.utils.devices import do_unlink_device from seahub.utils.repo import get_sub_repo_abbrev_origin_path, get_repo_owner from seahub.utils.star import star_file, unstar_file @@ -2622,24 +2624,39 @@ class FileRevision(APIView): return get_repo_file(request, repo_id, obj_id, file_name, 'download') class FileHistory(APIView): - authentication_classes = (TokenAuthentication, ) + authentication_classes = (TokenAuthentication,) permission_classes = (IsAuthenticated,) - throttle_classes = (UserRateThrottle, ) + throttle_classes = (UserRateThrottle,) def get(self, request, repo_id, format=None): + """ Get file history. + """ + path = request.GET.get('p', None) if path is None: - return api_error(status.HTTP_400_BAD_REQUEST, 'Path is missing.') + error_msg = 'p invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + 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) + + file_id = seafile_api.get_file_id_by_path(repo_id, path) + if not file_id: + error_msg = 'File %s not found.' % path + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + if check_folder_permission(request, repo_id, path) != 'rw': + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) try: - commits = seafserv_threaded_rpc.list_file_revisions(repo_id, path, - -1, -1) - except SearpcError as e: + commits = get_file_revisions_after_renamed(repo_id, path) + except Exception as e: logger.error(e) - return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, "Internal error") - - if not commits: - return api_error(status.HTTP_404_NOT_FOUND, 'File not found.') + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) for commit in commits: creator_name = commit.creator_name @@ -2651,7 +2668,8 @@ class FileHistory(APIView): commit._dict['user_info'] = user_info - return HttpResponse(json.dumps({"commits": commits}, cls=SearpcObjEncoder), status=200, content_type=json_content_type) + return HttpResponse(json.dumps({"commits": commits}, + cls=SearpcObjEncoder), status=200, content_type=json_content_type) class FileSharedLinkView(APIView): """ @@ -3242,8 +3260,7 @@ class SharedFileDetailView(APIView): file_id = None try: file_id = seafile_api.get_file_id_by_path(repo_id, path) - commits = seafserv_threaded_rpc.list_file_revisions(repo_id, path, - -1, -1) + commits = get_file_revisions_after_renamed(repo_id, path) c = commits[0] except SearpcError, e: return api_error(HTTP_520_OPERATION_FAILED, diff --git a/seahub/settings.py b/seahub/settings.py index 31abdbb971..216c3d894e 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -403,6 +403,7 @@ REST_FRAMEWORK = { REST_FRAMEWORK_THROTTING_WHITELIST = [] # file and path +GET_FILE_HISTORY_TIMEOUT = 10 * 60 # seconds MAX_UPLOAD_FILE_NAME_LEN = 255 MAX_FILE_NAME = MAX_UPLOAD_FILE_NAME_LEN MAX_PATH = 4096 diff --git a/seahub/templates/file_revisions.html b/seahub/templates/file_revisions.html index 1771ccbddb..78daae4209 100644 --- a/seahub/templates/file_revisions.html +++ b/seahub/templates/file_revisions.html @@ -1,5 +1,6 @@ {% extends 'base_wide_page.html' %} -{% load seahub_tags avatar_tags i18n %} + +{% load seahub_tags avatar_tags i18n staticfiles %} {% load url from future %} {% block sub_title %}{% trans "History" %} - {% endblock %} @@ -12,6 +13,7 @@ {% block wide_page_content %}

{% blocktrans %}{{ u_filename }} Version History{% endblocktrans %}

+ {% if referer %} @@ -30,73 +32,187 @@ {% endif %} {% endfor %}

- - - {% if days != 7 %} -
{% trans "a week" %} / - {% else %} - {% trans "a week" %} / - {% endif %} - {% if days != 30 %} - {% trans "a month" %} / - {% else %} - {% trans "a month" %} / - {% endif %} - {% if days != -1 %} - {% trans "all" %} - {% else %} - {% trans "all" %} - {% endif %} - - - - - - - - {% for commit in commits %} - - + + + + + + + - {% if commit.rev_renamed_old_path %} -
{% blocktrans with old_path=commit.rev_renamed_old_path %}(Renamed or moved from {{ old_path }}){% endblocktrans %} - {% endif %} - - - {% if commit.creator_name %} - - {% else %} - - {% endif %} - - - - - {% endfor %} + +
{% trans 'Time' %}{% trans 'Modifier' %}{% trans 'Size' %}{% trans 'Operations' %}
- {{ commit.props.ctime|translate_seahub_time }} - {% if commit.is_first_commit %} - {% trans '(current version)' %} - {% endif %} +
{% trans 'Time' %}{% trans 'Modifier' %}{% trans 'Size' %}{% trans 'Operations' %}
- {% avatar commit.creator_name 16 %} - {{ commit.creator_name|email2nickname }} - {% trans 'Unknown' %}{{ commit.rev_file_size|filesizeformat }} - {% if can_revert_file and not commit.is_first_commit %} - {% trans 'Restore' %} - {% endif %} - {% trans 'Download' %} - {% trans 'View' %} - {% if can_compare and not forloop.last %} - {% trans 'Diff' %} - {% endif %} -
+ +
+
+ +
+ {% endblock %} {% block extra_script %} + + + + + + + {% endblock %} diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html index ca1f16a1d0..b2ae2c09de 100644 --- a/seahub/templates/js/templates.html +++ b/seahub/templates/js/templates.html @@ -637,6 +637,7 @@
  • {% trans "Copy" %}
  • <% } %>
  • {% trans "Comment" %}
  • +
  • {% trans "History" %}
  • {% trans "Details" %}
  • @@ -748,6 +749,7 @@
  • {% trans "Details" %}
  • <% } else if (dirent.perm == 'r') { %>
  • {% trans "Comment" %}
  • +
  • {% trans "History" %}
  • {% trans "Details" %}
  • <% } %> @@ -826,6 +828,7 @@ <% } else if (dirent.perm == 'r') { %>
  • {% trans "Comment" %}
  • +
  • {% trans "History" %}
  • {% trans "Details" %}
  • <% } %> diff --git a/seahub/urls.py b/seahub/urls.py index 4140550dd4..48e91e898d 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -32,6 +32,7 @@ from seahub.api2.endpoints.repos_batch import ReposBatchView, \ ReposBatchCopyDirView, ReposBatchCreateDirView from seahub.api2.endpoints.repos import RepoView from seahub.api2.endpoints.file import FileView +from seahub.api2.endpoints.file_history import FileHistoryView from seahub.api2.endpoints.dir import DirView, DirDetailView from seahub.api2.endpoints.file_tag import FileTagView from seahub.api2.endpoints.file_tag import FileTagsView @@ -253,6 +254,7 @@ urlpatterns = patterns( url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/tags/$', FileTagsView.as_view(), name="api-v2.1-filetags-view"), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/tags/(?P.*?)/$',FileTagView.as_view(), name="api-v2.1-filetag-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})/file/history/$', FileHistoryView.as_view(), name='api-v2.1-file-history-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'), url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/trash/$', RepoTrash.as_view(), name='api-v2.1-repo-trash'), diff --git a/seahub/utils/file_revisions.py b/seahub/utils/file_revisions.py new file mode 100644 index 0000000000..7b820f5598 --- /dev/null +++ b/seahub/utils/file_revisions.py @@ -0,0 +1,78 @@ +# Copyright (c) 2012-2016 Seafile Ltd. +# encoding: utf-8 +import time +from seaserv import seafile_api +from seahub.settings import GET_FILE_HISTORY_TIMEOUT + +def get_file_revisions_within_limit(repo_id, path, commit_id=None, limit=100): + if not commit_id: + repo = seafile_api.get_repo(repo_id) + commit_id = repo.head_cmmt_id + + file_revisions = seafile_api.get_file_revisions(repo_id, + commit_id, path, limit) + next_start_commit = file_revisions[-1].next_start_commit + return file_revisions[0:-1], next_start_commit + +def get_file_revisions_after_renamed(repo_id, path): + all_file_revisions = [] + repo = seafile_api.get_repo(repo_id) + commit_id = repo.head_cmmt_id + + start_time = time.time() + keep_on_search = True + while keep_on_search: + file_revisions = seafile_api.get_file_revisions(repo_id, + commit_id, path, 100) + + all_file_revisions += file_revisions[0:-1] + + end_time = time.time() + next_start_commit = file_revisions[-1].next_start_commit + rev_renamed_old_path = file_revisions[-2].rev_renamed_old_path if \ + len(file_revisions) > 1 else None + + if not next_start_commit or \ + rev_renamed_old_path or \ + end_time - start_time > GET_FILE_HISTORY_TIMEOUT: + # have searched all commits or + # found a file renamed/moved commit or + # timeout + keep_on_search = False + else: + # keep on searching, use next_start_commit + # as the commit_id start to search + commit_id = next_start_commit + + return all_file_revisions + +def get_all_file_revisions(repo_id, path, commit_id=None): + """ Only used for test revert file. + + py.test tests/api/endpoints/test_file_view.py::FileViewTest::test_can_revert_file + """ + + all_file_revisions = [] + + if not commit_id: + repo = seafile_api.get_repo(repo_id) + commit_id = repo.head_cmmt_id + + file_revisions = seafile_api.get_file_revisions(repo_id, + commit_id, path, -1) + all_file_revisions += file_revisions + + # if commit's rev_renamed_old_path value not None, seafile will stop searching. + # so always uses `rev_renamed_old_path` info. + next_start_commit = file_revisions[-1].next_start_commit + if next_start_commit: + path = file_revisions[-2].rev_renamed_old_path if \ + len(file_revisions) > 1 else None + file_revisions = get_all_file_revisions(repo_id, path, + next_start_commit) + all_file_revisions += file_revisions + + # from seafile_api: + # @next_start_commit: commit_id for next page. + # An extra commit which only contains @next_start_commit will be appended to the list. + return all_file_revisions[0:-1] diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py index acc54ef43a..4d7de67c1e 100644 --- a/seahub/views/__init__.py +++ b/seahub/views/__init__.py @@ -823,55 +823,41 @@ def validate_filename(request): content_type = 'application/json; charset=utf-8' return HttpResponse(json.dumps(result), content_type=content_type) -def render_file_revisions (request, repo_id): - """List all history versions of a file.""" - - days_str = request.GET.get('days', '') - try: - days = int(days_str) - except ValueError: - days = 7 - - path = request.GET.get('p', '/') - if path[-1] == '/': - path = path[:-1] - u_filename = os.path.basename(path) - - if not path: - return render_error(request) - +@login_required +def file_revisions(request, repo_id): + """List file revisions in file version history page. + """ repo = get_repo(repo_id) if not repo: error_msg = _(u"Library does not exist") return render_error(request, error_msg) + # perm check + if not check_folder_permission(request, repo_id, '/'): + error_msg = _(u"Permission denied.") + return render_error(request, error_msg) + + path = request.GET.get('p', '/') + if not path: + return render_error(request) + + if path[-1] == '/': + path = path[:-1] + + u_filename = os.path.basename(path) + filetype = get_file_type_and_ext(u_filename)[0].lower() if filetype == 'text' or filetype == 'markdown': can_compare = True else: can_compare = False - try: - commits = seafile_api.get_file_revisions(repo_id, path, -1, -1, days) - except SearpcError, e: - logger.error(e.msg) - return render_error(request, e.msg) - - if not commits: - return render_error(request, _(u'No revisions found')) - # Check whether user is repo owner if validate_owner(request, repo_id): is_owner = True else: is_owner = False - cur_path = path - for commit in commits: - commit.path = cur_path - if commit.rev_renamed_old_path: - cur_path = '/' + commit.rev_renamed_old_path - zipped = gen_path_link(path, repo.name) can_revert_file = True @@ -882,8 +868,6 @@ def render_file_revisions (request, repo_id): (is_locked and not locked_by_me): can_revert_file = False - commits[0].is_first_commit = True - # for 'go back' referer = request.GET.get('referer', '') @@ -892,27 +876,12 @@ def render_file_revisions (request, repo_id): 'path': path, 'u_filename': u_filename, 'zipped': zipped, - 'commits': commits, 'is_owner': is_owner, 'can_compare': can_compare, 'can_revert_file': can_revert_file, - 'days': days, 'referer': referer, }, context_instance=RequestContext(request)) -@login_required -def file_revisions(request, repo_id): - """List file revisions in file version history page. - """ - repo = get_repo(repo_id) - if not repo: - raise Http404 - - # perm check - if check_folder_permission(request, repo_id, '/') is None: - raise Http404 - - return render_file_revisions(request, repo_id) def demo(request): """ diff --git a/tests/api/endpoints/test_file_history.py b/tests/api/endpoints/test_file_history.py new file mode 100644 index 0000000000..ff962f84a0 --- /dev/null +++ b/tests/api/endpoints/test_file_history.py @@ -0,0 +1,41 @@ +import json + +from django.core.urlresolvers import reverse + +from seahub.test_utils import BaseTestCase +from tests.common.utils import randstring + +class FileHistoryTest(BaseTestCase): + + def setUp(self): + + self.endpoint = reverse('api-v2.1-file-history-view', args=[self.repo.id]) + self.username = self.user.username + + def tearDown(self): + self.remove_repo() + + def test_can_get(self): + self.login_as(self.user) + file_path = self.file + resp = self.client.get(self.endpoint + "?path=%s" % file_path) + self.assertEqual(200, resp.status_code) + json_resp = json.loads(resp.content) + assert json_resp['data'][0]['path'] == file_path + assert json_resp['data'][0]['creator_email'] == self.user.username + + def test_can_get_with_invalid_path_parameter(self): + self.login_as(self.user) + resp = self.client.get(self.endpoint) + self.assertEqual(400, resp.status_code) + + def test_can_get_with_invalid_user_permission(self): + self.login_as(self.admin) + file_path = self.file + resp = self.client.get(self.endpoint + "?path=%s" % file_path) + self.assertEqual(403, resp.status_code) + + def test_can_get_with_invalid_file(self): + self.login_as(self.admin) + resp = self.client.get(self.endpoint + "?path=%s" % randstring(6)) + self.assertEqual(404, resp.status_code) diff --git a/tests/api/endpoints/test_file_view.py b/tests/api/endpoints/test_file_view.py index b9bfd8bdae..94fe9bcfe9 100644 --- a/tests/api/endpoints/test_file_view.py +++ b/tests/api/endpoints/test_file_view.py @@ -9,6 +9,7 @@ from django.core.urlresolvers import reverse from seahub.test_utils import BaseTestCase from seahub.utils import check_filename_with_rename +from seahub.utils.file_revisions import get_all_file_revisions from tests.common.utils import randstring @@ -484,8 +485,7 @@ class FileViewTest(BaseTestCase): new_file_path = '/' + new_name # get file revisions - commits = seafile_api.get_file_revisions( - self.repo_id, new_file_path, -1, -1, 100) + commits = get_all_file_revisions(self.repo_id, new_file_path) # then revert file data = { @@ -504,8 +504,7 @@ class FileViewTest(BaseTestCase): new_file_path = '/' + new_name # get file revisions - commits = seafile_api.get_file_revisions( - self.repo_id, new_file_path, -1, -1, 100) + commits = get_all_file_revisions(self.repo_id, new_file_path) # then revert file data = { @@ -523,8 +522,7 @@ class FileViewTest(BaseTestCase): new_file_path = '/' + new_name # get file revisions - commits = seafile_api.get_file_revisions( - self.repo_id, new_file_path, -1, -1, 100) + commits = get_all_file_revisions(self.repo_id, new_file_path) self.share_repo_to_admin_with_r_permission() self.login_as(self.admin) diff --git a/tests/seahub/views/init/test_file_revisions.py b/tests/seahub/views/init/test_file_revisions.py index 7a9fac9b8e..924af7f03f 100644 --- a/tests/seahub/views/init/test_file_revisions.py +++ b/tests/seahub/views/init/test_file_revisions.py @@ -12,4 +12,3 @@ class RepoBasicInfoTest(BaseTestCase): self.assertEqual(200, resp.status_code) self.assertTemplateUsed(resp, 'file_revisions.html') - assert len(resp.context['commits']) == 1