upload: Limit total size of files uploaded by a user to 1GB.

Fixes #3884.
This commit is contained in:
Philip Skomorokhov
2017-03-02 13:17:10 +03:00
committed by Tim Abbott
parent 777b7d4cb7
commit 866a7b06b2
7 changed files with 93 additions and 3 deletions

View File

@@ -1109,6 +1109,10 @@ $(function () {
case 'REQUEST ENTITY TOO LARGE':
msg = i18n.t("Sorry, the file was too large.");
break;
case 'QuotaExceeded':
msg = i18n.t("Upload would exceed your maximum quota."
+ " Consider deleting some previously uploaded files.");
break;
default:
msg = i18n.t("An unknown error occured.");
break;

View File

@@ -360,8 +360,13 @@
// Pass any errors to the error option
if (xhr.status < 200 || xhr.status > 299) {
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);
}
}
};
}

View File

@@ -6,6 +6,7 @@ from django.conf import settings
from django.template.defaultfilters import slugify
from django.core.files import File
from django.http import HttpRequest
from django.db.models import Sum
from jinja2 import Markup as mark_safe
import unicodedata
@@ -84,6 +85,9 @@ def random_name(bytes=60):
class BadImageError(JsonableError):
pass
class ExceededQuotaError(JsonableError):
pass
def resize_avatar(image_data, size=DEFAULT_AVATAR_SIZE):
# type: (binary_type, int) -> binary_type
try:
@@ -165,6 +169,24 @@ def upload_image_to_s3(
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):
# type: (HttpRequest, File) -> Tuple[Text, int, Optional[Text]]

View 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),
),
]

View File

@@ -613,6 +613,10 @@ class UserProfile(ModelReprMixin, AbstractBaseUser, PermissionsMixin):
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):
# type: (UserProfile) -> bool
"""Returns whether this user has permission to modify target_user"""

View File

@@ -318,6 +318,38 @@ class FileUploadTest(UploadSerializeMixin, ZulipTestCase):
content = ujson.loads(result.content)
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):
# type: () -> None
destroy_uploads()

View File

@@ -10,7 +10,7 @@ from zerver.decorator import authenticated_json_post_view
from zerver.lib.request import has_request_variables, REQ
from zerver.lib.response import json_success, json_error
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.models import UserProfile
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"))
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") % (
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):
# It seems that in Python 2 unicode strings containing bytes are