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 %}
-
-
- {% trans 'Time' %} |
- {% trans 'Modifier' %} |
- {% trans 'Size' %} |
- {% trans 'Operations' %} |
-
- {% for commit in commits %}
-
-
- {{ commit.props.ctime|translate_seahub_time }}
- {% if commit.is_first_commit %}
- {% trans '(current version)' %}
- {% endif %}
+
+
+ {% trans 'Time' %} |
+ {% trans 'Modifier' %} |
+ {% trans 'Size' %} |
+ {% trans 'Operations' %} |
+
+
- {% 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 %}
-
- {% avatar commit.creator_name 16 %}
- {{ commit.creator_name|email2nickname }}
- |
- {% else %}
- {% trans 'Unknown' %} |
- {% endif %}
-
- {{ 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 %}
- |
-
- {% endfor %}
+
+
+
+
+
{% 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 "History" %}
{% trans "Details" %}
@@ -748,6 +749,7 @@
{% trans "Details" %}
<% } else if (dirent.perm == 'r') { %>
+ {% trans "History" %}
{% trans "Details" %}
<% } %>
@@ -826,6 +828,7 @@
<% } else if (dirent.perm == 'r') { %>
+ {% 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