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': 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;

View File

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

View File

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

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 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"""

View File

@@ -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()

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.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