implement file size upload limit

This commit is contained in:
Jakob Ketterl 2021-04-29 18:18:18 +02:00
parent 7115d5c951
commit af553c422d
4 changed files with 75 additions and 17 deletions

View File

@ -6,6 +6,7 @@ $.fn.imageUpload = function() {
var originalUrl = $img.prop('src'); var originalUrl = $img.prop('src');
var $input = $(this).find('input'); var $input = $(this).find('input');
var id = $input.prop('id'); var id = $input.prop('id');
var maxSize = $(this).data('max-size');
$uploadButton.click(function(){ $uploadButton.click(function(){
$uploadButton.prop('disabled', true); $uploadButton.prop('disabled', true);
var input = document.createElement('input'); var input = document.createElement('input');
@ -14,9 +15,20 @@ $.fn.imageUpload = function() {
input.onchange = function(e) { input.onchange = function(e) {
var reader = new FileReader() var reader = new FileReader()
// TODO: implement file size check
reader.readAsArrayBuffer(e.target.files[0]); reader.readAsArrayBuffer(e.target.files[0]);
reader.onprogress = function(e) {
if (e.loaded > maxSize) {
console.error('maximum file size exceeded, aborting file upload');
$uploadButton.prop('disabled', false);
reader.abort();
}
};
reader.onload = function(e) { reader.onload = function(e) {
if (e.loaded > maxSize) {
console.error('maximum file size exceeded, aborting file upload');
$uploadButton.prop('disabled', false);
return;
}
$.ajax({ $.ajax({
url: '/imageupload?id=' + id, url: '/imageupload?id=' + id,
type: 'POST', type: 'POST',

View File

@ -1,6 +1,10 @@
from datetime import datetime, timezone from datetime import datetime, timezone
class BodySizeError(Exception):
pass
class Controller(object): class Controller(object):
def __init__(self, handler, request, options): def __init__(self, handler, request, options):
self.handler = handler self.handler = handler
@ -33,10 +37,12 @@ class Controller(object):
self.handler.send_header("Location", location) self.handler.send_header("Location", location)
self.handler.end_headers() self.handler.end_headers()
def get_body(self): def get_body(self, max_size=None):
if "Content-Length" not in self.handler.headers: if "Content-Length" not in self.handler.headers:
return None return None
length = int(self.handler.headers["Content-Length"]) length = int(self.handler.headers["Content-Length"])
if max_size is not None and length > max_size:
raise BodySizeError("HTTP body exceeds maximum allowed size")
return self.handler.rfile.read(length) return self.handler.rfile.read(length)
def handle_request(self): def handle_request(self):

View File

@ -1,11 +1,20 @@
from owrx.controllers import BodySizeError
from owrx.controllers.assets import AssetsController from owrx.controllers.assets import AssetsController
from owrx.controllers.admin import AuthorizationMixin from owrx.controllers.admin import AuthorizationMixin
from owrx.config.core import CoreConfig from owrx.config.core import CoreConfig
from owrx.form.input.gfx import AvatarInput, TopPhotoInput
import uuid import uuid
import json import json
class ImageUploadController(AuthorizationMixin, AssetsController): class ImageUploadController(AuthorizationMixin, AssetsController):
# max upload filesizes
max_sizes = {
# not the best idea to instantiate inputs, but i didn't want to duplicate the sizes here
"receiver_avatar": AvatarInput("id", "label").getMaxSize(),
"receiver_top_photo": TopPhotoInput("id", "label").getMaxSize(),
}
def __init__(self, handler, request, options): def __init__(self, handler, request, options):
super().__init__(handler, request, options) super().__init__(handler, request, options)
self.file = request.query["file"][0] if "file" in request.query else None self.file = request.query["file"][0] if "file" in request.query else None
@ -29,22 +38,37 @@ class ImageUploadController(AuthorizationMixin, AssetsController):
def processImage(self): def processImage(self):
if "id" not in self.request.query: if "id" not in self.request.query:
self.send_response("{}", content_type="application/json", code=400) self.send_json_response({"error": "missing id"}, code=400)
# TODO: limit file size return
contents = self.get_body() file_id = self.request.query["id"][0]
if file_id not in ImageUploadController.max_sizes:
self.send_json_response({"error": "unexpected image id"}, code=400)
return
try:
contents = self.get_body(ImageUploadController.max_sizes[file_id])
except BodySizeError:
self.send_json_response({"error": "file size too large"}, code=400)
return
filetype = None filetype = None
if self._is_png(contents): if self._is_png(contents):
filetype = "png" filetype = "png"
if self._is_jpg(contents): if self._is_jpg(contents):
filetype = "jpg" filetype = "jpg"
if filetype is None: if filetype is None:
self.send_response("{}", content_type="application/json", code=400) self.send_json_response({"error": "unsupported file type"}, code=400)
else: return
self.file = "{id}-{uuid}.{ext}".format( self.file = "{id}-{uuid}.{ext}".format(
id=self.request.query["id"][0], id=file_id,
uuid=uuid.uuid4().hex, uuid=uuid.uuid4().hex,
ext=filetype, ext=filetype,
) )
with open(self.getFilePath(), "wb") as f: with open(self.getFilePath(), "wb") as f:
f.write(contents) f.write(contents)
self.send_response(json.dumps({"file": self.file}), content_type="application/json") self.send_json_response({"file": self.file}, code=200)
def send_json_response(self, obj, code):
self.send_response(json.dumps(obj), code=code, content_type="application/json")

View File

@ -7,7 +7,7 @@ class ImageInput(Input, metaclass=ABCMeta):
def render_input(self, value, errors): def render_input(self, value, errors):
# TODO display errors # TODO display errors
return """ return """
<div class="imageupload"> <div class="imageupload" data-max-size="{maxsize}">
<input type="hidden" id="{id}" name="{id}"> <input type="hidden" id="{id}" name="{id}">
<div class="image-container"> <div class="image-container">
<img class="{classes}" src="{url}" alt="{label}"/> <img class="{classes}" src="{url}" alt="{label}"/>
@ -16,7 +16,11 @@ class ImageInput(Input, metaclass=ABCMeta):
<button type="button" class="btn btn-secondary restore">Restore original image</button> <button type="button" class="btn btn-secondary restore">Restore original image</button>
</div> </div>
""".format( """.format(
id=self.id, label=self.label, url=self.cachebuster(self.getUrl()), classes=" ".join(self.getImgClasses()) id=self.id,
label=self.label,
url=self.cachebuster(self.getUrl()),
classes=" ".join(self.getImgClasses()),
maxsize=self.getMaxSize(),
) )
def cachebuster(self, url: str): def cachebuster(self, url: str):
@ -34,6 +38,10 @@ class ImageInput(Input, metaclass=ABCMeta):
def getImgClasses(self) -> list: def getImgClasses(self) -> list:
pass pass
@abstractmethod
def getMaxSize(self) -> int:
pass
class AvatarInput(ImageInput): class AvatarInput(ImageInput):
def getUrl(self) -> str: def getUrl(self) -> str:
@ -42,6 +50,10 @@ class AvatarInput(ImageInput):
def getImgClasses(self) -> list: def getImgClasses(self) -> list:
return ["webrx-rx-avatar"] return ["webrx-rx-avatar"]
def getMaxSize(self) -> int:
# 256 kB
return 250 * 1024
class TopPhotoInput(ImageInput): class TopPhotoInput(ImageInput):
def getUrl(self) -> str: def getUrl(self) -> str:
@ -49,3 +61,7 @@ class TopPhotoInput(ImageInput):
def getImgClasses(self) -> list: def getImgClasses(self) -> list:
return ["webrx-top-photo"] return ["webrx-top-photo"]
def getMaxSize(self) -> int:
# 2 MB
return 2 * 1024 * 1024