From 2c2bf4f98f78d47b92c9107e7f4726a7cba7dc53 Mon Sep 17 00:00:00 2001 From: lian Date: Sat, 11 Jul 2015 10:10:31 +0800 Subject: [PATCH] file lock --- media/css/seahub.css | 4 ++ media/img/file-locked-32.png | Bin 0 -> 768 bytes seahub/api2/views.py | 84 +++++++++++++++++++++++---- seahub/settings.py | 2 + seahub/templates/file_revisions.html | 2 + seahub/templates/js/templates.html | 26 ++++++++- seahub/templates/view_file_base.html | 55 +++++++++++++++++- seahub/views/__init__.py | 58 +++++++++++++----- seahub/views/ajax.py | 35 ++++++++++- seahub/views/file.py | 32 ++++++++-- static/scripts/app/views/dirent.js | 64 ++++++++++++++++++++ static/scripts/common.js | 1 + 12 files changed, 328 insertions(+), 35 deletions(-) create mode 100644 media/img/file-locked-32.png diff --git a/media/css/seahub.css b/media/css/seahub.css index 20d14c1bf5..773827f690 100644 --- a/media/css/seahub.css +++ b/media/css/seahub.css @@ -3655,6 +3655,10 @@ textarea:-moz-placeholder {/* for FF */ } #user-profile .info-detail { padding-left: 6px; + +/* file lock */ +#view-hd .file-locked-icon { + vertical-align: middle; } /* shared file view */ #shared-file-view-logo { diff --git a/media/img/file-locked-32.png b/media/img/file-locked-32.png new file mode 100644 index 0000000000000000000000000000000000000000..fbf9418978d1a52fa5b67c66a540385198a92763 GIT binary patch literal 768 zcmV+b1ONPqP)HV%7>CW*#>dFS(1`(ogcv{+oI9HZL2O?_m=FU4Q%5A!0Ted=fUZbLED;i7tU7f; zl~60x1yMCZjcYrpNs$Vb_#BS+=h%tkhBk5Rob)Qz`R;k%+*dA&lA^SyjS~&EIOVA3 zc?6zOmxeWT@wjo;W8#s+#JtPI27T^sGV3n_&tcs>+MsP_Vn;VF5d4+L#qPFVmcCY! z^ejn6VTB=rI=n+0v_&8EO)Xwz?5JC?W}BM$)+Q(cC4JkW0&UR;eZc^0B>CqXl~XPk zuLCh3Uc4s}-bn;3Fo%+_(PF|}ZtHhai<1Esm}>_u)T&ZFY%}X+mhyNKV8ij>1RAP% zfQ6K`JPNSk7%2C3VMDwwd zOyI}6_n9RCRx$xN;f~h8Lf;8kFlFohjPz$_GYPQ3jQD|zoV`WJ%(-@A7ueT}LDrcB zvEuBG%bbgE!+Vq71#or~@Ko~*z1SJ$(vdbo#}1r*$1zpblGBh!gpS&PpJMZBlb8ND zIm*F?1FnHT$+R+GpXSU%q*gK+otF z(*_Jfx!*2v!r|_SMH_v=NG0zdjpCHcOHVvzeQ>z-3&RRYJWz*sXoI%sGqyNcRxUj} y*5c)JEpFYVKcSClRX<1I8FhGvHkpg_EdKygwp8Xu|AAfr0000{{ commit.rev_file_size|filesizeformat }} {% if commit.id != repo.head_cmmt_id %} + {% if can_revert_file %} {% trans 'Restore' %} {% endif %} + {% endif %} {% trans 'Download' %} {% trans 'View' %} {% if can_compare and not forloop.last %} diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html index 5dd99a12b1..8e0aac058e 100644 --- a/seahub/templates/js/templates.html +++ b/seahub/templates/js/templates.html @@ -266,6 +266,13 @@ <% } else { %> <%- dirent.obj_name %> <% } %> + <% if (is_pro) { %> + <% if (dirent.is_locked) { %> + locked + <% } else { %> + locked + <% } %> + <% } %> @@ -276,16 +283,33 @@ <% } %> <% if (dirent.perm == 'rw') { %> - + <% if (!dirent.is_locked) { %> + + <% } else { %> + <% if (dirent.locked_by_me) { %> + + <% } %> + <% } %> <% } %> <% if (dirent.perm == 'rw') { %> {% trans <% } %> diff --git a/seahub/templates/view_file_base.html b/seahub/templates/view_file_base.html index e6c0966451..5ed1acd47e 100644 --- a/seahub/templates/view_file_base.html +++ b/seahub/templates/view_file_base.html @@ -12,6 +12,9 @@

