mirror of
https://github.com/zulip/zulip.git
synced 2025-11-15 11:22:04 +00:00
upload: Limit total size of files uploaded by a user to 1GB.
Fixes #3884.
This commit is contained in:
committed by
Tim Abbott
parent
777b7d4cb7
commit
866a7b06b2
@@ -1109,6 +1109,10 @@ $(function () {
|
|||||||
case 'REQUEST ENTITY TOO LARGE':
|
case 'REQUEST ENTITY TOO LARGE':
|
||||||
msg = i18n.t("Sorry, the file was too large.");
|
msg = i18n.t("Sorry, the file was too large.");
|
||||||
break;
|
break;
|
||||||
|
case 'QuotaExceeded':
|
||||||
|
msg = i18n.t("Upload would exceed your maximum quota."
|
||||||
|
+ " Consider deleting some previously uploaded files.");
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
msg = i18n.t("An unknown error occured.");
|
msg = i18n.t("An unknown error occured.");
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -360,7 +360,12 @@
|
|||||||
|
|
||||||
// Pass any errors to the error option
|
// Pass any errors to the error option
|
||||||
if (xhr.status < 200 || xhr.status > 299) {
|
if (xhr.status < 200 || xhr.status > 299) {
|
||||||
on_error(xhr.statusText, xhr.status);
|
if (this.responseText.includes("Upload would exceed your maximum quota.")) {
|
||||||
|
var errorString = "QuotaExceeded";
|
||||||
|
on_error(errorString, xhr.status);
|
||||||
|
} else {
|
||||||
|
on_error(xhr.statusText, xhr.status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from django.conf import settings
|
|||||||
from django.template.defaultfilters import slugify
|
from django.template.defaultfilters import slugify
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from django.db.models import Sum
|
||||||
from jinja2 import Markup as mark_safe
|
from jinja2 import Markup as mark_safe
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
@@ -84,6 +85,9 @@ def random_name(bytes=60):
|
|||||||
class BadImageError(JsonableError):
|
class BadImageError(JsonableError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class ExceededQuotaError(JsonableError):
|
||||||
|
pass
|
||||||
|
|
||||||
def resize_avatar(image_data, size=DEFAULT_AVATAR_SIZE):
|
def resize_avatar(image_data, size=DEFAULT_AVATAR_SIZE):
|
||||||
# type: (binary_type, int) -> binary_type
|
# type: (binary_type, int) -> binary_type
|
||||||
try:
|
try:
|
||||||
@@ -165,6 +169,24 @@ def upload_image_to_s3(
|
|||||||
|
|
||||||
key.set_contents_from_string(force_str(contents), headers=headers)
|
key.set_contents_from_string(force_str(contents), headers=headers)
|
||||||
|
|
||||||
|
def get_total_uploads_size_for_user(user):
|
||||||
|
# type: (UserProfile) -> int
|
||||||
|
uploads = Attachment.objects.filter(owner=user)
|
||||||
|
total_quota = uploads.aggregate(Sum('size'))['size__sum']
|
||||||
|
|
||||||
|
# In case user has no uploads
|
||||||
|
if (total_quota is None):
|
||||||
|
total_quota = 0
|
||||||
|
return total_quota
|
||||||
|
|
||||||
|
def within_upload_quota(user, uploaded_file_size):
|
||||||
|
# type: (UserProfile, int) -> bool
|
||||||
|
total_quota = get_total_uploads_size_for_user(user)
|
||||||
|
if (total_quota + uploaded_file_size > user.quota):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
def get_file_info(request, user_file):
|
def get_file_info(request, user_file):
|
||||||
# type: (HttpRequest, File) -> Tuple[Text, int, Optional[Text]]
|
# type: (HttpRequest, File) -> Tuple[Text, int, Optional[Text]]
|
||||||
|
|
||||||
|
|||||||
20
zerver/migrations/0057_userprofile_quota.py
Normal file
20
zerver/migrations/0057_userprofile_quota.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-03-04 07:40
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('zerver', '0056_userprofile_emoji_alt_code'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='userprofile',
|
||||||
|
name='quota',
|
||||||
|
field=models.IntegerField(default=1073741824),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -613,6 +613,10 @@ class UserProfile(ModelReprMixin, AbstractBaseUser, PermissionsMixin):
|
|||||||
|
|
||||||
objects = UserManager() # type: UserManager
|
objects = UserManager() # type: UserManager
|
||||||
|
|
||||||
|
DEFAULT_UPLOADS_QUOTA = 1024*1024*1024
|
||||||
|
|
||||||
|
quota = models.IntegerField(default=DEFAULT_UPLOADS_QUOTA) # type: int
|
||||||
|
|
||||||
def can_admin_user(self, target_user):
|
def can_admin_user(self, target_user):
|
||||||
# type: (UserProfile) -> bool
|
# type: (UserProfile) -> bool
|
||||||
"""Returns whether this user has permission to modify target_user"""
|
"""Returns whether this user has permission to modify target_user"""
|
||||||
|
|||||||
@@ -318,6 +318,38 @@ class FileUploadTest(UploadSerializeMixin, ZulipTestCase):
|
|||||||
content = ujson.loads(result.content)
|
content = ujson.loads(result.content)
|
||||||
assert sanitize_name(expected) in content['uri']
|
assert sanitize_name(expected) in content['uri']
|
||||||
|
|
||||||
|
def test_upload_size_quote(self):
|
||||||
|
# type: () -> None
|
||||||
|
"""
|
||||||
|
User quote for uploading should not be exceeded
|
||||||
|
"""
|
||||||
|
self.login("hamlet@zulip.com")
|
||||||
|
|
||||||
|
d1 = StringIO("zulip!")
|
||||||
|
d1.name = "dummy_1.txt"
|
||||||
|
result = self.client_post("/json/upload_file", {'file': d1})
|
||||||
|
json = ujson.loads(result.content)
|
||||||
|
uri = json["uri"]
|
||||||
|
d1_path_id = re.sub('/user_uploads/', '', uri)
|
||||||
|
d1_attachment = Attachment.objects.get(path_id = d1_path_id)
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Below we set size quota to the limit without 1 upload(1GB - 11 bytes).
|
||||||
|
"""
|
||||||
|
d1_attachment.size = UserProfile.DEFAULT_UPLOADS_QUOTA - 11
|
||||||
|
d1_attachment.save()
|
||||||
|
|
||||||
|
d2 = StringIO("zulip!")
|
||||||
|
d2.name = "dummy_2.txt"
|
||||||
|
result = self.client_post("/json/upload_file", {'file': d2})
|
||||||
|
self.assert_json_success(result)
|
||||||
|
|
||||||
|
d3 = StringIO("zulip!")
|
||||||
|
d3.name = "dummy_3.txt"
|
||||||
|
result = self.client_post("/json/upload_file", {'file': d3})
|
||||||
|
self.assert_json_error(result, "Upload would exceed your maximum quota.")
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
# type: () -> None
|
# type: () -> None
|
||||||
destroy_uploads()
|
destroy_uploads()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from zerver.decorator import authenticated_json_post_view
|
|||||||
from zerver.lib.request import has_request_variables, REQ
|
from zerver.lib.request import has_request_variables, REQ
|
||||||
from zerver.lib.response import json_success, json_error
|
from zerver.lib.response import json_success, json_error
|
||||||
from zerver.lib.upload import upload_message_image_from_request, get_local_file_path, \
|
from zerver.lib.upload import upload_message_image_from_request, get_local_file_path, \
|
||||||
get_signed_upload_url, get_realm_for_filename
|
get_signed_upload_url, get_realm_for_filename, within_upload_quota
|
||||||
from zerver.lib.validator import check_bool
|
from zerver.lib.validator import check_bool
|
||||||
from zerver.models import UserProfile
|
from zerver.models import UserProfile
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -69,9 +69,12 @@ def upload_file_backend(request, user_profile):
|
|||||||
return json_error(_("You may only upload one file at a time"))
|
return json_error(_("You may only upload one file at a time"))
|
||||||
|
|
||||||
user_file = list(request.FILES.values())[0]
|
user_file = list(request.FILES.values())[0]
|
||||||
if ((settings.MAX_FILE_UPLOAD_SIZE * 1024 * 1024) < user_file._get_size()):
|
file_size = user_file._get_size()
|
||||||
|
if settings.MAX_FILE_UPLOAD_SIZE * 1024 * 1024 < file_size:
|
||||||
return json_error(_("Uploaded file is larger than the allowed limit of %s MB") % (
|
return json_error(_("Uploaded file is larger than the allowed limit of %s MB") % (
|
||||||
settings.MAX_FILE_UPLOAD_SIZE))
|
settings.MAX_FILE_UPLOAD_SIZE))
|
||||||
|
if not within_upload_quota(user_profile, file_size):
|
||||||
|
return json_error(_("Upload would exceed your maximum quota."))
|
||||||
|
|
||||||
if not isinstance(user_file.name, str):
|
if not isinstance(user_file.name, str):
|
||||||
# It seems that in Python 2 unicode strings containing bytes are
|
# It seems that in Python 2 unicode strings containing bytes are
|
||||||
|
|||||||
Reference in New Issue
Block a user