diff --git a/frontend/src/components/dialog/generate-upload-link.js b/frontend/src/components/dialog/generate-upload-link.js index 05a24e9294..1983fc2818 100644 --- a/frontend/src/components/dialog/generate-upload-link.js +++ b/frontend/src/components/dialog/generate-upload-link.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import copy from 'copy-to-clipboard'; +import moment from 'moment'; import { Button, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon, Alert } from 'reactstrap'; import { gettext, shareLinkPasswordMinLength, canSendShareLinkEmail } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; @@ -25,7 +26,9 @@ class GenerateUploadLink extends React.Component { password: '', passwdnew: '', sharedUploadInfo: null, - isSendLinkShown: false + isSendLinkShown: false, + isExpireChecked: false, + expireDays: 0, }; } @@ -90,29 +93,61 @@ class GenerateUploadLink extends React.Component { generateUploadLink = () => { let path = this.props.itemPath; let repoID = this.props.repoID; + let { password, expireDays } = this.state; - if (this.state.showPasswordInput && (this.state.password == '')) { - this.setState({ - errorInfo: gettext('Please enter password') - }); - } - else if (this.state.showPasswordInput && (this.state.showPasswordInput && this.state.password.length < shareLinkPasswordMinLength)) { - this.setState({ - errorInfo: gettext('Password is too short') - }); - } - else if (this.state.showPasswordInput && (this.state.password !== this.state.passwordnew)) { - this.setState({ - errorInfo: gettext('Passwords don\'t match') - }); - } else { - seafileAPI.createUploadLink(repoID, path, this.state.password).then((res) => { + let isValid = this.validateParamsInput(); + if (isValid) { + seafileAPI.createUploadLink(repoID, path, password, expireDays).then((res) => { let sharedUploadInfo = new SharedUploadInfo(res.data); this.setState({sharedUploadInfo: sharedUploadInfo}); }); } } + validateParamsInput = () => { + let { showPasswordInput , password, passwordnew, isExpireChecked, expireDays } = this.state; + + // check password params + if (showPasswordInput) { + if (password.length === 0) { + this.setState({errorInfo: gettext('Please enter password')}); + return false; + } + if (password.length < shareLinkPasswordMinLength) { + this.setState({errorInfo: gettext('Password is too short')}); + return false; + } + if (password !== passwordnew) { + this.setState({errorInfo: gettext('Passwords don\'t match')}); + return false; + } + } + + // check expire day params + let reg = /^\d+$/; + if (isExpireChecked) { + if (!expireDays) { + this.setState({errorInfo: gettext('Please enter days')}); + return false; + } + if (!reg.test(expireDays)) { + this.setState({errorInfo: gettext('Please enter a non-negative integer')}); + return false; + } + this.setState({expireDays: parseInt(expireDays)}); + } + return true; + } + + onExpireChecked = (e) => { + this.setState({isExpireChecked: e.target.checked}); + } + + onExpireDaysChanged = (e) => { + let day = e.target.value.trim(); + this.setState({expireDays: day}); + } + onCopyUploadLink = () => { let uploadLink = this.state.sharedUploadInfo.link; copy(uploadLink); @@ -156,6 +191,12 @@ class GenerateUploadLink extends React.Component { + {sharedUploadInfo.expire_date && ( + +
{gettext('Expiration Date:')}
+
{moment(sharedUploadInfo.expire_date).format('YYYY-MM-DD hh:mm:ss')}
+
+ )} {canSendShareLinkEmail && !isSendLinkShown && } {!isSendLinkShown && } @@ -192,6 +233,18 @@ class GenerateUploadLink extends React.Component { } + + + + {this.state.isExpireChecked && + + + + } {this.state.errorInfo && {this.state.errorInfo}} diff --git a/frontend/src/models/shared-upload-info.js b/frontend/src/models/shared-upload-info.js index e9776e62e9..cd7f0f9b8c 100644 --- a/frontend/src/models/shared-upload-info.js +++ b/frontend/src/models/shared-upload-info.js @@ -10,6 +10,8 @@ class SharedUploadInfo { this.ctime = object.ctime; this.token = object.token; this.view_cnt = object.view_cnt; + this.expire_date = object.expire_date; + this.is_expired = object.is_expired; } } diff --git a/frontend/src/pages/share-admin/upload-links.js b/frontend/src/pages/share-admin/upload-links.js index 97d3efb2ae..130d26b7e3 100644 --- a/frontend/src/pages/share-admin/upload-links.js +++ b/frontend/src/pages/share-admin/upload-links.js @@ -1,5 +1,6 @@ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; import { Link } from '@reach/router'; +import moment from 'moment'; import { Modal, ModalHeader, ModalBody } from 'reactstrap'; import { gettext, siteRoot, loginUrl, canGenerateShareLink } from '../../utils/constants'; import { seafileAPI } from '../../utils/seafile-api'; @@ -52,9 +53,10 @@ class Content extends Component { {/*icon*/} - {gettext('Name')} - {gettext('Library')} - {gettext('Visits')} + {gettext('Name')} + {gettext('Library')} + {gettext('Visits')} + {gettext('Expiration')} {/*Operations*/} @@ -114,6 +116,24 @@ class Item extends Component { return { iconUrl, uploadUrl }; } + renderExpriedData = () => { + let item = this.props.item; + if (!item.expire_date) { + return ( + -- + ); + } + let expire_date = moment(item.expire_date).format('YYYY-MM-DD'); + return ( + + {item.is_expired ? + {expire_date} : + expire_date + } + + ); + } + render() { let item = this.props.item; let { iconUrl, uploadUrl } = this.getUploadParams(); @@ -128,6 +148,7 @@ class Item extends Component { {item.obj_name} {item.repo_name} {item.view_cnt} + {this.renderExpriedData()} diff --git a/seahub/api2/endpoints/upload_links.py b/seahub/api2/endpoints/upload_links.py index 03777cc832..290b0ebff7 100644 --- a/seahub/api2/endpoints/upload_links.py +++ b/seahub/api2/endpoints/upload_links.py @@ -2,6 +2,7 @@ import os import logging from constance import config +from dateutil.relativedelta import relativedelta from rest_framework.authentication import SessionAuthentication from rest_framework.permissions import IsAuthenticated @@ -9,6 +10,7 @@ from rest_framework.response import Response from rest_framework.views import APIView from rest_framework import status +from django.utils import timezone from django.utils.translation import ugettext as _ from seaserv import seafile_api @@ -48,6 +50,11 @@ def get_upload_link_info(uls): else: ctime = '' + if uls.expire_date: + expire_date = datetime_to_isoformat_timestr(uls.expire_date) + else: + expire_date = '' + data['repo_id'] = repo_id data['repo_name'] = repo.repo_name if repo else '' data['path'] = path @@ -57,6 +64,8 @@ def get_upload_link_info(uls): data['link'] = gen_shared_upload_link(token) data['token'] = token data['username'] = uls.username + data['expire_date'] = expire_date + data['is_expired'] = uls.is_expired() return data @@ -143,6 +152,12 @@ class UploadLinks(APIView): error_msg = _('Password is too short') return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + try: + expire_days = int(request.data.get('expire_days', 0)) + except ValueError: + error_msg = 'expire_days invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + # resource check repo = seafile_api.get_repo(repo_id) if not repo: @@ -164,11 +179,16 @@ class UploadLinks(APIView): error_msg = 'Permission denied.' return api_error(status.HTTP_403_FORBIDDEN, error_msg) + if expire_days <= 0: + expire_date = None + else: + expire_date = timezone.now() + relativedelta(days=expire_days) + username = request.user.username uls = UploadLinkShare.objects.get_upload_link_by_path(username, repo_id, path) if not uls: uls = UploadLinkShare.objects.create_upload_link_share(username, - repo_id, path, password) + repo_id, path, password, expire_date) link_info = get_upload_link_info(uls) return Response(link_info) diff --git a/seahub/share/models.py b/seahub/share/models.py index 408ab90772..6291cc5683 100644 --- a/seahub/share/models.py +++ b/seahub/share/models.py @@ -451,6 +451,12 @@ class UploadLinkShare(models.Model): def is_owner(self, owner): return owner == self.username + def is_expired(self): + if self.expire_date is not None and timezone.now() > self.expire_date: + return True + else: + return False + class PrivateFileDirShareManager(models.Manager): def add_private_file_share(self, from_user, to_user, repo_id, path, perm): """ diff --git a/tests/api/endpoints/test_upload_links.py b/tests/api/endpoints/test_upload_links.py index 98534e9c59..e2337830f3 100644 --- a/tests/api/endpoints/test_upload_links.py +++ b/tests/api/endpoints/test_upload_links.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- import json from mock import patch +from dateutil.relativedelta import relativedelta +from django.utils import timezone from django.core.urlresolvers import reverse @@ -29,9 +31,9 @@ class UploadLinksTest(BaseTestCase): def tearDown(self): self.remove_repo() - def _add_upload_link(self): + def _add_upload_link(self, expire_date=None): upload_link = UploadLinkShare.objects.create_upload_link_share(self.user_name, - self.repo_id, self.folder_path, None, None) + self.repo_id, self.folder_path, None, expire_date=expire_date) return upload_link.token @@ -50,6 +52,8 @@ class UploadLinksTest(BaseTestCase): assert json_resp[0]['link'] is not None assert json_resp[0]['token'] is not None + assert json_resp[0]['is_expired'] is not None + assert token in json_resp[0]['link'] assert 'u/d' in json_resp[0]['link'] @@ -58,6 +62,20 @@ class UploadLinksTest(BaseTestCase): self._remove_upload_link(token) + def test_get_expired_upload_link(self): + self.login_as(self.user) + # create a upload link expired one day ago. + expire_date = timezone.now() + relativedelta(days=-1) + token = self._add_upload_link(expire_date=expire_date) + + resp = self.client.get(self.url + '?path=' + self.folder_path + '&repo_id=' + self.repo_id) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert json_resp[0]['is_expired'] == True + + self._remove_upload_link(token) + @patch.object(CanGenerateUploadLink, 'has_permission') def test_get_link_with_invalid_user_role_permission(self, mock_has_permission): self.login_as(self.user)