diff --git a/forms.py b/forms.py index 940e9d9be1..a4a73e4774 100644 --- a/forms.py +++ b/forms.py @@ -32,3 +32,13 @@ class AddUserForm(forms.Form): if self.cleaned_data['password1'] != self.cleaned_data['password2']: raise forms.ValidationError(_("The two password fields didn't match.")) return self.cleaned_data + +class FileLinkShareForm(forms.Form): + """ + Form for sharing file shared link to emails. + """ + + email = forms.CharField(max_length=512) + file_shared_link = forms.CharField(max_length=40) + + diff --git a/media/css/seahub.css b/media/css/seahub.css index 0e5728427b..c2e5eab207 100644 --- a/media/css/seahub.css +++ b/media/css/seahub.css @@ -466,6 +466,7 @@ table img { margin-top:8px; } /*repo-share-form*/ +#email, #email_or_group, #share-link, #added-member-name { @@ -653,3 +654,4 @@ table img { max-width: 550px; max-height: 550px; } + diff --git a/share/models.py b/share/models.py index 2f826b4cf0..f707d43e8d 100644 --- a/share/models.py +++ b/share/models.py @@ -1,8 +1,21 @@ +import datetime from django.db import models class AnonymousShare(models.Model): + """ + Model used for sharing repo to unregistered email. + """ repo_owner = models.EmailField(max_length=255) repo_id = models.CharField(max_length=36) anonymous_email = models.EmailField(max_length=255) token = models.CharField(max_length=25, unique=True) - + +class FileShare(models.Model): + """ + Model used for file share link. + """ + username = models.EmailField(max_length=255) + repo_id = models.CharField(max_length=36, db_index=True) + path = models.TextField() + token = models.CharField(max_length=10, unique=True) + ctime = models.DateTimeField(default=datetime.datetime.now) diff --git a/templates/repo.html b/templates/repo.html index df799839cc..46c30de988 100644 --- a/templates/repo.html +++ b/templates/repo.html @@ -153,9 +153,9 @@ 文件 {% if view_history %} - {{ dirent.props.obj_name }} + {{ dirent.props.obj_name }} {% else %} - {{ dirent.props.obj_name }} + {{ dirent.props.obj_name }} {% endif %} {{ dirent.file_size|filesizeformat }} diff --git a/templates/repo_view_file.html b/templates/repo_view_file.html index 9451886e26..fe9bc8c1f0 100644 --- a/templates/repo_view_file.html +++ b/templates/repo_view_file.html @@ -25,6 +25,17 @@

查看原始文件

查看所有历史版本

下载文件

+ + {% if not view_history %} +

共享

+ + + {% endif %}
@@ -44,17 +55,53 @@

正在读取文件内容...
+ + + {% endblock %} {% block extra_script %} {% endblock %} diff --git a/templates/shared_link_email.html b/templates/shared_link_email.html new file mode 100644 index 0000000000..f3fc756460 --- /dev/null +++ b/templates/shared_link_email.html @@ -0,0 +1,12 @@ +{% autoescape off %} +亲爱的 {{ to_email }}: + +{{ email }} 在SeaCloud上共享了一个文件给你,请点击以下链接查看: + +{{ file_shared_link }} + +感谢使用我们的网站! + +Seafile团队 + +{% endautoescape %} diff --git a/templates/view_shared_file.html b/templates/view_shared_file.html new file mode 100644 index 0000000000..f12ee86582 --- /dev/null +++ b/templates/view_shared_file.html @@ -0,0 +1,54 @@ +{% extends "myhome_base.html" %} +{% load seahub_tags %} + +{% block main_panel %} +

+ {{ file_name }}
+ 共享者:{{ username }} +

+ +
+

操作

+

查看原始文件

+

下载文件

