mirror of
https://github.com/haiwen/seahub.git
synced 2025-07-13 15:05:30 +00:00
Merge pull request #3782 from haiwen/7.0-upload-links-expire-days
add expire days to upload link
This commit is contained in:
commit
b64c38bad7
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
|
import moment from 'moment';
|
||||||
import { Button, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon, Alert } from 'reactstrap';
|
import { Button, Form, FormGroup, Label, Input, InputGroup, InputGroupAddon, Alert } from 'reactstrap';
|
||||||
import { gettext, shareLinkPasswordMinLength, canSendShareLinkEmail } from '../../utils/constants';
|
import { gettext, shareLinkPasswordMinLength, canSendShareLinkEmail } from '../../utils/constants';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
@ -25,7 +26,9 @@ class GenerateUploadLink extends React.Component {
|
|||||||
password: '',
|
password: '',
|
||||||
passwdnew: '',
|
passwdnew: '',
|
||||||
sharedUploadInfo: null,
|
sharedUploadInfo: null,
|
||||||
isSendLinkShown: false
|
isSendLinkShown: false,
|
||||||
|
isExpireChecked: false,
|
||||||
|
expireDays: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,29 +93,61 @@ class GenerateUploadLink extends React.Component {
|
|||||||
generateUploadLink = () => {
|
generateUploadLink = () => {
|
||||||
let path = this.props.itemPath;
|
let path = this.props.itemPath;
|
||||||
let repoID = this.props.repoID;
|
let repoID = this.props.repoID;
|
||||||
|
let { password, expireDays } = this.state;
|
||||||
|
|
||||||
if (this.state.showPasswordInput && (this.state.password == '')) {
|
let isValid = this.validateParamsInput();
|
||||||
this.setState({
|
if (isValid) {
|
||||||
errorInfo: gettext('Please enter password')
|
seafileAPI.createUploadLink(repoID, path, password, expireDays).then((res) => {
|
||||||
});
|
|
||||||
}
|
|
||||||
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 sharedUploadInfo = new SharedUploadInfo(res.data);
|
let sharedUploadInfo = new SharedUploadInfo(res.data);
|
||||||
this.setState({sharedUploadInfo: sharedUploadInfo});
|
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 = () => {
|
onCopyUploadLink = () => {
|
||||||
let uploadLink = this.state.sharedUploadInfo.link;
|
let uploadLink = this.state.sharedUploadInfo.link;
|
||||||
copy(uploadLink);
|
copy(uploadLink);
|
||||||
@ -156,6 +191,12 @@ class GenerateUploadLink extends React.Component {
|
|||||||
<span className="far fa-copy action-icon" onClick={this.onCopyUploadLink}></span>
|
<span className="far fa-copy action-icon" onClick={this.onCopyUploadLink}></span>
|
||||||
</dd>
|
</dd>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
{sharedUploadInfo.expire_date && (
|
||||||
|
<FormGroup className="mb-0">
|
||||||
|
<dt className="text-secondary font-weight-normal">{gettext('Expiration Date:')}</dt>
|
||||||
|
<dd>{moment(sharedUploadInfo.expire_date).format('YYYY-MM-DD hh:mm:ss')}</dd>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
{canSendShareLinkEmail && !isSendLinkShown && <Button onClick={this.toggleSendLink} className="mr-2">{gettext('Send')}</Button>}
|
{canSendShareLinkEmail && !isSendLinkShown && <Button onClick={this.toggleSendLink} className="mr-2">{gettext('Send')}</Button>}
|
||||||
{!isSendLinkShown && <Button onClick={this.deleteUploadLink}>{gettext('Delete')}</Button>}
|
{!isSendLinkShown && <Button onClick={this.deleteUploadLink}>{gettext('Delete')}</Button>}
|
||||||
@ -192,6 +233,18 @@ class GenerateUploadLink extends React.Component {
|
|||||||
<Input className="passwd" type={this.state.passwordVisible ? 'text' : 'password'} value={this.state.passwordnew || ''} onChange={this.inputPasswordNew} />
|
<Input className="passwd" type={this.state.passwordVisible ? 'text' : 'password'} value={this.state.passwordnew || ''} onChange={this.inputPasswordNew} />
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
}
|
}
|
||||||
|
<FormGroup check>
|
||||||
|
<Label check>
|
||||||
|
<Input className="expire-checkbox" type="checkbox" onChange={this.onExpireChecked}/>{' '}{gettext('Add auto expiration')}
|
||||||
|
</Label>
|
||||||
|
</FormGroup>
|
||||||
|
{this.state.isExpireChecked &&
|
||||||
|
<FormGroup check>
|
||||||
|
<Label check>
|
||||||
|
<Input className="expire-input expire-input-border" type="text" value={this.state.expireDays} onChange={this.onExpireDaysChanged} readOnly={!this.state.isExpireChecked}/><span className="expir-span">{gettext('days')}</span>
|
||||||
|
</Label>
|
||||||
|
</FormGroup>
|
||||||
|
}
|
||||||
{this.state.errorInfo && <Alert color="danger" className="mt-2">{this.state.errorInfo}</Alert>}
|
{this.state.errorInfo && <Alert color="danger" className="mt-2">{this.state.errorInfo}</Alert>}
|
||||||
<Button className="generate-link-btn" onClick={this.generateUploadLink}>{gettext('Generate')}</Button>
|
<Button className="generate-link-btn" onClick={this.generateUploadLink}>{gettext('Generate')}</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -10,6 +10,8 @@ class SharedUploadInfo {
|
|||||||
this.ctime = object.ctime;
|
this.ctime = object.ctime;
|
||||||
this.token = object.token;
|
this.token = object.token;
|
||||||
this.view_cnt = object.view_cnt;
|
this.view_cnt = object.view_cnt;
|
||||||
|
this.expire_date = object.expire_date;
|
||||||
|
this.is_expired = object.is_expired;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { Link } from '@reach/router';
|
import { Link } from '@reach/router';
|
||||||
|
import moment from 'moment';
|
||||||
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
|
import { Modal, ModalHeader, ModalBody } from 'reactstrap';
|
||||||
import { gettext, siteRoot, loginUrl, canGenerateShareLink } from '../../utils/constants';
|
import { gettext, siteRoot, loginUrl, canGenerateShareLink } from '../../utils/constants';
|
||||||
import { seafileAPI } from '../../utils/seafile-api';
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
@ -52,9 +53,10 @@ class Content extends Component {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th width="4%">{/*icon*/}</th>
|
<th width="4%">{/*icon*/}</th>
|
||||||
<th width="42%">{gettext('Name')}</th>
|
<th width="30%">{gettext('Name')}</th>
|
||||||
<th width="30%">{gettext('Library')}</th>
|
<th width="24%">{gettext('Library')}</th>
|
||||||
<th width="14%">{gettext('Visits')}</th>
|
<th width="16%">{gettext('Visits')}</th>
|
||||||
|
<th width="16%">{gettext('Expiration')}</th>
|
||||||
<th width="10%">{/*Operations*/}</th>
|
<th width="10%">{/*Operations*/}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -114,6 +116,24 @@ class Item extends Component {
|
|||||||
return { iconUrl, uploadUrl };
|
return { iconUrl, uploadUrl };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderExpriedData = () => {
|
||||||
|
let item = this.props.item;
|
||||||
|
if (!item.expire_date) {
|
||||||
|
return (
|
||||||
|
<Fragment>--</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let expire_date = moment(item.expire_date).format('YYYY-MM-DD');
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{item.is_expired ?
|
||||||
|
<span className="error">{expire_date}</span> :
|
||||||
|
expire_date
|
||||||
|
}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let item = this.props.item;
|
let item = this.props.item;
|
||||||
let { iconUrl, uploadUrl } = this.getUploadParams();
|
let { iconUrl, uploadUrl } = this.getUploadParams();
|
||||||
@ -128,6 +148,7 @@ class Item extends Component {
|
|||||||
<td><Link to={uploadUrl}>{item.obj_name}</Link></td>
|
<td><Link to={uploadUrl}>{item.obj_name}</Link></td>
|
||||||
<td><Link to={`${siteRoot}library/${item.repo_id}/${item.repo_name}`}>{item.repo_name}</Link></td>
|
<td><Link to={`${siteRoot}library/${item.repo_id}/${item.repo_name}`}>{item.repo_name}</Link></td>
|
||||||
<td>{item.view_cnt}</td>
|
<td>{item.view_cnt}</td>
|
||||||
|
<td>{this.renderExpriedData()}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="#" className={linkIconClassName} title={gettext('View')} onClick={this.viewLink}></a>
|
<a href="#" className={linkIconClassName} title={gettext('View')} onClick={this.viewLink}></a>
|
||||||
<a href="#" className={deleteIconClassName} title={gettext('Remove')} onClick={this.removeLink}></a>
|
<a href="#" className={deleteIconClassName} title={gettext('Remove')} onClick={this.removeLink}></a>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
from constance import config
|
from constance import config
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from rest_framework.authentication import SessionAuthentication
|
from rest_framework.authentication import SessionAuthentication
|
||||||
from rest_framework.permissions import IsAuthenticated
|
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.views import APIView
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from seaserv import seafile_api
|
from seaserv import seafile_api
|
||||||
@ -48,6 +50,11 @@ def get_upload_link_info(uls):
|
|||||||
else:
|
else:
|
||||||
ctime = ''
|
ctime = ''
|
||||||
|
|
||||||
|
if uls.expire_date:
|
||||||
|
expire_date = datetime_to_isoformat_timestr(uls.expire_date)
|
||||||
|
else:
|
||||||
|
expire_date = ''
|
||||||
|
|
||||||
data['repo_id'] = repo_id
|
data['repo_id'] = repo_id
|
||||||
data['repo_name'] = repo.repo_name if repo else ''
|
data['repo_name'] = repo.repo_name if repo else ''
|
||||||
data['path'] = path
|
data['path'] = path
|
||||||
@ -57,6 +64,8 @@ def get_upload_link_info(uls):
|
|||||||
data['link'] = gen_shared_upload_link(token)
|
data['link'] = gen_shared_upload_link(token)
|
||||||
data['token'] = token
|
data['token'] = token
|
||||||
data['username'] = uls.username
|
data['username'] = uls.username
|
||||||
|
data['expire_date'] = expire_date
|
||||||
|
data['is_expired'] = uls.is_expired()
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -143,6 +152,12 @@ class UploadLinks(APIView):
|
|||||||
error_msg = _('Password is too short')
|
error_msg = _('Password is too short')
|
||||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
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
|
# resource check
|
||||||
repo = seafile_api.get_repo(repo_id)
|
repo = seafile_api.get_repo(repo_id)
|
||||||
if not repo:
|
if not repo:
|
||||||
@ -164,11 +179,16 @@ class UploadLinks(APIView):
|
|||||||
error_msg = 'Permission denied.'
|
error_msg = 'Permission denied.'
|
||||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
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
|
username = request.user.username
|
||||||
uls = UploadLinkShare.objects.get_upload_link_by_path(username, repo_id, path)
|
uls = UploadLinkShare.objects.get_upload_link_by_path(username, repo_id, path)
|
||||||
if not uls:
|
if not uls:
|
||||||
uls = UploadLinkShare.objects.create_upload_link_share(username,
|
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)
|
link_info = get_upload_link_info(uls)
|
||||||
return Response(link_info)
|
return Response(link_info)
|
||||||
|
@ -451,6 +451,12 @@ class UploadLinkShare(models.Model):
|
|||||||
def is_owner(self, owner):
|
def is_owner(self, owner):
|
||||||
return owner == self.username
|
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):
|
class PrivateFileDirShareManager(models.Manager):
|
||||||
def add_private_file_share(self, from_user, to_user, repo_id, path, perm):
|
def add_private_file_share(self, from_user, to_user, repo_id, path, perm):
|
||||||
"""
|
"""
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import json
|
import json
|
||||||
from mock import patch
|
from mock import patch
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
@ -29,9 +31,9 @@ class UploadLinksTest(BaseTestCase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.remove_repo()
|
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,
|
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
|
return upload_link.token
|
||||||
|
|
||||||
@ -50,6 +52,8 @@ class UploadLinksTest(BaseTestCase):
|
|||||||
|
|
||||||
assert json_resp[0]['link'] is not None
|
assert json_resp[0]['link'] is not None
|
||||||
assert json_resp[0]['token'] 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 token in json_resp[0]['link']
|
||||||
assert 'u/d' 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)
|
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')
|
@patch.object(CanGenerateUploadLink, 'has_permission')
|
||||||
def test_get_link_with_invalid_user_role_permission(self, mock_has_permission):
|
def test_get_link_with_invalid_user_role_permission(self, mock_has_permission):
|
||||||
self.login_as(self.user)
|
self.login_as(self.user)
|
||||||
|
Loading…
Reference in New Issue
Block a user