mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-08 18:30:53 +00:00
Rate limit (#5169)
* set upload/download rate limit by user role * admin set user upload/download rate limit * update Co-authored-by: lian <lian@seafile.com>
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Alert, Modal, ModalHeader, ModalBody, ModalFooter, Button, Form, FormGroup, Input, InputGroup, InputGroupAddon, InputGroupText } from 'reactstrap';
|
||||||
|
import { gettext } from '../../../utils/constants';
|
||||||
|
import { Utils } from '../../../utils/utils';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
uploadOrDownload: PropTypes.string.isRequired,
|
||||||
|
toggle: PropTypes.func.isRequired,
|
||||||
|
updateUploadDownloadRateLimit: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
class SysAdminSetUploadDownloadRateLimitDialog extends React.Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
rateLimit: '',
|
||||||
|
isSubmitBtnActive: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle = () => {
|
||||||
|
this.props.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRateLimitChange = (e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
this.setState({
|
||||||
|
rateLimit: value,
|
||||||
|
isSubmitBtnActive: value.trim() != ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyPress = (e) => {
|
||||||
|
if (e.key == 'Enter') {
|
||||||
|
this.handleSubmit();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit = () => {
|
||||||
|
this.props.updateUploadDownloadRateLimit(this.props.uploadOrDownload, this.state.rateLimit.trim());
|
||||||
|
this.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { rateLimit, isSubmitBtnActive } = this.state;
|
||||||
|
return (
|
||||||
|
<Modal isOpen={true} toggle={this.toggle}>
|
||||||
|
<ModalHeader toggle={this.toggle}>{this.props.uploadOrDownload == "upload" ? gettext('Set Upload Rate Limit') : gettext('Set Download Rate Limit')}</ModalHeader>
|
||||||
|
<ModalBody>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<InputGroup>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={rateLimit}
|
||||||
|
onKeyPress={this.handleKeyPress}
|
||||||
|
onChange={this.handleRateLimitChange}
|
||||||
|
/>
|
||||||
|
<InputGroupAddon addonType="append">
|
||||||
|
<InputGroupText>kB/s</InputGroupText>
|
||||||
|
</InputGroupAddon>
|
||||||
|
</InputGroup>
|
||||||
|
<p className="small text-secondary mt-2 mb-2">
|
||||||
|
{gettext('An integer that is greater than or equal to 0.')}
|
||||||
|
<br />
|
||||||
|
{gettext('Tip: 0 means default limit')}
|
||||||
|
</p>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
|
||||||
|
<Button color="primary" onClick={this.handleSubmit} disabled={!isSubmitBtnActive}>{gettext('Submit')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SysAdminSetUploadDownloadRateLimitDialog.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default SysAdminSetUploadDownloadRateLimitDialog;
|
@@ -6,6 +6,7 @@ import { gettext } from '../../../utils/constants';
|
|||||||
import toaster from '../../../components/toast';
|
import toaster from '../../../components/toast';
|
||||||
import Loading from '../../../components/loading';
|
import Loading from '../../../components/loading';
|
||||||
import SysAdminSetQuotaDialog from '../../../components/dialog/sysadmin-dialog/set-quota';
|
import SysAdminSetQuotaDialog from '../../../components/dialog/sysadmin-dialog/set-quota';
|
||||||
|
import SysAdminSetUploadDownloadRateLimitDialog from '../../../components/dialog/sysadmin-dialog/set-upload-download-rate-limit';
|
||||||
import SysAdminUpdateUserDialog from '../../../components/dialog/sysadmin-dialog/update-user';
|
import SysAdminUpdateUserDialog from '../../../components/dialog/sysadmin-dialog/update-user';
|
||||||
import MainPanelTopbar from '../main-panel-topbar';
|
import MainPanelTopbar from '../main-panel-topbar';
|
||||||
import Nav from './user-nav';
|
import Nav from './user-nav';
|
||||||
@@ -20,6 +21,8 @@ class Content extends Component {
|
|||||||
currentKey: '',
|
currentKey: '',
|
||||||
dialogTitle: '',
|
dialogTitle: '',
|
||||||
isSetQuotaDialogOpen: false,
|
isSetQuotaDialogOpen: false,
|
||||||
|
isSetUserUploadRateLimitDialogOpen: false,
|
||||||
|
isSetUserDownloadRateLimitDialogOpen: false,
|
||||||
isUpdateUserDialogOpen: false
|
isUpdateUserDialogOpen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -28,10 +31,27 @@ class Content extends Component {
|
|||||||
this.setState({isSetQuotaDialogOpen: !this.state.isSetQuotaDialogOpen});
|
this.setState({isSetQuotaDialogOpen: !this.state.isSetQuotaDialogOpen});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleSetUserUploadRateLimitDialog = () => {
|
||||||
|
this.setState({isSetUserUploadRateLimitDialogOpen: !this.state.isSetUserUploadRateLimitDialogOpen});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleSetUserDownloadRateLimitDialog = () => {
|
||||||
|
this.setState({isSetUserDownloadRateLimitDialogOpen: !this.state.isSetUserDownloadRateLimitDialogOpen});
|
||||||
|
}
|
||||||
|
|
||||||
updateQuota = (value) => {
|
updateQuota = (value) => {
|
||||||
this.props.updateUser('quota_total', value);
|
this.props.updateUser('quota_total', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUploadDownloadRateLimit = (uploadOrDownload, value) => {
|
||||||
|
if (uploadOrDownload == 'upload'){
|
||||||
|
this.props.updateUser('upload_rate_limit', value);
|
||||||
|
}
|
||||||
|
if (uploadOrDownload == 'download'){
|
||||||
|
this.props.updateUser('download_rate_limit', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toggleDialog = (key, dialogTitle) => {
|
toggleDialog = (key, dialogTitle) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
currentKey: key,
|
currentKey: key,
|
||||||
@@ -84,7 +104,8 @@ class Content extends Component {
|
|||||||
const user = this.props.userInfo;
|
const user = this.props.userInfo;
|
||||||
const {
|
const {
|
||||||
currentKey, dialogTitle,
|
currentKey, dialogTitle,
|
||||||
isSetQuotaDialogOpen, isUpdateUserDialogOpen
|
isSetQuotaDialogOpen, isUpdateUserDialogOpen,
|
||||||
|
isSetUserUploadRateLimitDialogOpen, isSetUserDownloadRateLimitDialogOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
@@ -134,6 +155,18 @@ class Content extends Component {
|
|||||||
{this.showEditIcon(this.toggleSetQuotaDialog)}
|
{this.showEditIcon(this.toggleSetQuotaDialog)}
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
|
<dt className="info-item-heading">{gettext('Upload Rate Limit')}</dt>
|
||||||
|
<dd className="info-item-content">
|
||||||
|
{user.upload_rate_limit > 0 ? user.upload_rate_limit + ' kB/s' : '--'}
|
||||||
|
{this.showEditIcon(this.toggleSetUserUploadRateLimitDialog)}
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt className="info-item-heading">{gettext('Download Rate Limit')}</dt>
|
||||||
|
<dd className="info-item-content">
|
||||||
|
{user.download_rate_limit > 0 ? user.download_rate_limit + ' kB/s' : '--'}
|
||||||
|
{this.showEditIcon(this.toggleSetUserDownloadRateLimitDialog)}
|
||||||
|
</dd>
|
||||||
|
|
||||||
{twoFactorAuthEnabled &&
|
{twoFactorAuthEnabled &&
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<dt className="info-item-heading">{gettext('Two-Factor Authentication')}</dt>
|
<dt className="info-item-heading">{gettext('Two-Factor Authentication')}</dt>
|
||||||
@@ -163,6 +196,20 @@ class Content extends Component {
|
|||||||
toggle={this.toggleSetQuotaDialog}
|
toggle={this.toggleSetQuotaDialog}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
{isSetUserUploadRateLimitDialogOpen &&
|
||||||
|
<SysAdminSetUploadDownloadRateLimitDialog
|
||||||
|
uploadOrDownload="upload"
|
||||||
|
updateUploadDownloadRateLimit={this.updateUploadDownloadRateLimit}
|
||||||
|
toggle={this.toggleSetUserUploadRateLimitDialog}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{isSetUserDownloadRateLimitDialogOpen &&
|
||||||
|
<SysAdminSetUploadDownloadRateLimitDialog
|
||||||
|
uploadOrDownload="download"
|
||||||
|
updateUploadDownloadRateLimit={this.updateUploadDownloadRateLimit}
|
||||||
|
toggle={this.toggleSetUserDownloadRateLimitDialog}
|
||||||
|
/>
|
||||||
|
}
|
||||||
{isUpdateUserDialogOpen &&
|
{isUpdateUserDialogOpen &&
|
||||||
<SysAdminUpdateUserDialog
|
<SysAdminUpdateUserDialog
|
||||||
dialogTitle={dialogTitle}
|
dialogTitle={dialogTitle}
|
||||||
|
@@ -37,7 +37,7 @@ from seahub.utils import is_valid_username2, is_org_context, \
|
|||||||
IS_EMAIL_CONFIGURED, send_html_email, get_site_name, \
|
IS_EMAIL_CONFIGURED, send_html_email, get_site_name, \
|
||||||
gen_shared_link, gen_shared_upload_link
|
gen_shared_link, gen_shared_upload_link
|
||||||
|
|
||||||
from seahub.utils.file_size import get_file_size_unit
|
from seahub.utils.file_size import get_file_size_unit, byte_to_kb
|
||||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr, \
|
from seahub.utils.timeutils import timestamp_to_isoformat_timestr, \
|
||||||
datetime_to_isoformat_timestr
|
datetime_to_isoformat_timestr
|
||||||
from seahub.utils.user_permissions import get_user_role
|
from seahub.utils.user_permissions import get_user_role
|
||||||
@@ -180,7 +180,9 @@ def create_user_info(request, email, role, nickname, contact_email, quota_total_
|
|||||||
|
|
||||||
|
|
||||||
def update_user_info(request, user, password, is_active, is_staff, role,
|
def update_user_info(request, user, password, is_active, is_staff, role,
|
||||||
nickname, login_id, contact_email, reference_id, quota_total_mb, institution_name):
|
nickname, login_id, contact_email, reference_id,
|
||||||
|
quota_total_mb, institution_name,
|
||||||
|
upload_rate_limit, download_rate_limit):
|
||||||
|
|
||||||
# update basic user info
|
# update basic user info
|
||||||
if is_active is not None:
|
if is_active is not None:
|
||||||
@@ -239,6 +241,12 @@ def update_user_info(request, user, password, is_active, is_staff, role,
|
|||||||
logger.error(e)
|
logger.error(e)
|
||||||
seafile_api.set_user_quota(email, -1)
|
seafile_api.set_user_quota(email, -1)
|
||||||
|
|
||||||
|
if upload_rate_limit is not None:
|
||||||
|
seafile_api.set_user_upload_rate_limit(email, upload_rate_limit * 1000)
|
||||||
|
|
||||||
|
if download_rate_limit is not None:
|
||||||
|
seafile_api.set_user_download_rate_limit(email, download_rate_limit * 1000)
|
||||||
|
|
||||||
|
|
||||||
def get_user_info(email):
|
def get_user_info(email):
|
||||||
|
|
||||||
@@ -995,6 +1003,9 @@ class AdminUser(APIView):
|
|||||||
|
|
||||||
user_info = get_user_info(email)
|
user_info = get_user_info(email)
|
||||||
user_info['avatar_url'], _, _ = api_avatar_url(email, avatar_size)
|
user_info['avatar_url'], _, _ = api_avatar_url(email, avatar_size)
|
||||||
|
if is_pro_version():
|
||||||
|
user_info['upload_rate_limit'] = byte_to_kb(seafile_api.get_user_upload_rate_limit(email))
|
||||||
|
user_info['download_rate_limit'] = byte_to_kb(seafile_api.get_user_download_rate_limit(email))
|
||||||
|
|
||||||
last_login_obj = UserLastLogin.objects.get_by_username(email)
|
last_login_obj = UserLastLogin.objects.get_by_username(email)
|
||||||
if last_login_obj:
|
if last_login_obj:
|
||||||
@@ -1106,6 +1117,30 @@ class AdminUser(APIView):
|
|||||||
error_msg = 'Institution %s does not exist' % institution
|
error_msg = 'Institution %s does not exist' % institution
|
||||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
upload_rate_limit = request.data.get("upload_rate_limit", None)
|
||||||
|
if upload_rate_limit:
|
||||||
|
try:
|
||||||
|
upload_rate_limit = int(upload_rate_limit)
|
||||||
|
except ValueError:
|
||||||
|
error_msg = _('Must be an integer that is greater than or equal to 0.')
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
if upload_rate_limit < 0:
|
||||||
|
error_msg = _('Must be an integer that is greater than or equal to 0.')
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
download_rate_limit = request.data.get("download_rate_limit", None)
|
||||||
|
if download_rate_limit:
|
||||||
|
try:
|
||||||
|
download_rate_limit = int(download_rate_limit)
|
||||||
|
except ValueError:
|
||||||
|
error_msg = _('Must be an integer that is greater than or equal to 0.')
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
|
if download_rate_limit < 0:
|
||||||
|
error_msg = _('Must be an integer that is greater than or equal to 0.')
|
||||||
|
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||||
|
|
||||||
# query user info
|
# query user info
|
||||||
try:
|
try:
|
||||||
user_obj = User.objects.get(email=email)
|
user_obj = User.objects.get(email=email)
|
||||||
@@ -1114,9 +1149,20 @@ class AdminUser(APIView):
|
|||||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
update_user_info(request, user=user_obj, password=password, is_active=is_active, is_staff=is_staff,
|
update_user_info(request,
|
||||||
role=role, nickname=name, login_id=login_id, contact_email=contact_email,
|
user=user_obj,
|
||||||
reference_id=reference_id, quota_total_mb=quota_total_mb, institution_name=institution)
|
password=password,
|
||||||
|
is_active=is_active,
|
||||||
|
is_staff=is_staff,
|
||||||
|
role=role,
|
||||||
|
nickname=name,
|
||||||
|
login_id=login_id,
|
||||||
|
contact_email=contact_email,
|
||||||
|
reference_id=reference_id,
|
||||||
|
quota_total_mb=quota_total_mb,
|
||||||
|
institution_name=institution,
|
||||||
|
upload_rate_limit=upload_rate_limit,
|
||||||
|
download_rate_limit=download_rate_limit)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
error_msg = 'Internal Server Error'
|
error_msg = 'Internal Server Error'
|
||||||
@@ -1147,6 +1193,9 @@ class AdminUser(APIView):
|
|||||||
|
|
||||||
user_info = get_user_info(email)
|
user_info = get_user_info(email)
|
||||||
user_info['update_status_tip'] = update_status_tip
|
user_info['update_status_tip'] = update_status_tip
|
||||||
|
if is_pro_version():
|
||||||
|
user_info['upload_rate_limit'] = byte_to_kb(seafile_api.get_user_upload_rate_limit(email))
|
||||||
|
user_info['download_rate_limit'] = byte_to_kb(seafile_api.get_user_download_rate_limit(email))
|
||||||
|
|
||||||
return Response(user_info)
|
return Response(user_info)
|
||||||
|
|
||||||
|
@@ -2,7 +2,10 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from seaserv import seafile_api
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from seahub.utils import is_pro_version
|
||||||
from seahub.constants import DEFAULT_USER, GUEST_USER, \
|
from seahub.constants import DEFAULT_USER, GUEST_USER, \
|
||||||
DEFAULT_ADMIN, SYSTEM_ADMIN, DAILY_ADMIN, AUDIT_ADMIN
|
DEFAULT_ADMIN, SYSTEM_ADMIN, DAILY_ADMIN, AUDIT_ADMIN
|
||||||
|
|
||||||
@@ -43,6 +46,8 @@ DEFAULT_ENABLED_ROLE_PERMISSIONS = {
|
|||||||
'storage_ids': [],
|
'storage_ids': [],
|
||||||
'role_quota': '',
|
'role_quota': '',
|
||||||
'can_publish_repo': True,
|
'can_publish_repo': True,
|
||||||
|
'upload_rate_limit': 0,
|
||||||
|
'download_rate_limit': 0,
|
||||||
},
|
},
|
||||||
GUEST_USER: {
|
GUEST_USER: {
|
||||||
'can_add_repo': False,
|
'can_add_repo': False,
|
||||||
@@ -62,6 +67,8 @@ DEFAULT_ENABLED_ROLE_PERMISSIONS = {
|
|||||||
'storage_ids': [],
|
'storage_ids': [],
|
||||||
'role_quota': '',
|
'role_quota': '',
|
||||||
'can_publish_repo': False,
|
'can_publish_repo': False,
|
||||||
|
'upload_rate_limit': 0,
|
||||||
|
'download_rate_limit': 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +81,18 @@ ENABLED_ROLE_PERMISSIONS = merge_roles(
|
|||||||
DEFAULT_ENABLED_ROLE_PERMISSIONS, custom_role_permissions
|
DEFAULT_ENABLED_ROLE_PERMISSIONS, custom_role_permissions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if is_pro_version():
|
||||||
|
for role, permissions in ENABLED_ROLE_PERMISSIONS.items():
|
||||||
|
|
||||||
|
upload_rate_limit = permissions.get('upload_rate_limit', 0)
|
||||||
|
if upload_rate_limit >= 0:
|
||||||
|
seafile_api.set_role_upload_rate_limit(role, upload_rate_limit * 1000)
|
||||||
|
|
||||||
|
download_rate_limit = permissions.get('download_rate_limit', 0)
|
||||||
|
if download_rate_limit >= 0:
|
||||||
|
seafile_api.set_role_download_rate_limit(role, download_rate_limit * 1000)
|
||||||
|
|
||||||
|
|
||||||
# role permission for administraror
|
# role permission for administraror
|
||||||
|
|
||||||
# 1, Admin without a role or with a role of `default_admin` can view ALL pages.
|
# 1, Admin without a role or with a role of `default_admin` can view ALL pages.
|
||||||
|
@@ -15,6 +15,7 @@ UNIT_GIB = 'gib'
|
|||||||
UNIT_TIB = 'tib'
|
UNIT_TIB = 'tib'
|
||||||
UNIT_PIB = 'pib'
|
UNIT_PIB = 'pib'
|
||||||
|
|
||||||
|
|
||||||
def get_file_size_unit(unit_type):
|
def get_file_size_unit(unit_type):
|
||||||
"""
|
"""
|
||||||
File size unit according to https://en.wikipedia.org/wiki/Kibibyte.
|
File size unit according to https://en.wikipedia.org/wiki/Kibibyte.
|
||||||
@@ -40,6 +41,7 @@ def get_file_size_unit(unit_type):
|
|||||||
|
|
||||||
return table.get(unit_type)
|
return table.get(unit_type)
|
||||||
|
|
||||||
|
|
||||||
def get_quota_from_string(quota_str):
|
def get_quota_from_string(quota_str):
|
||||||
quota_str = quota_str.lower()
|
quota_str = quota_str.lower()
|
||||||
if quota_str.endswith('g'):
|
if quota_str.endswith('g'):
|
||||||
@@ -51,6 +53,20 @@ def get_quota_from_string(quota_str):
|
|||||||
|
|
||||||
return quota
|
return quota
|
||||||
|
|
||||||
|
|
||||||
|
def byte_to_kb(byte):
|
||||||
|
|
||||||
|
if byte < 0:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
try:
|
||||||
|
unit = get_file_size_unit(UNIT_KB)
|
||||||
|
return round(float(byte)/unit, 2)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
def byte_to_mb(byte):
|
def byte_to_mb(byte):
|
||||||
|
|
||||||
if byte < 0:
|
if byte < 0:
|
||||||
|
@@ -11,4 +11,4 @@ class UtilsTest(BaseTestCase):
|
|||||||
assert DEFAULT_USER in get_available_roles()
|
assert DEFAULT_USER in get_available_roles()
|
||||||
|
|
||||||
def test_get_enabled_role_permissions_by_role(self):
|
def test_get_enabled_role_permissions_by_role(self):
|
||||||
assert len(list(get_enabled_role_permissions_by_role(DEFAULT_USER).keys())) == 17
|
assert len(list(get_enabled_role_permissions_by_role(DEFAULT_USER).keys())) == 19
|
||||||
|
Reference in New Issue
Block a user