diff --git a/AUTHORS.rst b/AUTHORS.rst index 1b2e13f2..8837d8bc 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -1,6 +1,6 @@ .. This file is part of Invenio. - Copyright (C) 2015 CERN. + Copyright (C) 2015, 2016 CERN. Invenio is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as @@ -27,7 +27,8 @@ Authors Files download/upload REST API similar to S3 for Invenio. +- Jiri Kuncar - Jose Benito Gonzalez Lopez - Lars Holm Nielsen - Nicolas Harraudeau -- Jiri Kuncar +- Sami Hiltunen diff --git a/invenio_files_rest/__init__.py b/invenio_files_rest/__init__.py index 809a799d..cabfaf68 100644 --- a/invenio_files_rest/__init__.py +++ b/invenio_files_rest/__init__.py @@ -28,5 +28,6 @@ from .ext import InvenioFilesREST from .version import __version__ +from .views import current_files_rest -__all__ = ('__version__', 'InvenioFilesREST') +__all__ = ('__version__', 'current_files_rest', 'InvenioFilesREST', ) diff --git a/invenio_files_rest/config.py b/invenio_files_rest/config.py index 0ab17d36..717ed221 100644 --- a/invenio_files_rest/config.py +++ b/invenio_files_rest/config.py @@ -38,10 +38,20 @@ FILES_REST_DEFAULT_STORAGE_CLASS = 'S' """Default storage class.""" -FILES_REST_STORAGE_FACTORY = None +FILES_REST_DEFAULT_QUOTA_SIZE = None +"""Default quota size for a bucket in bytes.""" + +FILES_REST_DEFAULT_MAX_FILE_SIZE = None +"""Default maximum file size for a bucket in bytes.""" + +FILES_REST_SIZE_LIMITERS = 'invenio_files_rest.limiters.file_size_limiters' +"""Import path of file size limiters factory.""" + +FILES_REST_STORAGE_FACTORY = 'invenio_files_rest.storage.pyfs_storage_factory' """Import path of factory used to create a storage instance.""" -FILES_REST_PERMISSION_FACTORY = None +FILES_REST_PERMISSION_FACTORY = \ + 'invenio_files_rest.permissions.permission_factory' """Import path of permission factory.""" FILES_REST_OBJECT_KEY_MAX_LEN = 255 diff --git a/invenio_files_rest/errors.py b/invenio_files_rest/errors.py index d296f9cc..12000a4b 100644 --- a/invenio_files_rest/errors.py +++ b/invenio_files_rest/errors.py @@ -26,6 +26,8 @@ from __future__ import absolute_import, print_function +from invenio_rest.errors import RESTException + class FilesException(Exception): """Base exception for all errors .""" @@ -44,4 +46,10 @@ class FileInstanceAlreadySetError(FilesException): class InvalidOperationError(FilesException): - """Exception raise when an invalid operation is performed.""" + """Exception raised when an invalid operation is performed.""" + + +class FileSizeError(RESTException): + """Exception raised when a file larger than allowed.""" + + code = 400 diff --git a/invenio_files_rest/ext.py b/invenio_files_rest/ext.py index a6843af8..c9b63f53 100644 --- a/invenio_files_rest/ext.py +++ b/invenio_files_rest/ext.py @@ -31,7 +31,6 @@ from . import config from .cli import files as files_cmd -from .storage import pyfs_storage_factory class _FilesRESTState(object): @@ -58,43 +57,30 @@ def record_file_factory(self): @cached_property def storage_factory(self): """Load default storage factory.""" - imp = self.app.config.get("FILES_REST_STORAGE_FACTORY") - return import_string(imp) if imp else pyfs_storage_factory + return import_string(self.app.config.get('FILES_REST_STORAGE_FACTORY')) @cached_property def permission_factory(self): """Load default permission factory.""" - imp = self.app.config.get("FILES_REST_PERMISSION_FACTORY") - if imp: - return import_string(imp) - else: - from invenio_files_rest.permissions import permission_factory - return permission_factory + return import_string( + self.app.config.get('FILES_REST_PERMISSION_FACTORY')) @cached_property - def file_size_limiter(self): + def file_size_limiters(self): r"""Load the file size limiter. - The file size limiter is a function used to get the maximum size a file - can have. This function can use anything to decide this maximum size, - example: bucket quota, user quota, custom limit. + The file size limiter is a function used to get the file size limiters. + This function can use anything to limit the file size, for example: + bucket quota, user quota, custom limit. Its prototype is: - py::function: limiter(bucket=None\ - ) -> (size limit: int, reason: str) - The `reason` is the message displayed to the user when the limit is - exceeded. - The `size limit` and `reason` can be None if there is no limit. + py::function: limiter(bucket=None\ + ) -> [FileSizeLimit, FileSizeLimit, ...] - This function is used by the REST API and any other file creation - input. + An empty list should be returned if there should be no limit. The + lowest limit will be used. """ - imp = self.app.config.get("FILES_REST_FILE_SIZE_LIMITER") - if imp: - return import_string(imp) - else: - from invenio_files_rest.helpers import file_size_limiter - return file_size_limiter + return import_string(self.app.config.get('FILES_REST_SIZE_LIMITERS')) class InvenioFilesREST(object): diff --git a/invenio_files_rest/helpers.py b/invenio_files_rest/helpers.py index 6111fcc6..9de46e7f 100644 --- a/invenio_files_rest/helpers.py +++ b/invenio_files_rest/helpers.py @@ -85,15 +85,6 @@ def send_stream(stream, filename, size, mtime, mimetype=None, restricted=False, return rv -def file_size_limiter(bucket): - """Retrieve the internal quota from the provided bucket.""" - if bucket.quota_size: - return (bucket.quota_size - bucket.size, - 'Bucket quota is {0} bytes. {1} bytes are currently ' - 'used.'.format(bucket.quota_size, bucket.size)) - return (None, None) - - def compute_md5_checksum(src, chunk_size=None, progress_callback=None): """Helper method to compute checksum from a stream. diff --git a/invenio_files_rest/limiters.py b/invenio_files_rest/limiters.py new file mode 100644 index 00000000..1e2897cb --- /dev/null +++ b/invenio_files_rest/limiters.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# Copyright (C) 2016 CERN. +# +# Invenio is free software; you can redistribute it +# and/or modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# Invenio is distributed in the hope that it will be +# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Invenio; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, +# MA 02111-1307, USA. +# +# In applying this license, CERN does not +# waive the privileges and immunities granted to it by virtue of its status +# as an Intergovernmental Organization or submit itself to any jurisdiction. + +"""File size limiting functionality for Invenio-Files-REST.""" + +from __future__ import absolute_import, print_function + + +def file_size_limiters(bucket): + """Default file size limiters.""" + return [ + FileSizeLimit( + bucket.quota_left, + 'Bucket quota exceeded.', + ), + FileSizeLimit( + bucket.max_file_size, + 'Maximum file size exceeded.', + ), + ] + + +class FileSizeLimit(object): + """File size limiter.""" + + not_implemented_error = NotImplementedError( + 'FileSizeLimit supports only comparisons with integers and other ' + 'FileSizeLimits.') + + def __init__(self, limit, reason): + """Instantiate a new file size limit.""" + self.limit = limit + self.reason = reason + + def __lt__(self, other): + """Check if this limit is less than the other one.""" + if isinstance(other, int): + return self.limit < other + elif isinstance(other, FileSizeLimit): + return self.limit < other.limit + raise not_implemented_error + + def __gt__(self, other): + """Check if this limit is greater than the other one.""" + if isinstance(other, int): + return self.limit > other + elif isinstance(other, FileSizeLimit): + return self.limit > other.limit + raise not_implemented_error + + def __eq__(self, other): + """Check for equality.""" + if isinstance(other, int): + return self.limit == other + elif isinstance(other, FileSizeLimit): + return self.limit == other.limit + raise not_implemented_error diff --git a/invenio_files_rest/models.py b/invenio_files_rest/models.py index 6168fe75..8a91d6b0 100644 --- a/invenio_files_rest/models.py +++ b/invenio_files_rest/models.py @@ -62,6 +62,8 @@ from sqlalchemy_utils.types import UUIDType from .errors import FileInstanceAlreadySetError, InvalidOperationError +from .limiters import FileSizeLimit +from .proxies import current_files_rest slug_pattern = re.compile('^[a-z][a-z0-9-]+$') @@ -192,13 +194,19 @@ class Bucket(db.Model, Timestamp): inside the bucket. """ - quota_size = db.Column(db.BigInteger, nullable=True) - """Quota size of bucket. Used only by the file_size_limiter. + quota_size = db.Column( + db.BigInteger, + nullable=True, + default=lambda: current_app.config['FILES_REST_DEFAULT_QUOTA_SIZE'] + ) + """Quota size of bucket.""" - Note, don't use this attribute directly. It MAY be used to store - the actual quota size for this bucket. Change the file_size_limiter if - you want to filter accepted files based on their size. - """ + max_file_size = db.Column( + db.BigInteger, + nullable=True, + default=lambda: current_app.config['FILES_REST_DEFAULT_MAX_FILE_SIZE'] + ) + """Maximum size of a single file in the bucket.""" locked = db.Column(db.Boolean, default=False, nullable=False) """Is bucket locked?""" @@ -213,6 +221,12 @@ def __repr__(self): """Return representation of location.""" return str(self.id) + @property + def quota_left(self): + """Get how much space is left in the bucket.""" + if self.quota_size: + return max(self.quota_size - self.size, 0) + @validates('default_storage_class') def validate_storage_class(self, key, default_storage_class): """Validate storage class.""" @@ -453,8 +467,7 @@ def storage(self, **kwargs): :returns: Storage interface. """ - return current_app.extensions['invenio-files-rest'].storage_factory( - fileinstance=self, **kwargs) + return current_files_rest.storage_factory(fileinstance=self, **kwargs) def verify_checksum(self, progress_callback=None, **kwargs): """Verify checksum of file instance.""" @@ -465,7 +478,7 @@ def verify_checksum(self, progress_callback=None, **kwargs): self.last_check_at = datetime.utcnow() return self.last_check - def set_contents(self, stream, chunk_size=None, + def set_contents(self, stream, chunk_size=None, size=None, size_limit=None, progress_callback=None, **kwargs): """Save contents of stream to this file. @@ -477,8 +490,8 @@ def set_contents(self, stream, chunk_size=None, raise ValueError('File instance is not writable.') self.set_uri( *self.storage(**kwargs).save( - stream, chunk_size=chunk_size, - progress_callback=progress_callback)) + stream, chunk_size=chunk_size, size=size, + size_limit=size_limit, progress_callback=progress_callback)) def copy_contents(self, fileinstance, progress_callback=None, **kwargs): """Copy this file instance into another file instance.""" @@ -599,7 +612,7 @@ def is_deleted(self): """Determine if object version is a delete marker.""" return self.file_id is None - def set_contents(self, stream, size=None, chunk_size=None, + def set_contents(self, stream, size_limit=None, size=None, chunk_size=None, progress_callback=None): """Save contents of stream to file instance. @@ -614,9 +627,17 @@ def set_contents(self, stream, size=None, chunk_size=None, if self.file_id is not None: raise FileInstanceAlreadySetError() + if size_limit is None: + limits = [ + lim for lim in current_files_rest.file_size_limiters( + self.bucket) + if lim.limit is not None + ] + size_limit = min(limits) if limits else None + self.file = FileInstance.create() self.file.set_contents( - stream, size=size, chunk_size=chunk_size, + stream, size_limit=size_limit, size=size, chunk_size=chunk_size, progress_callback=progress_callback, objectversion=self) self.bucket.size += self.file.size @@ -753,18 +774,18 @@ def get(cls, bucket, key, version_id=None): """ bucket_id = bucket.id if isinstance(bucket, Bucket) else bucket - args = [ + filters = [ cls.bucket_id == bucket_id, cls.key == key, ] if version_id: - args.append(cls.version_id == version_id) + filters.append(cls.version_id == version_id) else: - args.append(cls.is_head.is_(True)) - args.append(cls.file_id.isnot(None)) + filters.append(cls.is_head.is_(True)) + filters.append(cls.file_id.isnot(None)) - return cls.query.filter(*args).one_or_none() + return cls.query.filter(*filters).one_or_none() @classmethod def get_versions(cls, bucket, key): @@ -775,12 +796,12 @@ def get_versions(cls, bucket, key): """ bucket_id = bucket.id if isinstance(bucket, Bucket) else bucket - args = [ + filters = [ cls.bucket_id == bucket_id, cls.key == key, ] - return cls.query.filter(*args).order_by(cls.key, cls.created.desc()) + return cls.query.filter(*filters).order_by(cls.key, cls.created.desc()) @classmethod def delete(cls, bucket, key): @@ -803,15 +824,15 @@ def get_by_bucket(cls, bucket, versions=False): """Return query that fetches all the objects in a bucket.""" bucket_id = bucket.id if isinstance(bucket, Bucket) else bucket - args = [ + filters = [ cls.bucket_id == bucket_id, ] if not versions: - args.append(cls.file_id.isnot(None)) - args.append(cls.is_head.is_(True)) + filters.append(cls.file_id.isnot(None)) + filters.append(cls.is_head.is_(True)) - return cls.query.filter(*args).order_by(cls.key, cls.created.desc()) + return cls.query.filter(*filters).order_by(cls.key, cls.created.desc()) @classmethod def relink_all(cls, old_file, new_file): diff --git a/invenio_files_rest/permissions.py b/invenio_files_rest/permissions.py index b0965f7a..9d355f8e 100644 --- a/invenio_files_rest/permissions.py +++ b/invenio_files_rest/permissions.py @@ -51,7 +51,7 @@ _action2need_map = { 'objects-read': ObjectsRead, 'objects-update': ObjectsUpdate, - 'objects-delete': ObjectsRead, + 'objects-delete': ObjectsDelete, } diff --git a/invenio_files_rest/proxies.py b/invenio_files_rest/proxies.py index 1a97f7fb..26c99a91 100644 --- a/invenio_files_rest/proxies.py +++ b/invenio_files_rest/proxies.py @@ -31,3 +31,6 @@ current_permission_factory = LocalProxy( lambda: current_app.extensions['invenio-files-rest'].permission_factory) + +current_files_rest = LocalProxy( + lambda: current_app.extensions['invenio-files-rest']) diff --git a/invenio_files_rest/storage.py b/invenio_files_rest/storage.py index 7dc3f72c..3d04bd0e 100644 --- a/invenio_files_rest/storage.py +++ b/invenio_files_rest/storage.py @@ -33,7 +33,7 @@ from fs.opener import opener -from .errors import StorageError, UnexpectedFileSizeError +from .errors import FileSizeError, StorageError, UnexpectedFileSizeError from .helpers import compute_md5_checksum, send_stream @@ -108,8 +108,8 @@ def _compute_checksum(self, src, chunk_size=None, progress_callback=None): raise StorageError( 'Could not compute checksum of file: {}'.format(e)) - def _write_stream(self, src, dst, size=None, chunk_size=None, - progress_callback=None): + def _write_stream(self, src, dst, size_limit=None, size=None, + chunk_size=None, progress_callback=None): """Helper method to save stream from src to dest + compute checksum.""" chunk_size = chunk_size or 1024 * 64 @@ -117,9 +117,11 @@ def _write_stream(self, src, dst, size=None, chunk_size=None, bytes_written = 0 while 1: - chunk = src.read(chunk_size) + if size_limit is not None and bytes_written > size_limit: + raise FileSizeError(description=size_limit.reason) if size is not None and bytes_written > size: raise UnexpectedFileSizeError('File is bigger than expected.') + chunk = src.read(chunk_size) if not chunk: if progress_callback: progress_callback(bytes_written, bytes_written) @@ -168,8 +170,8 @@ def open(self): """ return opener.open(self.file.uri, mode='rb') - def save(self, incoming_stream, size=None, chunk_size=None, - progress_callback=None): + def save(self, incoming_stream, size_limit=None, size=None, + chunk_size=None, progress_callback=None): """Save file in the file system.""" fs = opener.opendir(self.file.uri or self.make_path(), create_dir=True) fp = fs.open(self.filename, 'wb') @@ -177,7 +179,7 @@ def save(self, incoming_stream, size=None, chunk_size=None, bytes_written, checksum = self._write_stream( incoming_stream, fp, chunk_size=chunk_size, progress_callback=progress_callback, - size=size) + size_limit=size_limit, size=size) finally: fp.close() diff --git a/invenio_files_rest/views.py b/invenio_files_rest/views.py index e34f65c8..629de704 100644 --- a/invenio_files_rest/views.py +++ b/invenio_files_rest/views.py @@ -37,9 +37,9 @@ from webargs.flaskparser import parser, use_kwargs from werkzeug.local import LocalProxy -from .errors import UnexpectedFileSizeError +from .errors import FileSizeError, UnexpectedFileSizeError from .models import Bucket, Location, ObjectVersion -from .proxies import current_permission_factory +from .proxies import current_files_rest, current_permission_factory from .serializer import json_serializer from .signals import file_downloaded @@ -49,9 +49,6 @@ url_prefix='/files' ) -current_files_rest = LocalProxy( - lambda: current_app.extensions['invenio-files-rest']) - def file_download_ui(pid, record, **kwargs): """File download view for a given record. @@ -429,10 +426,6 @@ class ObjectResource(ContentNegotiatedMethodView): """GET query arguments.""" put_args = dict( - content_length=fields.Int( - load_from='Content-Length', - location='headers', - required=True), content_md5=fields.Str( load_from='Content-MD5', location='headers', ), @@ -515,7 +508,7 @@ def get(self, bucket_id, key, version_id=None, **kwargs): return self.send_object(bucket_id, key, version_id=version_id) @use_kwargs(put_args) - def put(self, bucket_id, key, content_length=None, content_md5=None): + def put(self, bucket_id, key, content_md5=None): """Upload object file. .. http:put:: /files/(uuid:bucket_id) @@ -583,36 +576,25 @@ def put(self, bucket_id, key, content_length=None, content_md5=None): abort(403) abort(401) - # check content size limit - size_limit, size_limit_reason = current_files_rest.file_size_limiter( - bucket=bucket) - if size_limit is not None and content_length > size_limit: - abort(400, size_limit_reason) - # TODO: Check access permission on the bucket # TODO: Support checking incoming MD5 header # TODO: Support setting content-type # TODO: Don't create a new file if content is identical. - try: - # TODO: Pass storage class to get_or_create + # TODO: Pass storage class to get_or_create + with db.session.begin_nested(): obj = ObjectVersion.create(bucket, key) - obj.set_contents(uploaded_file, size=content_length) - db.session.commit() + obj.set_contents(uploaded_file) + db.session.commit() - # TODO: Fix response object to only include headers? - return {'json': { + # TODO: Fix response object to only include headers? + return { + 'json': { 'checksum': obj.file.checksum, 'size': obj.file.size, - 'verisionId': str(obj.version_id), - }} - except SQLAlchemyError: - db.session.rollback() - current_app.logger.exception('Failed to create object.') - abort(500, 'Failed to create object.') - except UnexpectedFileSizeError: - db.session.rollback() - abort(400, 'File size different than Content-Length') + 'versionId': str(obj.version_id), + } + } def delete(self, bucket_id, key, **kwargs): """Set object file as deleted. diff --git a/run-tests.sh b/run-tests.sh index 8a87f678..01be6f8d 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -1,3 +1,4 @@ +#!/usr/bin/env sh # -*- coding: utf-8 -*- # # This file is part of Invenio. diff --git a/tests/conftest.py b/tests/conftest.py index b59cd445..33f493bc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -162,7 +162,7 @@ def objects(db, bucket): data_bytes2 = b('readme file') obj2 = ObjectVersion.create( bucket, 'README.rst', stream=BytesIO(data_bytes2), - size=len(data_bytes) + size=len(data_bytes2) ) db.session.commit() diff --git a/tests/test_helpers.py b/tests/test_helpers.py deleted file mode 100644 index d2d29217..00000000 --- a/tests/test_helpers.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# Copyright (C) 2016 CERN. -# -# Invenio is free software; you can redistribute it -# and/or modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 2 of the -# License, or (at your option) any later version. -# -# Invenio is distributed in the hope that it will be -# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Invenio; if not, write to the -# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, -# MA 02111-1307, USA. -# -# In applying this license, CERN does not -# waive the privileges and immunities granted to it by virtue of its status -# as an Intergovernmental Organization or submit itself to any jurisdiction. - -"""Helpers for invenio files REST's tests.""" - -from __future__ import absolute_import, print_function - -CONSTANT_FILE_SIZE_LIMIT = 120 -"""File size limit provided by the constant_file_size_limiter.""" - - -def constant_file_size_limiter(bucket): - """Provide always the same file size limit.""" - return (CONSTANT_FILE_SIZE_LIMIT, 'Constant file size limit is exceeded') diff --git a/tests/test_views.py b/tests/test_views.py index 051dedd3..3b08c2cc 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -27,11 +27,12 @@ from __future__ import absolute_import, print_function +from struct import pack + import pytest from flask import json, url_for from invenio_db import db from six import BytesIO, b -from test_helpers import CONSTANT_FILE_SIZE_LIMIT from invenio_files_rest import InvenioFilesREST from invenio_files_rest.views import blueprint @@ -190,18 +191,13 @@ def test_get_object_permissions(app, objects, bucket, users_data, permissions): assert resp.get_etag()[0] == obj.file.checksum -@pytest.mark.parametrize('limiter, bucket_quota, file_size_limit', [ - # use the default file size limiter - (None, 50, 50), - # internal quota is not used by the constant_file_size_limiter - ('test_helpers:constant_file_size_limiter', CONSTANT_FILE_SIZE_LIMIT + 2, - CONSTANT_FILE_SIZE_LIMIT), +@pytest.mark.parametrize('bucket_quota, file_size_limit', [ + (50, 50), + (122, 120), ]) -def test_put_object(limiter, bucket_quota, file_size_limit, base_app, objects, +def test_put_object(bucket_quota, file_size_limit, base_app, objects, bucket, users_data, permissions): """Test ObjectResource view PUT method.""" - if limiter is not None: - base_app.config['FILES_REST_FILE_SIZE_LIMITER'] = limiter InvenioFilesREST(base_app) base_app.register_blueprint(blueprint) @@ -220,7 +216,7 @@ def test_put_object(limiter, bucket_quota, file_size_limit, base_app, objects, # Get object that doesn't exist. Gets the "401 Unauthorized" before 404 # Try to update the file under 'key' (with 'contents2') data_bytes = b'contents2' - headers = {'Accept': '*/*', 'Content-Length': str(len(data_bytes))} + headers = {'Accept': '*/*'} data = {'file': (BytesIO(data_bytes), 'file.dat')} resp = client.put(object_url, data=data, headers=headers) assert resp.status_code == 401 @@ -231,52 +227,48 @@ def test_put_object(limiter, bucket_quota, file_size_limit, base_app, objects, resp = client.put(object_url, data=data, headers=headers) assert resp.status_code == 403 - # FIXME: check that the file is not created + +@pytest.mark.parametrize( + 'quota_size, max_file_size, file_size, expected', [ + (None, None, 100, (200, '')), + (50, None, 100, (400, 'Bucket quota exceeded.')), + (100, None, 100, (200, '')), + (150, None, 100, (200, '')), + (None, 50, 100, (400, 'Maximum file size exceeded.')), + (None, 100, 100, (200, '')), + (None, 150, 100, (200, '')), + ]) +def test_file_size_errors(quota_size, max_file_size, file_size, expected, + base_app, users_data, permissions, bucket): + """Test that file size errors are properly raised.""" + InvenioFilesREST(base_app) + base_app.register_blueprint(blueprint) + key = 'file.dat' + user = users_data[0] + login_url = url_for('security.login') + object_url = "/files/{0}/{1}".format(bucket.id, key) + + with base_app.app_context(): + bucket.quota_size = quota_size + bucket.max_file_size = max_file_size + db.session.merge(bucket) + db.session.commit() with base_app.test_client() as client: # Login with 'user1' (has permissions) - client.post(login_url, data=u1_data) - # Test with an mismatching Content-Length - headers = {'Accept': '*/*', 'Content-Length': str(len(data_bytes) - 1)} - data = {'file': (BytesIO(data_bytes), 'mismatching1.dat')} + client.post(login_url, data=user) + + content = pack( + ''.join('c' for i in range(file_size)), + *[b'v' for i in range(file_size)]) + headers = { + 'Accept': '*/*', + } + data = {'file': (BytesIO(content), key)} resp = client.put(object_url, data=data, headers=headers) - assert resp.status_code == 400 + assert resp.status_code == expected[0] + assert expected[1] in resp.get_data(as_text=True) - headers = {'Accept': '*/*', 'Content-Length': str(len(data_bytes) + 1)} - data = {'file': (BytesIO(data_bytes), 'mismatching2.dat')} - resp = client.put(object_url, data=data, headers=headers) - assert resp.status_code == 400 - - # FIXME: check that the file is not created - - # Test exceeding the file size limit - data_bytes2 = b(''.join('a' for i in - range(file_size_limit + 1))) - headers = {'Accept': '*/*', 'Content-Length': str(len(data_bytes2))} - data = {'file': (BytesIO(data_bytes2), 'exceeding.dat')} - resp = client.put(object_url, data=data, headers=headers) - assert resp.status_code == 400 - - # FIXME: check that the file is not created - - # if the default limiter is used - if limiter is None: - # set the bucket quota to unlimited - with base_app.app_context(): - bucket.quota_size = None - db.session.merge(bucket) - db.session.commit() - # Test again with the precedly exceeding size - with base_app.test_client() as client: - # Login with 'user1' (has permissions) - client.post(login_url, data=u1_data) - data_bytes2 = b(''.join('a' for i in - range(file_size_limit + 1))) - headers = {'Accept': '*/*', - 'Content-Length': str(len(data_bytes2))} - data = {'file': (BytesIO(data_bytes2), 'exceeding.dat')} - resp = client.put(object_url, data=data, headers=headers) - assert resp.status_code == 200 # def test_get_object_get_access_denied_403(app, objects): # """Test object download 403 access denied"""