{{ filename }} + {% if is_pro %} + {% trans + {% endif %}

@@ -41,6 +44,16 @@ + {% if can_lock_unlock_file %} + {% if not file_locked %} + + + {% elif locked_by_me %} + + + {% endif %} + {% endif %} + {% if not repo.encrypted %} {% if request.user.permissions.can_generate_shared_link %} @@ -51,7 +64,7 @@ {% endif %} - {% if request.user.is_authenticated %} + {% if request.user.is_authenticated and can_edit_file %} {% block edit_file %} {% endblock %} {% endif %} @@ -72,7 +85,7 @@ - {% include "snippets/file_share_popup.html" %} + {% include "snippets/file_share_popup.html" %}
    @@ -125,7 +138,7 @@ $('#file-star').click(function() { } else { url_base = '{% url 'repo_unstar_file' repo.id %}'; } - + $.ajax({ url: url_base + '?file=' + e("{{path|escapejs}}"), cache: false, @@ -137,6 +150,42 @@ $('#file-star').click(function() { }); }); +{% if can_lock_unlock_file %} +// lock file +$('#lock-file').click(function() { + $.ajax({ + url: '{% url 'FileView' repo.id %}', + type: 'PUT', + dataType: 'json', + cache: 'false', + beforeSend: prepareCSRFToken, + data: {'operation': 'lock', 'p': '{{path}}'}, + success: function() { + $('#lock-file').hide(); + $('#unlock-file').show(); + $('.file-locked-icon').show(); + }, + error: ajaxErrorHandler + }); +}); + +$('#unlock-file').click(function() { + $.ajax({ + url: '{% url 'FileView' repo.id %}', + type: 'PUT', + dataType: 'json', + cache: 'false', + beforeSend: prepareCSRFToken, + data: {'operation': 'unlock', 'p': '{{path}}'}, + success: function() { + $('#lock-file').show(); + $('#unlock-file').hide(); + $('.file-locked-icon').hide(); + }, + error: ajaxErrorHandler + }); +}); +{% endif %} // set 'side toolbar' position diff --git a/seahub/views/__init__.py b/seahub/views/__init__.py index 8d829cf7e8..b7704b9b2c 100644 --- a/seahub/views/__init__.py +++ b/seahub/views/__init__.py @@ -27,8 +27,7 @@ from seaserv import get_repo, get_commits, is_valid_filename, \ seafserv_threaded_rpc, seafserv_rpc, is_repo_owner, check_permission, \ is_passwd_set, get_file_size, get_group, get_session_info, get_commit, \ MAX_DOWNLOAD_DIR_SIZE, send_message, ccnet_threaded_rpc, \ - get_personal_groups_by_user -from seaserv import seafile_api + get_personal_groups_by_user, seafile_api from pysearpc import SearpcError from seahub.avatar.util import get_avatar_file_storage @@ -132,6 +131,29 @@ def check_file_permission(request, repo_id, path): return seafile_api.check_permission_by_path(repo_id, path, username) +def check_file_lock(repo_id, file_path, username): + """ check if file is locked to current user + according to returned value of seafile_api.check_file_lock: + + 0: not locked + 1: locked by other + 2: locked by me + -1: error + + return (is_locked, locked_by_me) + """ + return_value = seafile_api.check_file_lock(repo_id, + file_path.lstrip('/'), username) + + if return_value == 0: + return (False, False) + elif return_value == 1: + return (True , False) + elif return_value == 2: + return (True, True) + else: + return None + def check_repo_access_permission(repo_id, user): """Check repo access permission of a user, always return 'rw' when repo is system repo and user is admin. @@ -1196,6 +1218,7 @@ def libraries(request): "sub_lib_enabled": sub_lib_enabled, 'enable_upload_folder': settings.ENABLE_UPLOAD_FOLDER, 'max_upload_file_size': max_upload_file_size, + 'is_pro': True if is_pro_version() else False, 'folder_perm_enabled': folder_perm_enabled, 'is_pro': True if is_pro_version() else False, }, context_instance=RequestContext(request)) @@ -1464,6 +1487,14 @@ def render_file_revisions (request, repo_id): zipped = gen_path_link(path, repo.name) + can_revert_file = True + username = request.user.username + is_locked, locked_by_me = check_file_lock(repo_id, path, username) + print (repo_id, path, username) + if seafile_api.check_permission_by_path(repo_id, path, username) != 'rw' or \ + (is_locked and not locked_by_me): + can_revert_file = False + return render_to_response('file_revisions.html', { 'repo': repo, 'path': path, @@ -1472,6 +1503,7 @@ def render_file_revisions (request, repo_id): 'commits': commits, 'is_owner': is_owner, 'can_compare': can_compare, + 'can_revert_file': can_revert_file, }, context_instance=RequestContext(request)) @login_required @@ -1487,21 +1519,22 @@ def repo_revert_file(request, repo_id): if not (commit_id and path and from_page): return render_error(request, _(u"Invalid arguments")) + referer = request.META.get('HTTP_REFERER', None) + next = settings.SITE_ROOT if referer is None else referer + + username = request.user.username # perm check - if check_folder_permission(request, repo.id, path) != 'rw': - next = request.META.get('HTTP_REFERER', None) - if not next: - next = settings.SITE_ROOT + is_locked, locked_by_me = check_file_lock(repo_id, path, username) + if check_folder_permission(request, repo.id, path) != 'rw' or \ + (is_locked and not locked_by_me): messages.error(request, _("Permission denied")) return HttpResponseRedirect(next) try: - ret = seafile_api.revert_file(repo_id, commit_id, path, request.user.username) + ret = seafile_api.revert_file(repo_id, commit_id, path, username) except Exception as e: logger.error(e) messages.error(request, _('Failed to restore, please try again later.')) - referer = request.META.get('HTTP_REFERER', None) - next = settings.SITE_ROOT if referer is None else referer return HttpResponseRedirect(next) else: if from_page == 'repo_history': @@ -1511,12 +1544,11 @@ def repo_revert_file(request, repo_id): # When revert from recycle page, redirect to recycle page. url = reverse('repo_recycle_view', args=[repo_id]) else: - # When revert file from file history, we redirect to parent dir of this file - parent_dir = os.path.dirname(path) - url = reverse('repo', args=[repo_id]) + ('?p=%s' % urllib2.quote(parent_dir.encode('utf-8'))) + # When revert file from file history, we reload page + url = next if ret == 1: - root_url = reverse('repo', args=[repo_id]) + u'?p=/' + root_url = reverse('view_common_lib_dir', args=[repo_id, '/']) msg = _(u'Successfully revert %(path)s to root directory.') % {"path": escape(path.lstrip('/')), "root": root_url} messages.success(request, msg, extra_tags='safe') else: diff --git a/seahub/views/ajax.py b/seahub/views/ajax.py index 5a3868819f..7a91e6f39d 100644 --- a/seahub/views/ajax.py +++ b/seahub/views/ajax.py @@ -37,7 +37,8 @@ from seahub.signals import upload_file_successful, repo_created, repo_deleted from seahub.views import get_repo_dirents_with_perm, validate_owner, \ check_repo_access_permission, get_unencry_rw_repos_by_user, \ get_system_default_repo_id, get_diff, group_events_data, \ - get_owned_repo_list, check_folder_permission, is_registered_user + get_owned_repo_list, check_folder_permission, is_registered_user, \ + check_file_lock from seahub.views.repo import get_nav_path, get_fileshare, get_dir_share_link, \ get_uploadlink, get_dir_shared_upload_link from seahub.views.modules import get_enabled_mods_by_group, \ @@ -500,11 +501,18 @@ def list_lib_dir(request, repo_id): if not repo.encrypted and ENABLE_THUMBNAIL and \ os.path.exists(os.path.join(THUMBNAIL_ROOT, str(size), f.obj_id)): - file_path = posixpath.join(path, f.obj_name) src = get_thumbnail_src(repo_id, size, file_path) f_['encoded_thumbnail_src'] = urlquote(src) + if is_pro_version(): + f_['is_locked'] = True if f.is_locked else False + f_['lock_owner'] = f.lock_owner + if username == f.lock_owner: + f_['locked_by_me'] = True + else: + f_['locked_by_me'] = False + dirent_list.append(f_) result["dirent_list"] = dirent_list @@ -646,6 +654,12 @@ def rename_dirent(request, repo_id): return HttpResponse(json.dumps({'error': err_msg}), status=403, content_type=content_type) + is_locked, locked_by_me = check_file_lock(repo_id, full_path, username) + if is_locked and not locked_by_me: + err_msg = _('File is locked') + return HttpResponse(json.dumps({'error': err_msg}), status=403, + content_type=content_type) + if newname == oldname: return HttpResponse(json.dumps({'success': True}), content_type=content_type) @@ -702,6 +716,12 @@ def delete_dirent(request, repo_id): return HttpResponse(json.dumps({'error': err_msg}), status=403, content_type=content_type) + is_locked, locked_by_me = check_file_lock(repo_id, full_path, username) + if is_locked and not locked_by_me: + err_msg = _('File is locked') + return HttpResponse(json.dumps({'error': err_msg}), status=403, + content_type=content_type) + # delete file/dir try: seafile_api.del_file(repo_id, parent_dir, dirent_name, username) @@ -823,12 +843,21 @@ def mv_file(request, src_repo_id, src_path, dst_repo_id, dst_path, obj_name): return HttpResponse(json.dumps(result), status=403, content_type=content_type) + file_path = posixpath.join(src_path, obj_name) + is_locked, locked_by_me = check_file_lock(src_repo_id, file_path, username) + if is_locked and not locked_by_me: + err_msg = _('File is locked') + return HttpResponse(json.dumps({'error': err_msg}), status=403, + content_type=content_type) + + new_obj_name = check_filename_with_rename(dst_repo_id, dst_path, obj_name) try: res = seafile_api.move_file(src_repo_id, src_path, obj_name, dst_repo_id, dst_path, new_obj_name, username, need_progress=1) - except SearpcError, e: + except SearpcError as e: + logger.error(e) res = None # res can be None or an object diff --git a/seahub/views/file.py b/seahub/views/file.py index fdc8437385..08cb650906 100644 --- a/seahub/views/file.py +++ b/seahub/views/file.py @@ -51,7 +51,7 @@ from seahub.wiki.utils import get_wiki_dirent from seahub.wiki.models import WikiDoesNotExist, WikiPageMissing from seahub.utils import show_delete_days, render_error, is_org_context, \ get_file_type_and_ext, gen_file_get_url, gen_file_share_link, \ - render_permission_error, \ + render_permission_error, is_pro_version, \ is_textual_file, mkstemp, EMPTY_SHA1, HtmlDiff, \ check_filename_with_rename, gen_inner_file_get_url, normalize_file_path, \ user_traffic_over_limit, do_md5 @@ -60,7 +60,7 @@ from seahub.utils.file_types import (IMAGE, PDF, DOCUMENT, SPREADSHEET, AUDIO, MARKDOWN, TEXT, OPENDOCUMENT, VIDEO) from seahub.utils.star import is_file_starred from seahub.utils import HAS_OFFICE_CONVERTER, FILEEXT_TYPE_MAP -from seahub.views import check_folder_permission +from seahub.views import check_folder_permission, check_file_lock if HAS_OFFICE_CONVERTER: from seahub.utils import ( @@ -461,6 +461,20 @@ def _file_view(request, repo_id, path): office_preview_token = ret_dict.get('office_preview_token', '') + is_locked, locked_by_me = check_file_lock(repo_id, path, username) + file_perm = seafile_api.check_permission_by_path(repo_id, path, username) + + can_edit_file = True + if file_perm == 'r': + can_edit_file = False + elif is_locked and not locked_by_me: + can_edit_file = False + + if is_pro_version() and file_perm == 'rw': + can_lock_unlock_file = True + else: + can_lock_unlock_file = False + return render_to_response(template, { 'repo': repo, 'is_repo_owner': is_repo_owner, @@ -487,6 +501,11 @@ def _file_view(request, repo_id, path): 'last_commit_id': repo.head_cmmt_id, 'is_starred': is_starred, 'user_perm': user_perm, + 'file_locked': is_locked, + 'is_pro': is_pro_version(), + 'locked_by_me': locked_by_me, + 'can_edit_file': can_edit_file, + 'can_lock_unlock_file': can_lock_unlock_file, 'img_prev': img_prev, 'img_next': img_next, 'highlight_keyword': settings.HIGHLIGHT_KEYWORD, @@ -962,8 +981,14 @@ def file_edit_submit(request, repo_id): status=400, content_type=content_type) + path = request.GET.get('p') username = request.user.username - if check_repo_access_permission(repo_id, request.user) != 'rw': + parent_dir = os.path.dirname(path) + + # edit file, so check parent_dir's permission + is_locked, locked_by_me = check_file_lock(repo_id, path, username) + if check_folder_permission(request, repo_id, parent_dir) != 'rw' or \ + (is_locked and not locked_by_me): return error_json(_(u'Permission denied')) repo = get_repo(repo_id) @@ -976,7 +1001,6 @@ def file_edit_submit(request, repo_id): content = request.POST.get('content') encoding = request.POST.get('encoding') - path = request.GET.get('p') if content is None or not path or encoding not in ["gbk", "utf-8"]: return error_json(_(u'Invalid arguments')) diff --git a/static/scripts/app/views/dirent.js b/static/scripts/app/views/dirent.js index e943bc4e50..765bec000b 100644 --- a/static/scripts/app/views/dirent.js +++ b/static/scripts/app/views/dirent.js @@ -42,6 +42,18 @@ define([ is_pro: app.pageOptions.is_pro, repo_encrypted: dir.encrypted })); + + if (this.model.get('is_locked')) { + if (this.model.get('locked_by_me')) { + this.$('.file-lock').hide(); + } else { + this.$('.file-lock').hide(); + this.$('.file-unlock').hide(); + } + } else { + this.$('.file-unlock').hide(); + } + return this; }, @@ -57,9 +69,61 @@ define([ 'click .rename': 'rename', 'click .mv': 'mvcp', 'click .cp': 'mvcp', + 'click .file-lock': 'fileLock', + 'click .file-unlock': 'fileUnlock', 'click .set-folder-permission': 'setFolderPerm' }, + fileLock: function() { + var dir = this.dirView.dir, + _this = this, + filepath = Common.pathJoin([dir.path, this.model.get('obj_name')]); + + $.ajax({ + url: Common.getUrl({name: 'file_lock', repo_id: dir.repo_id}), + type: 'PUT', + dataType: 'json', + data: {'operation': 'lock', 'p': filepath}, + cache: 'false', + beforeSend: Common.prepareCSRFToken, + success: function(data) { + _this.$('.file-locked-icon').show(); + _this.$('.file-unlock').show(); + _this.$('.file-lock').hide(); + }, + error: function (xhr) { + Common.ajaxErrorHandler(xhr); + } + }); + + return false; + }, + + fileUnlock: function() { + var dir = this.dirView.dir, + _this = this, + filepath = Common.pathJoin([dir.path, this.model.get('obj_name')]); + + $.ajax({ + url: Common.getUrl({name: 'file_lock', repo_id: dir.repo_id}), + type: 'PUT', + dataType: 'json', + data: {'operation': 'unlock', 'p': filepath}, + cache: 'false', + beforeSend: Common.prepareCSRFToken, + success: function(data) { + _this.$('.file-locked-icon').hide(); + _this.$('.file-lock').show(); + _this.$('.file-unlock').hide(); + }, + error: function (xhr) { + Common.ajaxErrorHandler(xhr); + } + }); + + return false; + }, + highlight: function() { if (app.globalState.noFileOpPopup) { this.$el.addClass('hl').find('.repo-file-op').removeClass('vh'); diff --git a/static/scripts/common.js b/static/scripts/common.js index b15d073fd3..e5935c863f 100644 --- a/static/scripts/common.js +++ b/static/scripts/common.js @@ -67,6 +67,7 @@ define([ case 'unstar_file': return siteRoot + 'ajax/repo/' + options.repo_id + '/file/unstar/'; case 'del_dir': return siteRoot + 'ajax/repo/' + options.repo_id + '/dir/delete/'; case 'del_file': return siteRoot + 'ajax/repo/' + options.repo_id + '/file/delete/'; + case 'file_lock': return siteRoot + 'api2/repos/' + options.repo_id + '/file/'; case 'rename_dir': return siteRoot + 'ajax/repo/' + options.repo_id + '/dir/rename/'; case 'rename_file': return siteRoot + 'ajax/repo/' + options.repo_id + '/file/rename/'; case 'mv_dir': return siteRoot + 'ajax/repo/' + options.repo_id + '/dir/mv/';