1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-06 17:33:18 +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:
lian
2023-01-12 16:56:31 +08:00
committed by GitHub
parent 9f96732782
commit 4f9dcb344b
6 changed files with 224 additions and 7 deletions

View File

@@ -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;

View File

@@ -6,6 +6,7 @@ import { gettext } from '../../../utils/constants';
import toaster from '../../../components/toast';
import Loading from '../../../components/loading';
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 MainPanelTopbar from '../main-panel-topbar';
import Nav from './user-nav';
@@ -20,6 +21,8 @@ class Content extends Component {
currentKey: '',
dialogTitle: '',
isSetQuotaDialogOpen: false,
isSetUserUploadRateLimitDialogOpen: false,
isSetUserDownloadRateLimitDialogOpen: false,
isUpdateUserDialogOpen: false
};
}
@@ -28,10 +31,27 @@ class Content extends Component {
this.setState({isSetQuotaDialogOpen: !this.state.isSetQuotaDialogOpen});
}
toggleSetUserUploadRateLimitDialog = () => {
this.setState({isSetUserUploadRateLimitDialogOpen: !this.state.isSetUserUploadRateLimitDialogOpen});
}
toggleSetUserDownloadRateLimitDialog = () => {
this.setState({isSetUserDownloadRateLimitDialogOpen: !this.state.isSetUserDownloadRateLimitDialogOpen});
}
updateQuota = (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) => {
this.setState({
currentKey: key,
@@ -84,7 +104,8 @@ class Content extends Component {
const user = this.props.userInfo;
const {
currentKey, dialogTitle,
isSetQuotaDialogOpen, isUpdateUserDialogOpen
isSetQuotaDialogOpen, isUpdateUserDialogOpen,
isSetUserUploadRateLimitDialogOpen, isSetUserDownloadRateLimitDialogOpen
} = this.state;
return (
<Fragment>
@@ -134,6 +155,18 @@ class Content extends Component {
{this.showEditIcon(this.toggleSetQuotaDialog)}
</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 &&
<Fragment>
<dt className="info-item-heading">{gettext('Two-Factor Authentication')}</dt>
@@ -163,6 +196,20 @@ class Content extends Component {
toggle={this.toggleSetQuotaDialog}
/>
}
{isSetUserUploadRateLimitDialogOpen &&
<SysAdminSetUploadDownloadRateLimitDialog
uploadOrDownload="upload"
updateUploadDownloadRateLimit={this.updateUploadDownloadRateLimit}
toggle={this.toggleSetUserUploadRateLimitDialog}
/>
}
{isSetUserDownloadRateLimitDialogOpen &&
<SysAdminSetUploadDownloadRateLimitDialog
uploadOrDownload="download"
updateUploadDownloadRateLimit={this.updateUploadDownloadRateLimit}
toggle={this.toggleSetUserDownloadRateLimitDialog}
/>
}
{isUpdateUserDialogOpen &&
<SysAdminUpdateUserDialog
dialogTitle={dialogTitle}

View File

@@ -37,7 +37,7 @@ from seahub.utils import is_valid_username2, is_org_context, \
IS_EMAIL_CONFIGURED, send_html_email, get_site_name, \
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, \
datetime_to_isoformat_timestr
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,
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
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)
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):
@@ -995,6 +1003,9 @@ class AdminUser(APIView):
user_info = get_user_info(email)
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)
if last_login_obj:
@@ -1106,6 +1117,30 @@ class AdminUser(APIView):
error_msg = 'Institution %s does not exist' % institution
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
try:
user_obj = User.objects.get(email=email)
@@ -1114,9 +1149,20 @@ class AdminUser(APIView):
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
try:
update_user_info(request, user=user_obj, 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)
update_user_info(request,
user=user_obj,
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:
logger.error(e)
error_msg = 'Internal Server Error'
@@ -1147,6 +1193,9 @@ class AdminUser(APIView):
user_info = get_user_info(email)
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)

View File

@@ -2,7 +2,10 @@
from copy import deepcopy
import logging
from seaserv import seafile_api
from django.conf import settings
from seahub.utils import is_pro_version
from seahub.constants import DEFAULT_USER, GUEST_USER, \
DEFAULT_ADMIN, SYSTEM_ADMIN, DAILY_ADMIN, AUDIT_ADMIN
@@ -43,6 +46,8 @@ DEFAULT_ENABLED_ROLE_PERMISSIONS = {
'storage_ids': [],
'role_quota': '',
'can_publish_repo': True,
'upload_rate_limit': 0,
'download_rate_limit': 0,
},
GUEST_USER: {
'can_add_repo': False,
@@ -62,6 +67,8 @@ DEFAULT_ENABLED_ROLE_PERMISSIONS = {
'storage_ids': [],
'role_quota': '',
'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
)
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
# 1, Admin without a role or with a role of `default_admin` can view ALL pages.

View File

@@ -15,6 +15,7 @@ UNIT_GIB = 'gib'
UNIT_TIB = 'tib'
UNIT_PIB = 'pib'
def get_file_size_unit(unit_type):
"""
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)
def get_quota_from_string(quota_str):
quota_str = quota_str.lower()
if quota_str.endswith('g'):
@@ -51,6 +53,20 @@ def get_quota_from_string(quota_str):
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):
if byte < 0:

View File

@@ -11,4 +11,4 @@ class UtilsTest(BaseTestCase):
assert DEFAULT_USER in get_available_roles()
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