+
+ +
+
正在读取文件内容...
+
+ +{% endblock %} + +{% block extra_script %} + +{% endblock %} diff --git a/urls.py b/urls.py index d9b60c13c1..fd61b20787 100644 --- a/urls.py +++ b/urls.py @@ -6,13 +6,14 @@ from seahub.views import root, peers, myhome, \ repo, repo_history, modify_token, remove_repo, sys_seafadmin, sys_useradmin, \ org_seafadmin, org_useradmin, org_group_admin, org_remove, \ activate_user, user_add, user_remove, sys_group_admin, sys_org_admin, \ - ownerhome, repo_history_revert, \ + ownerhome, repo_history_revert, repo_file_get, \ user_info, repo_set_access_property, repo_access_file, \ repo_remove_share, repo_download, org_info, repo_view_file, \ seafile_access_check, back_local, repo_history_changes, \ - repo_upload_file, file_upload_progress, file_upload_progress_page, get_subdir, file_move, \ - repo_new_dir, repo_rename_file, validate_filename, \ - repo_create, repo_update_file, file_revisions + repo_upload_file, file_upload_progress, file_upload_progress_page, \ + get_subdir, file_move, repo_new_dir, repo_rename_file, validate_filename, \ + repo_create, repo_update_file, file_revisions, \ + get_shared_link, view_shared_file, remove_shared_link, send_shared_link from seahub.notifications.views import notification_list from seahub.share.views import share_admin from seahub.group.views import group_list @@ -42,7 +43,11 @@ urlpatterns = patterns('', (r'^share/', include('share.urls')), url(r'^shareadmin/$', share_admin, name='share_admin'), (r'^shareadmin/removeshare/$', repo_remove_share), - + (r'^sharedlink/get/$', get_shared_link), + (r'^sharedlink/remove/$', remove_shared_link), + (r'^sharedlink/send/$', send_shared_link), + (r'^f/(?P[^/]+)/$', view_shared_file), + (r'^file_upload_progress/$', file_upload_progress), (r'^file_upload_progress_page/$', file_upload_progress_page), (r'^repo/new_dir/$', repo_new_dir), @@ -61,7 +66,8 @@ urlpatterns = patterns('', # (r'^repo/removefetched/(?P[^/]+)/(?P[^/]+)/$', remove_fetched_repo), # (r'^repo/setap/(?P[^/]+)/$', repo_set_access_property), url(r'^repo/(?P[^/]+)/(?P[^/]+)/$', repo_access_file, name='repo_access_file'), - (r'^repo/(?P[^/]+)/view/(?P[^/]+)/$', repo_view_file), + (r'^repo/(?P[^/]+)/files/$', repo_view_file), + (r'^repo/(?P[^/]+)/file/get/$', repo_file_get), (r'^download/repo/$', repo_download), (r'^file/move/get_subdir/$', get_subdir), diff --git a/utils.py b/utils.py index 0b529065d7..0b26dcab57 100644 --- a/utils.py +++ b/utils.py @@ -68,13 +68,13 @@ def get_ccnetapplet_root(): ccnet_applet_root = settings.CCNET_APPLET_ROOT return ccnet_applet_root -def gen_token(): +def gen_token(max_length=5): """ - Generate short token used for owner to access repo file. + Generate a random token. """ - token = sha_constructor(settings.SECRET_KEY + unicode(time.time())).hexdigest()[:5] + token = sha_constructor(settings.SECRET_KEY + unicode(time.time())).hexdigest()[:max_length] return token def validate_group_name(group_name): diff --git a/views.py b/views.py index 9554a1c764..7218e0a0a0 100644 --- a/views.py +++ b/views.py @@ -24,6 +24,7 @@ from auth.decorators import login_required from auth.forms import AuthenticationForm, PasswordResetForm, SetPasswordForm, \ PasswordChangeForm from auth.tokens import default_token_generator +from share.models import FileShare from seaserv import ccnet_rpc, ccnet_threaded_rpc, get_groups, get_users, get_repos, \ get_repo, get_commits, get_branches, \ seafserv_threaded_rpc, seafserv_rpc, get_binding_peerids, get_ccnetuser, \ @@ -33,7 +34,7 @@ from pysearpc import SearpcError from seahub.base.accounts import CcnetUser from seahub.contacts.models import Contact from seahub.notifications.models import UserNotification -from forms import AddUserForm +from forms import AddUserForm, FileLinkShareForm from utils import go_permission_error, go_error, list_to_string, \ get_httpserver_root, get_ccnetapplet_root, gen_token, \ calculate_repo_last_modify, valid_previewed_file, \ @@ -147,8 +148,9 @@ def gen_path_link(path, repo_name): Generate navigate paths and links in repo page. """ - if path[-1:] != '/': + if path and path[-1] != '/': path += '/' + paths = [] links = [] if path and path != '/': @@ -210,7 +212,6 @@ def render_repo(request, repo_id, error=''): # query repo infomation repo_size = seafserv_threaded_rpc.server_repo_size(repo_id) -# latest_commit = get_commits(repo_id, 0, 1)[0] # get repo dirents dirs = [] @@ -789,61 +790,37 @@ def repo_del_file(request, repo_id): url = reverse('repo', args=[repo_id]) + ('?p=%s' % parent_dir) return HttpResponseRedirect(url) -def repo_view_file(request, repo_id, obj_id): +def repo_view_file(request, repo_id): + """ + Preview file on web, including files in current worktree and history. + """ http_server_root = get_httpserver_root() - filename = urllib2.quote(request.GET.get('file_name', '').encode('utf-8')) + path = request.GET.get('p', '/') + if path[-1] != '/': + path = path + '/' + filename = urllib2.quote(os.path.basename(path[:-1]).encode('utf-8')) + commit_id = request.GET.get('commit_id', '') view_history = True if commit_id else False current_commit = seafserv_threaded_rpc.get_commit(commit_id) if not current_commit: current_commit = get_commits(repo_id, 0, 1)[0] - if request.is_ajax(): - content_type = 'application/json; charset=utf-8' - token = request.GET.get('t') - tmp_str = '%s/access?repo_id=%s&id=%s&filename=%s&op=%s&t=%s&u=%s' - redirect_url = tmp_str % (http_server_root, - repo_id, obj_id, - filename, 'view', - token, - request.user.username) + if view_history: + obj_id = request.GET.get('obj_id', '') + else: try: - proxied_request = urllib2.urlopen(redirect_url) - if long(proxied_request.headers['Content-Length']) > FILE_PREVIEW_MAX_SIZE: - data = json.dumps([{'error': '文件超过10M,无法在线查看。'}]) - return HttpResponse(data, status=400, content_type=content_type) - else: - content = proxied_request.read() - except urllib2.HTTPError, e: - err = 'HTTPError: 无法在线打开该文件' - data = json.dumps([{'error': err}]) - return HttpResponse(data, status=400, content_type=content_type) - except urllib2.URLError as e: - err = 'URLError: 无法在线打开该文件' - data = json.dumps([{'error': err}]) - return HttpResponse(data, status=400, content_type=content_type) - else: - l, d = [], {} - try: - # XXX: file in windows is encoded in gbk - u_content = content.decode('gbk') - except: - u_content = content.decode('utf-8') - from django.utils.html import escape - d['content'] = re.sub("\r\n|\n", "
", escape(u_content)) - l.append(d) - data = json.dumps(l) - return HttpResponse(data, status=200, content_type=content_type) + obj_id = seafserv_rpc.get_file_by_path(repo_id, path[:-1]) + except: + obj_id = None + + if not obj_id: + return go_error(request, '文件不存在') repo = get_repo(repo_id) if not repo: raise Http404 - # if a repo doesn't have access property in db, then assume it's 'own' - repo_ap = seafserv_threaded_rpc.repo_query_access_property(repo_id) - if not repo_ap: - repo_ap = 'own' - # if a repo is shared to me, then I can view and download file no mater whether # repo's access property is 'own' or 'public' if check_shared_repo(request, repo_id): @@ -852,30 +829,22 @@ def repo_view_file(request, repo_id, obj_id): share_to_me = False token = '' - if repo_ap == 'own': - # people who is owner or this repo is shared to him, can visit the repo; - # others, just go to 404 page - if validate_owner(request, repo_id) or share_to_me: - # owner should get a token to visit repo - token = gen_token() - # put token into memory in seaf-server - seafserv_rpc.web_save_access_token(token, obj_id) - else: - raise Http404 + # people who is owner or this repo is shared to him, can visit the repo; + # others, just go to 404 page + if validate_owner(request, repo_id) or share_to_me: + # owner should get a token to visit repo + token = gen_token() + # put token into memory in seaf-server + seafserv_rpc.web_save_access_token(token, obj_id) + else: + raise Http404 - # query commit info - commit_id = request.GET.get('commit_id', None) - current_commit = seafserv_threaded_rpc.get_commit(commit_id) - if not current_commit: - current_commit = get_commits(repo.id, 0, 1)[0] - # generate path and link - path = request.GET.get('p', '/') zipped = gen_path_link(path, repo.name) - # filename + # determin whether file can preview on web can_preview, filetype = valid_previewed_file(filename) - + # raw path tmp_str = '%s/access?repo_id=%s&id=%s&filename=%s&op=%s&t=%s&u=%s' raw_path = tmp_str % (http_server_root, @@ -883,12 +852,26 @@ def repo_view_file(request, repo_id, obj_id): filename, 'view', token, request.user.username) - + + # file share link + l = FileShare.objects.filter(repo_id=repo_id).filter(path=path[:-1]) + fileshare = l[0] if len(l) > 0 else None + + http_or_https = request.is_secure() and 'https' or 'http' + domain = RequestSite(request).domain + if fileshare: + file_shared_link = '%s://%s%sf/%s/' % (http_or_https, domain, + settings.SITE_ROOT, + fileshare.token) + else: + file_shared_link = '' + return render_to_response('repo_view_file.html', { 'repo': repo, 'path': path, 'obj_id': obj_id, 'file_name': filename, + 'path': path, 'zipped': zipped, 'view_history': view_history, 'current_commit': current_commit, @@ -896,8 +879,76 @@ def repo_view_file(request, repo_id, obj_id): 'can_preview': can_preview, 'filetype': filetype, 'raw_path': raw_path, + 'fileshare': fileshare, + 'protocol': http_or_https, + 'domain': domain, + 'file_shared_link': file_shared_link, }, context_instance=RequestContext(request)) - + +def repo_file_get(request, repo_id): + """ + Handle ajax request to get file content from httpserver. + If get current worktree file, need access_token, path and username from + url params. + If get history file, need access_token, path username and obj_id from + url params. + """ + if not request.is_ajax(): + return Http404 + + http_server_root = get_httpserver_root() + content_type = 'application/json; charset=utf-8' + access_token = request.GET.get('t') + path = request.GET.get('p', '/') + if path[-1] == '/': + path = path[:-1] + + filename = urllib2.quote(os.path.basename(path).encode('utf-8')) + obj_id = request.GET.get('obj_id', '') + if not obj_id: + try: + obj_id = seafserv_rpc.get_file_by_path(repo_id, path) + except: + obj_id = None + if not obj_id: + data = json.dumps([{'error': '获取文件数据失败'}]) + return HttpResponse(data, status=400, content_type=content_type) + + username = request.GET.get('u', '') + tmp_str = '%s/access?repo_id=%s&id=%s&filename=%s&op=%s&t=%s&u=%s' + redirect_url = tmp_str % (http_server_root, + repo_id, obj_id, + filename, 'view', + access_token, + username) + try: + proxied_request = urllib2.urlopen(redirect_url) + if long(proxied_request.headers['Content-Length']) > FILE_PREVIEW_MAX_SIZE: + data = json.dumps([{'error': '文件超过10M,无法在线查看。'}]) + return HttpResponse(data, status=400, content_type=content_type) + else: + content = proxied_request.read() + except urllib2.HTTPError, e: + err = 'HTTPError: 无法在线打开该文件' + data = json.dumps([{'error': err}]) + return HttpResponse(data, status=400, content_type=content_type) + except urllib2.URLError as e: + err = 'URLError: 无法在线打开该文件' + data = json.dumps([{'error': err}]) + return HttpResponse(data, status=400, content_type=content_type) + else: + l, d = [], {} + try: + # XXX: file in windows is encoded in gbk + u_content = content.decode('gbk') + except: + u_content = content.decode('utf-8') + from django.utils.html import escape + d['content'] = re.sub("\r\n|\n", "
", escape(u_content)) + l.append(d) + data = json.dumps(l) + return HttpResponse(data, status=200, content_type=content_type) + def repo_access_file(request, repo_id, obj_id): if repo_id: repo = get_repo(repo_id) @@ -1756,3 +1807,163 @@ def file_revisions(request, repo_id): % (commit_id, file_name, path) return HttpResponseRedirect(url) +@login_required +def get_shared_link(request): + """ + Handle ajax request to generate file shared link. + """ + if not request.is_ajax(): + raise Http404 + + content_type = 'application/json; charset=utf-8' + + repo_id = request.GET.get('repo_id') + obj_id = request.GET.get('obj_id') + path = request.GET.get('p', '/') + if path[-1] == '/': + path = path[:-1] + + l = FileShare.objects.filter(repo_id=repo_id).filter(path=path) + if len(l) > 0: + fileshare = l[0] + token = fileshare.token + else: + token = gen_token(max_length=10) + + fs = FileShare() + fs.username = request.user.username + fs.repo_id = repo_id + fs.path = path + fs.token = token + + try: + fs.save() + except IntegrityError, e: + err = '获取分享链接失败,请重新获取' + data = json.dumps([{'error': err}]) + return HttpResponse(data, status=500, content_type=content_type) + + data = json.dumps([{'token': token}]) + return HttpResponse(data, status=200, content_type=content_type) + +def view_shared_file(request, token): + """ + Preview file via shared link. + """ + assert token is not None # Checked by URLconf + + try: + fileshare = FileShare.objects.get(token=token) + except FileShare.DoesNotExist: + raise Http404 + + username = fileshare.username + repo_id = fileshare.repo_id + path = fileshare.path + + http_server_root = get_httpserver_root() + if path[-1] == '/': + path = path[:-1] + filename = os.path.basename(path) + quote_filename = urllib2.quote(filename.encode('utf-8')) + + try: + obj_id = seafserv_rpc.get_file_by_path(repo_id, path) + except: + obj_id = None + + if not obj_id: + return go_error(request, '文件不存在') + + repo = get_repo(repo_id) + if not repo: + raise Http404 + + access_token = gen_token() + seafserv_rpc.web_save_access_token(access_token, obj_id) + + can_preview, filetype = valid_previewed_file(filename) + + # raw path + tmp_str = '%s/access?repo_id=%s&id=%s&filename=%s&op=%s&t=%s&u=%s' + raw_path = tmp_str % (http_server_root, + repo_id, obj_id, + quote_filename, 'view', + access_token, + username) + + return render_to_response('view_shared_file.html', { + 'repo': repo, + 'obj_id': obj_id, + 'path': path, + 'file_name': filename, + 'token': token, + 'access_token': access_token, + 'can_preview': can_preview, + 'filetype': filetype, + 'raw_path': raw_path, + 'username': username, + }, context_instance=RequestContext(request)) + +@login_required +def remove_shared_link(request): + """ + Handle ajax request to remove file shared link. + """ + if not request.is_ajax(): + raise Http404 + + content_type = 'application/json; charset=utf-8' + + token = request.GET.get('t', '') + FileShare.objects.filter(token=token).delete() + + msg = '删除成功' + data = json.dumps([{'msg': msg}]) + return HttpResponse(data, status=200, content_type=content_type) + +@login_required +def send_shared_link(request): + """ + Handle ajax post request to share file shared link. + """ + if not request.is_ajax() and not request.method == 'POST': + raise Http404 + + content_type = 'application/json; charset=utf-8' + + form = FileLinkShareForm(request.POST) + if not form.is_valid(): + err = '发送失败' + data = json.dumps([{'error':err}]) + return HttpResponse(data, status=400, content_type=content_type) + + email = form.cleaned_data['email'] + file_shared_link = form.cleaned_data['file_shared_link'] + + # Handle the diffent separator + to_email_str = email.replace(';',',') + to_email_str = to_email_str.replace('\n',',') + to_email_str = to_email_str.replace('\r',',') + to_email_list = to_email_str.split(',') + + t = loader.get_template('shared_link_email.html') + for to_email in to_email_list: + c = { + 'email': request.user.username, + 'to_email': to_email, + 'file_shared_link': file_shared_link, + } + + try: + send_mail('您的好友通过SeaCloud分享了一个文件给您', + t.render(Context(c)), None, [to_email], + fail_silently=False) + except: + err = '发送失败' + data = json.dumps([{'error':err}]) + return HttpResponse(data, status=400, content_type=content_type) + + msg = '发送成功。' + data = json.dumps([{'msg': msg}]) + return HttpResponse(data, status=200, content_type=content_type)