From f94c4c5a611ce36779fc6d0da2658ac5c003a45e Mon Sep 17 00:00:00 2001
From: zhengxie
Date: Thu, 3 Jan 2013 19:28:57 +0800
Subject: [PATCH] Added dir shared link feature
---
media/css/seahub.css | 9 +-
media/img/download-blue.png | Bin 0 -> 3138 bytes
media/img/upload.png | Bin 503 -> 3125 bytes
share/models.py | 3 +-
share/templates/repo/share_admin.html | 13 +-
share/views.py | 53 ++++---
templates/file_view.html | 117 +--------------
templates/repo.html | 62 +++++---
templates/shared_file_view.html | 13 ++
templates/snippets/shared_link_js.html | 117 +++++++++++++++
templates/view_shared_dir.html | 157 ++++++++++++++++++++
thirdpart/seaserv/__init__.py | 2 +-
thirdpart/seaserv/service.py | 7 +
urls.py | 6 +-
utils/__init__.py | 10 ++
views.py | 192 +++++++++++++++++++++----
16 files changed, 577 insertions(+), 184 deletions(-)
create mode 100644 media/img/download-blue.png
create mode 100644 templates/snippets/shared_link_js.html
create mode 100644 templates/view_shared_dir.html
diff --git a/media/css/seahub.css b/media/css/seahub.css
index b0ef7f9fcb..8408fe5354 100644
--- a/media/css/seahub.css
+++ b/media/css/seahub.css
@@ -896,10 +896,14 @@ textarea:-moz-placeholder {/* for FF */
.repo-op .op-btn {
*margin-left:5px;/* for ie 7*/
}
-#upload-file {
+#upload-file{
padding-left:19px;
background-image:url('../img/upload.png?v=1');
}
+#download-dir {
+ padding-left:19px;
+ background-image:url('../img/download-blue.png?t=1352500800');
+}
#add-new-dir {
padding-left:23px;
background-image:url('../img/folder-add.png');
@@ -1364,7 +1368,8 @@ textarea:-moz-placeholder {/* for FF */
color:#444;
text-align:right;
}
-#shared-link {
+#shared-link,
+#shared-link-text {
border:0;
}
.file-op {
diff --git a/media/img/download-blue.png b/media/img/download-blue.png
new file mode 100644
index 0000000000000000000000000000000000000000..f24f147c4aa2f2a3fa4a50ef2a28512ad89521ee
GIT binary patch
literal 3138
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
literal 0
HcmV?d00001
diff --git a/media/img/upload.png b/media/img/upload.png
index 54f837c4b42fcbb38f7e6e51b29af752965a878c..78512522dbca6853f27509dc1ffdbbdf49f28d26 100644
GIT binary patch
delta 3100
zcmV+%4CC|n1GN~CB!2{FK}|sb0I`n?{9y$E0182ALqkwWLqi}?a&Km7Y-IodNDZBq
zcT`i^7KhKhH@(mjA|NI78hQyJ(mO~M1W}1efKUR4geG=G1x6GRDOO}uzyU{xB4b4q
z3xk4U*9r0vP{zSgL`CJ@jB5$+tu^!Bn*GOF-`VH4*V$+9eSg=w08spb1j2M!0f022
zSQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*TfMmPdEWc1DbJqWV
zks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0#IJ1jr{*iW$(WZW
z6^O6dCS`@&W(|l*Bo_at@36IcS~$c&2CMiUf&CVx|$-m}%wc$Y%QSvs3wEF)NhA
zP2q@mOwaVR4301tfUj?sMFAyQZJB71?X4W`?OE35Xn*;?+20<1Q~f>2diz!JChX6i
zL5!YR-)!uxFTEB3_9R-H9kafbRRFYY20-QEtWWPC0P+O@wBDTa9)@gQlEmT+Cku=0
z>}+!ZpKC5F=r{YHf^W*dhB^0{%kKMDJEj*ufg?#3Gi62Prl+P$M9j<#4wuhl{r3?6
z
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
- {% trans "File"%} |
+ {% trans "File or Folder"%} |
{% trans "Library"%} |
{% trans "Visits"%} |
{% trans "Operations"%} |
{% for fs in fileshares %}
- {{ fs.filename }} |
+ {% if fs.s_type == 'f' %}
+ {{ fs.filename }} |
+ {% else %}
+ {{ fs.filename }} |
+ {% endif %}
{{ fs.repo.name }} |
{{ fs.view_cnt }} |
- {% trans "View" %}
+ {% trans "View" %}
{% trans "Remove"%}
|
@@ -173,8 +177,7 @@ $('.cancel-share').click(function() {
});
$(".view-link").click(function() {
- var token = $(this).attr('data'),
- link = '{{ protocol }}://' + '{{ domain }}{{ SITE_ROOT }}f/' + token + '/';
+ var link = $(this).attr('data');
$('#link').before('' + link + '
');
$('#shared-link').val(link).css('width', $('#link').prev().width() + 2);
$("#link").modal({appendTo:'#main'});
diff --git a/share/views.py b/share/views.py
index f7f52f3a3c..64b0e38452 100644
--- a/share/views.py
+++ b/share/views.py
@@ -3,7 +3,8 @@ import os
import simplejson as json
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
-from django.http import HttpResponse, HttpResponseRedirect, Http404
+from django.http import HttpResponse, HttpResponseRedirect, Http404, \
+ HttpResponseBadRequest
from django.shortcuts import render_to_response
from django.template import Context, loader, RequestContext
from django.utils.translation import ugettext as _
@@ -25,12 +26,13 @@ from seahub.contacts.signals import mail_sended
from seahub.share.models import FileShare
from seahub.views import validate_owner, is_registered_user
from seahub.utils import render_permission_error, string2list, render_error, \
- gen_token
+ gen_token, gen_shared_link
try:
from seahub.settings import CLOUD_MODE
except ImportError:
CLOUD_MODE = False
+from seahub.settings import SITE_ROOT
@login_required
def share_repo(request):
@@ -176,7 +178,6 @@ def repo_remove_share(request):
next = request.META.get('HTTP_REFERER', None)
if not next:
- from seahub.settings import SITE_ROOT
next = SITE_ROOT
return HttpResponseRedirect(next)
@@ -233,13 +234,17 @@ def share_admin(request):
# link.repo_name = repo.name
# link.remain_time = anon_share_token_generator.get_remain_time(link.token)
- # File shared links
+ # Shared links
fileshares = FileShare.objects.filter(username=username)
p_fileshares = [] # personal file share
for fs in fileshares:
- if is_personal_repo(fs.repo_id):
- # only list files in personal repos
- fs.filename = os.path.basename(fs.path)
+ if is_personal_repo(fs.repo_id): # only list files in personal repos
+ if fs.s_type == 'f':
+ fs.filename = os.path.basename(fs.path)
+ fs.shared_link = gen_shared_link(request, fs.token, 'f')
+ else:
+ fs.filename = os.path.basename(fs.path[:-1])
+ fs.shared_link = gen_shared_link(request, fs.token, 'd')
fs.repo = get_repo(fs.repo_id)
p_fileshares.append(fs)
@@ -248,8 +253,6 @@ def share_admin(request):
"shared_repos": shared_repos,
# "out_links": out_links,
"fileshares": p_fileshares,
- "protocol": request.is_secure() and 'https' or 'http',
- "domain": RequestSite(request).domain,
}, context_instance=RequestContext(request))
@login_required
@@ -371,17 +374,32 @@ def remove_anonymous_share(request, token):
@login_required
def get_shared_link(request):
"""
- Handle ajax request to generate file shared link.
+ Handle ajax request to generate file or dir shared link.
"""
if not request.is_ajax():
raise Http404
content_type = 'application/json; charset=utf-8'
- repo_id = request.GET.get('repo_id')
- path = request.GET.get('p', '/')
- if path[-1] == '/':
- path = path[:-1]
+ repo_id = request.GET.get('repo_id', '')
+ share_type = request.GET.get('type', 'f') # `f` or `d`
+ path = request.GET.get('p', '')
+ if not (repo_id and path):
+ err = _('Invalid arguments')
+ data = json.dumps([{'error': err}])
+ return HttpResponse(data, status=400, content_type=content_type)
+
+ if share_type == 'f':
+ if path[-1] == '/': # cut out last '/' at end of path
+ path = path[:-1]
+ else:
+ if path == '/': # can not share root dir
+ err = _('Can not share root dir.')
+ data = json.dumps([{'error': err}])
+ return HttpResponse(data, status=400, content_type=content_type)
+ else:
+ if path[-1] != '/': # append '/' at end of path
+ path += '/'
l = FileShare.objects.filter(repo_id=repo_id).filter(
username=request.user.username).filter(path=path)
@@ -396,6 +414,7 @@ def get_shared_link(request):
fs.repo_id = repo_id
fs.path = path
fs.token = token
+ fs.s_type = 'f' if share_type == 'f' else 'd'
try:
fs.save()
@@ -403,8 +422,10 @@ def get_shared_link(request):
err = _('Failed to get the link, please retry it.')
data = json.dumps([{'error': err}])
return HttpResponse(data, status=500, content_type=content_type)
-
- data = json.dumps([{'token': token}])
+
+ shared_link = gen_shared_link(request, token, fs.s_type)
+
+ data = json.dumps([{'token': token, 'shared_link': shared_link}])
return HttpResponse(data, status=200, content_type=content_type)
@login_required
diff --git a/templates/file_view.html b/templates/file_view.html
index d2f570e441..1584866819 100644
--- a/templates/file_view.html
+++ b/templates/file_view.html
@@ -149,122 +149,9 @@ $('#view-original, #download').click(function() {
$('#edit').click(function() {
location.href = $(this).attr('data');
});
-function showLink() {
- $('#get-shared-link').addClass('hide');
- $('#shared-link, #send-shared-link, #rm-shared-link').removeClass('hide');
-}
-function hideLink() {
- $('#shared-link, #send-shared-link, #rm-shared-link').addClass('hide');
- $('#get-shared-link').removeClass('hide');
-}
-function setLinkWidth() {
- var link = $('#shared-link');
- link.before('' + link.val() + '
');
- link.css('width', link.prev().width() + 2);
- link.prev().remove();
-}
-if ($.trim($('#shared-link').val())) {
- setLinkWidth();
-}
-{% if fileshare.token %}
-showLink();
-{% else %}
-hideLink();
-{% endif %}
-
-$('#get-shared-link').click(function() {
- var url = $(this).attr('data');
- $.ajax({
- url: url,
- dataType: 'json',
- cache: false,
- contentType: 'application/json; charset=utf-8',
- success: function(data) {
- if (data.length > 0) {
- var t = data[0]['token'];
- $('#rm-shared-link').attr('data', '{% url 'remove_shared_link' %}?t=' + t);
- $('#shared-link, input[name="file_shared_link"]').val('{{ protocol }}://{{ domain }}{{ SITE_ROOT }}f/' + t + '/');
- setLinkWidth();
- showLink();
- }
- },
- error: function(xhr, ajaxOptions, thrownError) {
- var jsonVal = jQuery.parseJSON(xhr.responseText);
- feedback(jsonVal[0]['error'], 'error');
- }
- });
-});
-
-$('#rm-shared-link').click(function() {
- var url = $(this).attr('data');
- $.ajax({
- url: url,
- dataType: 'json',
- cache: false,
- contentType: 'application/json; charset=utf-8',
- success: function(data) {
- hideLink();
- $('#shared-link').val('');
- }
- });
-});
-
-var share_list = [];
-{% for contact in contacts %}
-share_list.push({value:'{{ contact.contact_email }}', label:'{{ contact.contact_email }}'});
-{% endfor %}
-$('#send-shared-link').click(function() {
- $("#link-send-form").modal({appendTo: "#main", focus: false});
- $('#simplemodal-container').css('height', 'auto');
- addAutocomplete('#link-send-input', '#link-send-form', share_list);
-});
-
-$("#link-send-form").submit(function(event) {
- var form = $(this),
- file_shared_link = form.children('input[name="file_shared_link"]').val(),
- email = $.trim(form.children('textarea[name="email"]').val()),
- submit_btn = form.children('input[type="submit"]');
-
- if (!email) {
- apply_form_error('link-send-form', '{% trans "Please input at least an email." %}');
- return false;
- }
-
- disable(submit_btn);
- $('#link-send-form .error').addClass('hide');
- $('#sending').removeClass('hide');
-
- $.ajax({
- type: "POST",
- url: "{% url 'send_shared_link' %}",
- dataType: 'json',
- cache: false,
- contentType: 'application/json; charset=utf-8',
- beforeSend: prepareCSRFToken,
- data: {file_shared_link: file_shared_link, email: email},
- success: function(data) {
- $.modal.close();
- feedback('{% trans "Successfully sent." %}', "success");
- },
- error: function(data, textStatus, jqXHR) {
- $('#sending').addClass('hide');
- enable(submit_btn);
- var errors = $.parseJSON(data.responseText);
- $.each(errors, function(index, value) {
- if (index == 'error') {
- apply_form_error('link-send-form', value);
- } else {
- apply_form_error('link-send-form', value[0]);
- }
- });
- }
- });
- return false;
-});
-$('#shared-link').click(function() {
- $(this).select();
-});
+//share link
+{% include "snippets/shared_link_js.html" %}
//star
$('#star').click(function() {
diff --git a/templates/repo.html b/templates/repo.html
index 59f4a197b2..871d57c137 100644
--- a/templates/repo.html
+++ b/templates/repo.html
@@ -17,10 +17,18 @@
{% if user_perm == 'r' %}
@@ -80,13 +88,16 @@
{% endif %}
{% endfor %}
- {% if user_perm == 'rw' %}
+ {% if user_perm and path != '/' %}
+
+ {% endif %}
+ {% if user_perm == 'rw' %}
+ {% endif %}
- {% endif %}
@@ -111,7 +122,8 @@
{% if user_perm %}
{% if user_perm == 'rw' %}

@@ -283,15 +295,16 @@
@@ -587,7 +600,7 @@ $('#add-new-file-form').submit(function() {
$('#add-new-dir-form').submit(function() {
var new_dir = $(this).find('input[name="new_dir_name"]').val();
if (!$.trim(new_dir)) {
- apply_form_error('add-new-dir-form', "{% trans "Directory name can't be empty" %}");
+ apply_form_error('add-new-dir-form', "{% trans "Directory name can not be empty" %}");
return false;
}
@@ -740,24 +753,31 @@ $('.file-share').click(function() {
var filename = $(this).data('name');
function showPopup(link) {
$('#file-share .op-target').html(trimFilename(filename, 30));
- $('#shared-link, #link-send-form input[name="file_shared_link"]').val(link);
+ $('#shared-link-text, #link-send-form input[name="file_shared_link"]').val(link);
$('#main').append('
' + link + '
');
- $('#shared-link').css({'width':$('#linkwidth').width() + 2});
- $('#file-share').modal({'focus':false}); // in ff: if 'focus' is true, 'shared-link' gets the focus
+ $('#shared-link-text').css({'width':$('#linkwidth').width() + 2});
+ $('#file-share').modal({'focus':false}); // in ff: if 'focus' is true, 'shared-link-text' gets the focus
$('#linkwidth').remove();
$('#simplemodal-container').css({'width':'auto', 'height':'auto'});
}
if ($(this).data('link')) {
showPopup($(this).data('link'));
} else {
+ var aj_url = '';
+ if ($(this).data('type') == 'd') {
+ aj_url = '{% url 'get_shared_link' %}?repo_id={{ repo.id }}&p={{ path|urlencode }}' + e(filename) + '&type=d';
+ } else {
+ aj_url = '{% url 'get_shared_link' %}?repo_id={{ repo.id }}&p={{ path|urlencode }}' + e(filename);
+ }
+
$.ajax({
- url: '{% url 'get_shared_link' %}?repo_id={{ repo.id }}&p={{ path|urlencode }}' + e(filename),
+ url: aj_url,
dataType: 'json',
cache: false,
contentType: 'application/json; charset=utf-8',
success: function(data) {
if (data.length > 0) {
- showPopup('{{ protocol }}://{{ domain }}{{ SITE_ROOT }}f/' + data[0]['token'] + '/');
+ showPopup(data[0]['shared_link']);
}
},
error: function(xhr, ajaxOptions, thrownError) {
@@ -824,9 +844,15 @@ $("#link-send-form").submit(function() {
});
return false;
});
-$('#shared-link').click(function() { $(this).select(); });
+$('#shared-link-text').click(function() { $(this).select(); });
+
+$('#download-dir').click(function() {
+ location.href = $(this).attr('data');
+});
+{% include "snippets/shared_link_js.html" %}
{% include "snippets/list_commit_detail.html" %}
{% include "snippets/bottom_bar.html" %}
+
{% include 'snippets/file_upload_progress_js.html' %}
{% endblock %}
diff --git a/templates/shared_file_view.html b/templates/shared_file_view.html
index 08087aec8c..85a87d8c9b 100644
--- a/templates/shared_file_view.html
+++ b/templates/shared_file_view.html
@@ -13,7 +13,20 @@
{{ file_name }}
+ {% if zipped %}
+
+ {% trans "Current path: "%}
+ {% for name, link in zipped %}
+ {% if not forloop.last %}
+ {{ name }} /
+ {% else %}
+ {{ name }}
+ {% endif %}
+ {% endfor %}
+
+ {% else %}
{% trans "Shared by: " %}{{ username|email2nickname }}
+ {% endif %}
{% if filetype == 'Text' or filetype == 'Image' or filetype == 'SVG' or filetype == 'Markdown' %}
diff --git a/templates/snippets/shared_link_js.html b/templates/snippets/shared_link_js.html
new file mode 100644
index 0000000000..46d81b914d
--- /dev/null
+++ b/templates/snippets/shared_link_js.html
@@ -0,0 +1,117 @@
+{% load i18n %}
+{% load url from future %}
+function showLink() {
+ $('#get-shared-link').addClass('hide');
+ $('#shared-link, #send-shared-link, #rm-shared-link').removeClass('hide');
+}
+function hideLink() {
+ $('#shared-link, #send-shared-link, #rm-shared-link').addClass('hide');
+ $('#get-shared-link').removeClass('hide');
+}
+function setLinkWidth() {
+ var link = $('#shared-link');
+ link.before('
' + link.val() + '
');
+ link.css('width', link.prev().width() + 2);
+ link.prev().remove();
+}
+if ($.trim($('#shared-link').val())) {
+ setLinkWidth();
+}
+{% if fileshare.token %}
+showLink();
+{% else %}
+hideLink();
+{% endif %}
+
+$('#get-shared-link').click(function() {
+ var url = $(this).attr('data');
+ $.ajax({
+ url: url,
+ dataType: 'json',
+ cache: false,
+ contentType: 'application/json; charset=utf-8',
+ success: function(data) {
+ if (data.length > 0) {
+ var t = data[0]['token'];
+ $('#rm-shared-link').attr('data', '{% url 'remove_shared_link' %}?t=' + t);
+ $('#shared-link, input[name="file_shared_link"]').val(data[0]['shared_link']);
+ setLinkWidth();
+ showLink();
+ }
+ },
+ error: function(xhr, ajaxOptions, thrownError) {
+ var jsonVal = jQuery.parseJSON(xhr.responseText);
+ feedback(jsonVal[0]['error'], 'error');
+ }
+ });
+});
+
+$('#rm-shared-link').click(function() {
+ var url = $(this).attr('data');
+ $.ajax({
+ url: url,
+ dataType: 'json',
+ cache: false,
+ contentType: 'application/json; charset=utf-8',
+ success: function(data) {
+ hideLink();
+ $('#shared-link').val('');
+ }
+ });
+});
+
+var share_list = [];
+{% for contact in contacts %}
+share_list.push({value:'{{ contact.contact_email }}', label:'{{ contact.contact_email }}'});
+{% endfor %}
+$('#send-shared-link').click(function() {
+ $("#link-send-form").modal({appendTo: "#main", focus: false});
+ $('#simplemodal-container').css('height', 'auto');
+ addAutocomplete('#link-send-input', '#link-send-form', share_list);
+});
+
+$("#link-send-form").submit(function(event) {
+ var form = $(this),
+ file_shared_link = form.children('input[name="file_shared_link"]').val(),
+ email = $.trim(form.children('textarea[name="email"]').val()),
+ submit_btn = form.children('input[type="submit"]');
+
+ if (!email) {
+ apply_form_error('link-send-form', '{% trans "Please input at least an email." %}');
+ return false;
+ }
+
+ disable(submit_btn);
+ $('#link-send-form .error').addClass('hide');
+ $('#sending').removeClass('hide');
+
+ $.ajax({
+ type: "POST",
+ url: "{% url 'send_shared_link' %}",
+ dataType: 'json',
+ cache: false,
+ contentType: 'application/json; charset=utf-8',
+ beforeSend: prepareCSRFToken,
+ data: {file_shared_link: file_shared_link, email: email},
+ success: function(data) {
+ $.modal.close();
+ feedback('{% trans "Successfully sent." %}', "success");
+ },
+ error: function(data, textStatus, jqXHR) {
+ $('#sending').addClass('hide');
+ enable(submit_btn);
+ var errors = $.parseJSON(data.responseText);
+ $.each(errors, function(index, value) {
+ if (index == 'error') {
+ apply_form_error('link-send-form', value);
+ } else {
+ apply_form_error('link-send-form', value[0]);
+ }
+ });
+ }
+ });
+ return false;
+});
+$('#shared-link').click(function() {
+ $(this).select();
+});
diff --git a/templates/view_shared_dir.html b/templates/view_shared_dir.html
new file mode 100644
index 0000000000..9d1ffebfbf
--- /dev/null
+++ b/templates/view_shared_dir.html
@@ -0,0 +1,157 @@
+{% extends base_template %}
+
+{% load seahub_tags i18n %}
+{% load url from future %}
+
+{% block info_bar_message %}
+{% endblock %}
+
+{% block main_panel %}
+
{{ dir_name }}
+
+
{% trans "Shared by: " %}{{ username|email2nickname }}
+
+
+
+
+
+
+ {% trans "Current path: "%}
+ {% for name, link in zipped %}
+ {% if not forloop.last %}
+ {{ name }} /
+ {% else %}
+ {{ name }}
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+ |
+ |
+ {% trans "Name"%} |
+ {% trans "Size"%} |
+ {% trans "Operations"%} |
+
+
+ {% for dirent in dir_list %}
+
+ |
+  |
+
+ {{ dirent.obj_name }}
+ |
+
+ |
+
+
+ |
+
+ {% endfor %}
+
+ {% for dirent in file_list %}
+
+ |
+  |
+
+ {{ dirent.props.obj_name }}
+ |
+
+ {{ dirent.file_size|filesizeformat }} |
+
+
+ |
+
+ {% endfor %}
+
+
+
+
+{% endblock %}
+
+{% block extra_script %}
+
+{% endblock %}
diff --git a/thirdpart/seaserv/__init__.py b/thirdpart/seaserv/__init__.py
index 9fba98ccb0..4b84e1c1ba 100644
--- a/thirdpart/seaserv/__init__.py
+++ b/thirdpart/seaserv/__init__.py
@@ -16,7 +16,7 @@ from service import get_repos, get_repo, get_commits, get_branches, remove_repo,
list_personal_shared_repos, is_personal_repo, list_inner_pub_repos, \
is_org_repo_owner, get_org_repo_owner, is_org_repo, get_file_size,\
list_personal_repos_by_owner, get_repo_token_nonnull, get_repo_owner, \
- server_repo_size
+ server_repo_size, get_file_id_by_path
from service import get_binding_peerids, is_valid_filename, check_permission,\
is_passwd_set
diff --git a/thirdpart/seaserv/service.py b/thirdpart/seaserv/service.py
index 431f58aebd..b0ac17be60 100644
--- a/thirdpart/seaserv/service.py
+++ b/thirdpart/seaserv/service.py
@@ -669,6 +669,13 @@ def get_file_size(file_id):
fs = 0
return fs
+def get_file_id_by_path(repo_id, path):
+ try:
+ ret = seafserv_threaded_rpc.get_file_id_by_path(repo_id, path)
+ except SearpcError, e:
+ ret = ''
+ return ret
+
def get_related_users_by_repo(repo_id):
"""Give a repo id, returns a list of users of:
- the repo owner
diff --git a/urls.py b/urls.py
index 344d806c45..167209af4d 100644
--- a/urls.py
+++ b/urls.py
@@ -57,7 +57,9 @@ urlpatterns = patterns('',
(r'^repo/(?P
[-0-9a-f]{36})/file/edit/$', file_edit),
url(r'^repo/(?P[-0-9a-f]{36})/(?P[^/]+)/$', repo_access_file, name='repo_access_file'),
- url(r'^f/(?P[^/]+)/$', view_shared_file, name='view_shared_file'),
+ url(r'^f/(?P[a-f0-9]{10})/$', view_shared_file, name='view_shared_file'),
+ url(r'^d/(?P[a-f0-9]{10})/$', view_shared_dir, name='view_shared_dir'),
+ url(r'^d/(?P[a-f0-9]{10})/files/$', view_file_via_shared_dir, name='view_file_via_shared_dir'),
(r'^file_upload_progress_page/$', file_upload_progress_page),
(r'^publicrepo/create/$', public_repo_create),
(r'^events/$', events),
@@ -77,7 +79,7 @@ urlpatterns = patterns('',
url(r'^useradmin/password/reset/(?P[^/]+)/$', user_reset, name='user_reset'),
### Apps ###
- (r'^api/', include('api.urls')),
+# (r'^api/', include('api.urls')),
(r'^api2/', include('api2.urls')),
(r'^avatar/', include('avatar.urls')),
(r'^notification/', include('notifications.urls')),
diff --git a/utils/__init__.py b/utils/__init__.py
index e3f1b81ea6..eddd2a1cbc 100644
--- a/utils/__init__.py
+++ b/utils/__init__.py
@@ -6,6 +6,7 @@ import random
import stat
import urllib2
+from django.contrib.sites.models import RequestSite
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.utils.hashcompat import sha_constructor
@@ -637,3 +638,12 @@ def get_dir_starred_files(email, repo_id, parent_dir, org_id=-1):
path__startswith=parent_dir,
org_id=org_id)
return [ f.path for f in starred_files ]
+
+def gen_shared_link(request, token, s_type):
+ http_or_https = request.is_secure() and 'https' or 'http'
+ domain = RequestSite(request).domain
+
+ if s_type == 'f':
+ return '%s://%s%sf/%s/' % (http_or_https, domain, settings.SITE_ROOT, token)
+ else:
+ return '%s://%s%sd/%s/' % (http_or_https, domain, settings.SITE_ROOT, token)
diff --git a/views.py b/views.py
index 53de75cee6..b93f6bd20f 100644
--- a/views.py
+++ b/views.py
@@ -44,7 +44,7 @@ from seaserv import ccnet_rpc, ccnet_threaded_rpc, get_repos, get_emailusers, \
list_inner_pub_repos, get_org_groups_by_repo, is_org_repo_owner, \
get_org_repo_owner, is_passwd_set, get_file_size, check_quota, \
get_related_users_by_repo, get_related_users_by_org_repo, HtmlDiff, \
- get_session_info, get_group_repoids, get_repo_owner
+ get_session_info, get_group_repoids, get_repo_owner, get_file_id_by_path
from pysearpc import SearpcError
from signals import repo_created, repo_deleted
@@ -64,7 +64,7 @@ from forms import AddUserForm, RepoCreateForm, RepoNewDirForm, RepoNewFileForm,\
FileCommentForm, RepoRenameFileForm, RepoPassowrdForm, SharedRepoCreateForm,\
SetUserQuotaForm
from utils import render_permission_error, render_error, list_to_string, \
- get_httpserver_root, get_ccnetapplet_root, \
+ get_httpserver_root, get_ccnetapplet_root, gen_shared_link, \
calculate_repo_last_modify, valid_previewed_file, \
check_filename_with_rename, get_accessible_repos, EMPTY_SHA1, \
get_file_revision_id_size, get_ccnet_server_addr_port, \
@@ -164,7 +164,7 @@ def get_repo_dirents(request, repo_id, commit, path):
# return render_error(self.request, e.msg)
org_id = -1
- if request.user.org:
+ if hasattr(request.user, 'org') and request.user.org:
org_id = request.user.org['org_id']
starred_files = get_dir_starred_files(request.user.username, repo_id, path, org_id)
@@ -173,19 +173,28 @@ def get_repo_dirents(request, repo_id, commit, path):
domain = RequestSite(request).domain
for dirent in dirs:
+ dirent.sharelink = ''
+
if stat.S_ISDIR(dirent.props.mode):
+ dpath = os.path.join(path, dirent.obj_name)
+ if dpath[-1] != '/':
+ dpath += '/'
+ for share in fileshares:
+ if dpath == share.path:
+ dirent.sharelink = gen_shared_link(request, share.token, 'd')
+ dirent.sharetoken = share.token
+ break
dir_list.append(dirent)
else:
file_list.append(dirent)
dirent.file_size = get_file_size(dirent.obj_id)
dirent.starred = False
- dirent.sharelink = ''
fpath = os.path.join(path, dirent.obj_name)
if fpath in starred_files:
dirent.starred = True
for share in fileshares:
if fpath == share.path:
- dirent.sharelink = '%s://%s%sf/%s/' % (http_or_https, domain, settings.SITE_ROOT, share.token)
+ dirent.sharelink = gen_shared_link(request, share.token, 'f')
dirent.sharetoken = share.token
break
@@ -342,6 +351,26 @@ class RepoView(LoginRequiredMixin, CtxSwitchRequiredMixin, RepoMixin,
return gen_file_upload_url(token, 'update')
else:
return ''
+
+ def get_fileshare(self, repo_id, user, path):
+ if path == '/': # no shared link for root dir
+ return None
+
+ l = FileShare.objects.filter(repo_id=repo_id).filter(\
+ username=user).filter(path=path)
+ fileshare = l[0] if len(l) > 0 else None
+ return fileshare
+
+ def get_shared_link(self, fileshare):
+ # dir shared link
+ http_or_https = self.request.is_secure() and 'https' or 'http'
+ domain = RequestSite(self.request).domain
+
+ if fileshare:
+ dir_shared_link = gen_shared_link(self.request, fileshare.token, 'd')
+ else:
+ dir_shared_link = ''
+ return dir_shared_link
def get_context_data(self, **kwargs):
kwargs['repo'] = self.repo
@@ -375,6 +404,9 @@ class RepoView(LoginRequiredMixin, CtxSwitchRequiredMixin, RepoMixin,
kwargs['protocol'] = self.protocol
kwargs['domain'] = self.domain
kwargs['contacts'] = self.contacts
+ kwargs['fileshare'] = self.get_fileshare(\
+ self.repo_id, self.request.user.username, self.path)
+ kwargs['dir_shared_link'] = self.get_shared_link(kwargs['fileshare'])
return kwargs
@@ -1293,13 +1325,12 @@ def repo_view_file(request, repo_id):
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)
+ file_shared_link = gen_shared_link(request, fileshare.token, 'f')
else:
file_shared_link = ''
# my constacts
contacts = Contact.objects.filter(user_email=request.user.username)
-
# Get groups this repo is shared.
if request.user.org:
@@ -2412,7 +2443,7 @@ def view_shared_file(request, token):
path = fileshare.path
http_server_root = get_httpserver_root()
- if path[-1] == '/':
+ if path[-1] == '/': # Normalize file path
path = path[:-1]
filename = os.path.basename(path)
quote_filename = urllib2.quote(filename.encode('utf-8'))
@@ -2468,7 +2499,113 @@ def view_shared_file(request, token):
'DOCUMENT_CONVERTOR_ROOT': DOCUMENT_CONVERTOR_ROOT,
}, context_instance=RequestContext(request))
+def view_shared_dir(request, token):
+ 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 = request.GET.get('p', '')
+ path = fileshare.path if not path else path
+ if path[-1] != '/': # Normalize dir path
+ path += '/'
+
+ if not path.startswith(fileshare.path):
+ path = fileshare.path # Can not view upper dir of shared dir
+
+ repo = get_repo(repo_id)
+ if not repo:
+ raise Http404
+
+ dir_name = os.path.basename(path[:-1])
+ current_commit = get_commits(repo_id, 0, 1)[0]
+ file_list, dir_list = get_repo_dirents(request, repo_id, current_commit,
+ path)
+ zipped = gen_path_link(path, '')
+
+ if path == fileshare.path: # When user view the shared dir..
+ # increase shared link view_cnt,
+ fileshare = FileShare.objects.get(token=token)
+ fileshare.view_cnt = F('view_cnt') + 1
+ fileshare.save()
+
+ return render_to_response('view_shared_dir.html', {
+ 'repo': repo,
+ 'token': token,
+ 'path': path,
+ 'username': username,
+ 'dir_name': dir_name,
+ 'file_list': file_list,
+ 'dir_list': dir_list,
+ 'zipped': zipped,
+ }, context_instance=RequestContext(request))
+
+def view_file_via_shared_dir(request, token):
+ 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 = request.GET.get('p', '')
+ if not path:
+ raise Http404
+
+ if not path.startswith(fileshare.path): # Can not view upper dir of shared dir
+ raise Http404
+
+ repo = get_repo(repo_id)
+ if not repo:
+ raise Http404
+
+ file_name = os.path.basename(path)
+ quote_filename = urllib2.quote(file_name.encode('utf-8'))
+ file_id = get_file_id_by_path(repo_id, path)
+ if not file_id:
+ return render_error(request, _(u'File not exists'))
+
+ access_token = seafserv_rpc.web_get_access_token(repo.id, file_id,
+ 'view', '')
+ filetype, fileext = valid_previewed_file(file_name)
+ # Raw path
+ raw_path = gen_file_get_url(access_token, quote_filename)
+ # get file content
+ err = ''
+ file_content = ''
+ swf_exists = False
+ if filetype == 'Text' or filetype == 'Markdown' or filetype == 'Sf':
+ err, file_content, encoding = repo_file_get(raw_path)
+ elif filetype == 'Document' or filetype == 'PDF':
+ err, swf_exists = flash_prepare(raw_path, obj_id, fileext)
+
+ zipped = gen_path_link(path, '')
+
+ return render_to_response('shared_file_view.html', {
+ 'repo': repo,
+ 'obj_id': file_id,
+ 'path': path,
+ 'file_name': file_name,
+ 'shared_token': token,
+ 'access_token': access_token,
+ 'filetype': filetype,
+ 'fileext': fileext,
+ 'raw_path': raw_path,
+ 'username': username,
+ 'err': err,
+ 'file_content': file_content,
+ 'swf_exists': swf_exists,
+ 'DOCUMENT_CONVERTOR_ROOT': DOCUMENT_CONVERTOR_ROOT,
+ 'zipped': zipped,
+ 'token': token,
+ }, context_instance=RequestContext(request))
+
def flash_prepare(raw_path, obj_id, doctype):
curl = DOCUMENT_CONVERTOR_ROOT + 'convert'
data = {'doctype': doctype,
@@ -2649,22 +2786,34 @@ def repo_star_file(request, repo_id):
unstar_file(request.user.username, repo_id, path)
return HttpResponse(json.dumps({'success':True}), content_type=content_type)
-@login_required
def repo_download_dir(request, repo_id):
repo = get_repo(repo_id)
if not repo:
return render_error(request, _(u'Library not exists'))
- try:
- parent_dir = request.GET['parent']
- dirname = request.GET['dirname']
- except KeyError:
- return render_error(request, _(u'Invalid arguments'))
+ path = request.GET.get('p', '/')
+ if path[-1] != '/': # Normalize dir path
+ path += '/'
- path = os.path.join(parent_dir, dirname.rstrip('/'))
+ if len(path) > 1:
+ dirname = os.path.basename(path.rstrip('/')) # Here use `rstrip` to cut out last '/' in path
+ else:
+ dirname = repo.name
+
+ allow_download = False
+ fileshare_token = request.GET.get('t', '')
+ if fileshare_token: # download dir from dir shared link
+ try:
+ fileshare = FileShare.objects.get(token=fileshare_token)
+ except FileShare.DoesNotExist:
+ raise Http404
- permission = get_user_permission(request, repo_id)
- if permission:
+ # Can not download upper dir of shared dir.
+ allow_download = True if path.startswith(fileshare.path) else False
+ else:
+ allow_download = True if get_user_permission(request, repo_id) else False
+
+ if allow_download:
dir_id = seafserv_threaded_rpc.get_dirid_by_path (repo.head_cmmt_id,
path.encode('utf-8'))
token = seafserv_rpc.web_get_access_token(repo_id,
@@ -2672,14 +2821,9 @@ def repo_download_dir(request, repo_id):
'download-dir',
request.user.username)
else:
- return render_permission_error(request, _(u'Unable to access file'))
-
- if len(path) > 1:
- filename = os.path.basename(path)
- else:
- filename = repo.name
- url = gen_file_get_url(token, filename)
+ return render_error(request, _(u'Unable to download "%s"') % dirname )
+ url = gen_file_get_url(token, dirname)
return redirect(url)
def events(request):