From 5570c77c2c663cccee0a81d14cd8a81c9630a28e Mon Sep 17 00:00:00 2001 From: Chris Topaloudis Date: Mon, 27 May 2019 10:16:36 +0200 Subject: [PATCH] docs: usage - overview structure and content - usage structure and content - added `BucketTag` in `__all__` of `models` so its documentation shows up - closes #198 Co-authored-by: Vlad Mihaescu Co-authored-by: Nicola Tarocco --- INSTALL.rst | 6 + MANIFEST.in | 9 +- README.rst | 15 +- docs/_static/InvenioFIlesREST.png | Bin 0 -> 49178 bytes docs/conf.py | 4 +- docs/exampleapp.rst | 6 +- docs/index.rst | 1 + docs/overview.rst | 136 +++ docs/usage.rst | 3 +- examples/app.py | 7 +- invenio_files_rest/__init__.py | 1457 ++++++++++++++++++++++++++++- invenio_files_rest/models.py | 5 +- 12 files changed, 1628 insertions(+), 21 deletions(-) create mode 100644 docs/_static/InvenioFIlesREST.png create mode 100644 docs/overview.rst diff --git a/INSTALL.rst b/INSTALL.rst index 7f12cc4c..3d3bac5c 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -7,3 +7,9 @@ Installation ============ + +Invenio-Files-REST is on PyPI so all you need is: + +.. code-block:: console + + $ pip install invenio-files-rest diff --git a/MANIFEST.in b/MANIFEST.in index 29eff874..0588b4cd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,17 +6,18 @@ # Invenio is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. -include *.rst -include *.sh -include *.txt include .dockerignore include .editorconfig include .tx/config -include LICENSE +include *.rst +include *.sh +include *.txt include babel.ini include docs/requirements.txt +include LICENSE include pytest.ini recursive-include docs *.bat +recursive-include docs *.png recursive-include docs *.py recursive-include docs *.rst recursive-include docs Makefile diff --git a/README.rst b/README.rst index 7c483f88..f753a269 100644 --- a/README.rst +++ b/README.rst @@ -23,9 +23,16 @@ :target: https://github.com/inveniosoftware/invenio-files-rest/blob/master/LICENSE -Files download/upload REST API similar to S3 for Invenio. +Invenio-Files-REST is a files storage module. It allows you to store and +retrieve files in a similar way to Amazon S3 APIs. -*This is an experimental developer preview release.* +Features: -* Free software: MIT license -* Documentation: https://invenio-files-rest.readthedocs.io/ + * Files storage with configurable storage backends + * Secure REST APIs similar to Amazon S3 + * Support for large file uploads and multipart upload. + * Customizable access control + * File integrity monitoring + + +Further documentation is available on https://invenio-files-rest.readthedocs.io/. diff --git a/docs/_static/InvenioFIlesREST.png b/docs/_static/InvenioFIlesREST.png new file mode 100644 index 0000000000000000000000000000000000000000..b9b2a682286c1aaa1fc09cf2cfe2db3a23dea7c4 GIT binary patch literal 49178 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 which offers integration +with any S3 REST API compatible object storage. + +See the API section of :py:class:`invenio_files_rest.storage` for more +information. + + +FileInstance +------------ +Files on disk are represented with :code:`FileInstance`. A file instance +records the path to the file, but also its `Storage`, size and checksum of the +file on disk. + +See the API section of :py:class:`invenio_files_rest.models.FileInstance` for +more information. + + +Object +------ +An :code:`Object` is an abstract representation of the file metadata: it +doesn't come with its own data model but it is defined by +:code:`ObjectVersion`. + + +ObjectVersion +------------- +An :code:`ObjectVersion` represents a version of an :code:`Object` at a +specific point in time. It contains the :code:`FileInstance` that describes and +a set of metadata. + +When it has no :code:`FileInstance`, it marks a deletion of a file as a delete +marker (or soft deletion). + +The latest version of the file is referred to as the :code:`HEAD`. + +Object version are very useful to perform operation on files metadata without +accessing directly to the storage. For example, multiple :code:`ObjectVersion` +can point to the same :code:`FileInstance`, allowing operations to be +performed more efficiently, such as snapshots without duplicating files or +migrating data. + +See the API section of :py:class:`invenio_files_rest.models.ObjectVersion` for +more information. + + +ObjectVersionTag +---------------- + +:code:`ObjectVersionTag` is useful to store extra information for an +:code:`ObjectVersion`. + +A :code:`ObjectVersionTag` is in the form of :code:`key: value` pair and an +:code:`ObjectVersion` can have multiple :code:`ObjectVersionTag`. + +See the API section of +:py:class:`invenio_files_rest.models.ObjectVersionTag` for more information. + + +Bucket +------ +Consider the :code:`Bucket` as a container for :code:`ObjectVersion` objects. +Just as in a computer, files are contained inside folders, each +:code:`ObjectVersion` has to be contained in a :code:`Bucket`. + +The :code:`Bucket` is identified by an unique ID and is created in a +given :code:`Location` with a given :code:`Storage`. + +:code:`ObjectVersion` are uniquely identified within a bucket by string keys. + +.. .note:: + + :code:`Objects` inside a :code:`Bucket` do not necessarily have the same + :code:`Location` or :code:`Storage` class as the :code:`Bucket`. + +A bucket can also be marked as deleted, in which case the contents become +inaccessible. It can also be permanently removed: in this case, all contained :code:`ObjectVersions` will be also deleted. + +See the API section of :py:class:`invenio_files_rest.models.Bucket` for more +information. + + +BucketTag +--------- +Similarly to :code:`ObjectVersionTag`, a :code:`BucketTag` is useful to store +extra information for a :code:`Bucket`. + +A :code:`BucketTag` is in the form of :code:`key: value` pair and a +:code:`Bucket` can have multiple :code:`BucketTag`. + +See the API section of :py:class:`invenio_files_rest.models.BucketTag` for +more information. diff --git a/docs/usage.rst b/docs/usage.rst index 0d697b2d..5ea2481e 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -6,8 +6,7 @@ under the terms of the MIT License; see LICENSE file for more details. - Usage -======= +===== .. automodule:: invenio_files_rest diff --git a/examples/app.py b/examples/app.py index b06058e6..6ff5180d 100644 --- a/examples/app.py +++ b/examples/app.py @@ -6,7 +6,7 @@ # Invenio is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. -"""Minimal Flask application example for development. +r"""Minimal Flask application example for development. SPHINX-START @@ -170,13 +170,13 @@ import os import shutil -from os import environ, makedirs +from os import makedirs from os.path import dirname, exists, join from flask import Flask, current_app from flask_babelex import Babel from flask_menu import Menu -from invenio_access import ActionSystemRoles, InvenioAccess, any_user +from invenio_access import InvenioAccess from invenio_accounts import InvenioAccounts from invenio_accounts.views import blueprint as accounts_blueprint from invenio_admin import InvenioAdmin @@ -192,6 +192,7 @@ def allow_all(*args, **kwargs): """Return permission that always allow an access. + :returns: A object instance with a ``can()`` method. """ return type('Allow', (), {'can': lambda self: True})() diff --git a/invenio_files_rest/__init__.py b/invenio_files_rest/__init__.py index cb82555a..b069a5c6 100644 --- a/invenio_files_rest/__init__.py +++ b/invenio_files_rest/__init__.py @@ -6,7 +6,1462 @@ # Invenio is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. -"""Files download/upload REST API similar to S3 for Invenio.""" + +r"""REST API for Files. + +Invenio-Files-REST provides configurable REST APIs for uploading, serving, +downloading and deleting files. It works as a standalone module or in +combination with `Invenio-Records `_ +through the +`Invenio-Records-Files `_ +integration. + +The module can be configured with different storage backends, and provides +features such as: + +- A robust REST API. +- Configurable storage backends with the ability to build your very own. +- Highly customizable access-control. +- Secure file handling. +- Integrity checking mechanism. +- Support for large file uploads and multipart upload. +- Signals for system events. + +The REST API follows best practices and supports, e.g.: + +- Content negotiation and links headers. +- Cache control via ETags and Last-Modified headers. +- Optimistic concurrency control via ETags. +- Rate-limiting, Cross-Origin Resource Sharing, and various security headers. + + +Initialization +-------------- + +First, let's create a Flask application: + +>>> from flask import Flask +>>> app = Flask('myapp') + +And add some configuration, mainly for storage: + +>>> app.config['BROKER_URL'] = 'redis://' +>>> app.config['CELERY_RESULT_BACKEND'] = 'redis://' +>>> app.config['DATADIR'] = 'data' +>>> app.config['FILES_REST_MULTIPART_CHUNKSIZE_MIN'] = 4 +>>> app.config['REST_ENABLE_CORS'] = True +>>> app.config['SECRET_KEY'] = 'CHANGEME' +>>> app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' +>>> app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +>>> allow_all = lambda *args, **kwargs: \ +... type('Allow', (), {'can': lambda self: True})() +>>> app.config['FILES_REST_PERMISSION_FACTORY'] = allow_all + +Now let's initialize all required Invenio extensions: + +>>> import shutil +>>> from os import makedirs +>>> from os.path import dirname, exists +>>> from pprint import pprint +>>> import json + +>>> from flask_babelex import Babel +>>> from flask_menu import Menu +>>> from invenio_db import InvenioDB, db +>>> from invenio_rest import InvenioREST +>>> from invenio_admin import InvenioAdmin +>>> from invenio_accounts import InvenioAccounts +>>> from invenio_access import InvenioAccess +>>> from invenio_accounts.views import blueprint as accounts_blueprint +>>> from invenio_celery import InvenioCelery +>>> from invenio_files_rest import InvenioFilesREST +>>> from invenio_files_rest.views import blueprint +>>> from invenio_files_rest.models import Location + +>>> ext_babel = Babel(app) +>>> ext_menu = Menu(app) +>>> ext_db = InvenioDB(app) +>>> ext_rest = InvenioREST(app) +>>> ext_admin = InvenioAdmin(app) +>>> ext_accounts = InvenioAccounts(app) +>>> ext_access = InvenioAccess(app) + +Finally, let's initialize InvenioFilesREST, register the blueprints +and push a Flask application context: + +>>> ext_rest = InvenioFilesREST(app) + +>>> app.register_blueprint(accounts_blueprint) +>>> app.register_blueprint(blueprint) + +>>> app.app_context().push() + +Let's create the database and tables, using an in-memory SQLite database: + +>>> db.create_all() + +To start storing file, let's create a location in a temporary directory: + +>>> srcroot = dirname(dirname('app.py')) +>>> d = app.config['DATADIR'] +>>> if exists(d): shutil.rmtree(d) +>>> makedirs(d) +>>> loc = Location(name='local', uri=d, default=True) +>>> db.session.add(loc) +>>> db.session.commit() + +Now let's create a bucket: + +>>> res = app.test_client().post('/files') + +And see the response containing the id of the bucket: + +>>> json_response = json.loads(res.get_data().decode("utf-8")) + + +REST API +-------- + +This part of the documentation will show you how to get started in using the +REST API of Invenio-Files-REST. + +The REST API allows you to create buckets and perform CRUD operations on files. +You can use query parameters in order to perform these operations. + +.. note:: + The REST APIs endpoint is registered by the Invenio API instance. This + means that the endpoint is reachable with the path ``/api/files/``. + + +Available methods and endpoints +------------------------------- + +The following is a brief overview of the methods the REST API provides. + +By default, the URL prefix for the REST API is under /files. + + +Bucket Endpoints +---------------- + +Create +^^^^^^ + +**Description** + Creates a bucket in the default location. + +**Parameters** + +.. code-block:: console + + No parameters. + +**Request** + +.. code-block:: console + + POST /files + +**Response** + +.. code-block:: json + + { + "max_file_size": null, + "updated": "2019-05-24T08:59:40.356202+00:00", + "locked": false, + "links": { + "self": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794", + "uploads": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794?uploads", + "versions": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794?versions" + }, + "created": "2019-05-24T08:59:40.356195+00:00", + "quota_size": null, + "id": "436ac279-d85f-4500-8217-295804c14794", + "size": 0 + } + + +Check existance +^^^^^^^^^^^^^^^ + +**Description** + Checks if a bucket exists. + +**Parameters** + +.. code-block:: console + + No parameters. + +**Request** + +.. code-block:: console + + HEAD /files/ + +**Response** + +.. code-block:: console + + Status: 200 OK + +**Errors** + +.. code-block:: console + + Status: 404 NOT FOUND + + +List files +^^^^^^^^^^ + +**Description** + Returns list of all of the files in the specified bucket. + +**Parameters** + +.. code-block:: console + + No parameters + +**Request** + +.. code-block:: console + + GET /files/ + +**Response** + +Example with no files in the bucket: + +.. code-block:: json + + { + "max_file_size": null, + "updated": "2019-05-24T08:59:40.356202+00:00", + "locked": false, + "links": { + "self": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794", + "uploads": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794?uploads", + "versions": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794?versions" + }, + "created": "2019-05-24T08:59:40.356195+00:00", + "quota_size": null, + "id": "436ac279-d85f-4500-8217-295804c14794", + "contents": [], + "size": 0 + } + +Example with one file in the bucket: + +.. code-block:: json + + { + "max_file_size": null, + "updated": "2019-05-24T09:20:36.361338+00:00", + "locked": false, + "links": { + "self": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794", + "uploads": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794?uploads", + "versions": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794?versions" + }, + "created": "2019-05-24T08:59:40.356195+00:00", + "quota_size": null, + "id": "436ac279-d85f-4500-8217-295804c14794", + "contents": [ + { + "mimetype": "text/plain", + "updated": "2019-05-24T09:20:36.344541+00:00", + "links": { + "self": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/example.txt", + "version": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/example.txt? + versionId=39075b38-b354-4ce9-bd36-2425495e6a7a", + "uploads": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/example.txt? + uploads" + }, + "is_head": true, + "tags": {}, + "checksum": "md5:2cad20c19a8eb9bb11a9f76527aec9bc", + "created": "2019-05-24T09:20:36.341621+00:00", + "version_id": "39075b38-b354-4ce9-bd36-2425495e6a7a", + "delete_marker": false, + "key": "example.txt", + "size": 12 + } + ], + "size": 12 + } + +**Errors** + +.. code-block:: json + + { + "message":"Bucket does not exist.", + "status":404 + } + + +Working with files: +------------------- + +Upload files +^^^^^^^^^^^^ + +**Description** + Uploads a file. + +**Parameters** + +.. code-block:: console + + binary: + +**Request** + +.. code-block:: console + + PUT /files// + +**Response** + +.. code-block:: json + + { + "mimetype": "text/plain", + "updated": "2019-05-24T09:20:36.344541+00:00", + "links": { + "self": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/example.txt", + "version": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/example.txt? + versionId=39075b38-b354-4ce9-bd36-2425495e6a7a", + "uploads": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/example.txt?uploads" + }, + "is_head": true, + "tags": {}, + "checksum": "md5:2cad20c19a8eb9bb11a9f76527aec9bc", + "created": "2019-05-24T09:20:36.341621+00:00", + "version_id": "39075b38-b354-4ce9-bd36-2425495e6a7a", + "delete_marker": false, + "key": "example.txt", + "size": 12 + } + + +List file versions +^^^^^^^^^^^^^^^^^^ + +**Description** + Returns a list of all versions of all files in the bucket. + +**Parameters** + +.. code-block:: console + + No parameters. + +**Request** + +.. code-block:: console + + GET /files/?versions + +**Response** + +Example with two files (one with two versions): + +.. code-block:: json + + { + "max_file_size": null, + "updated": "2019-05-24T10:08:34.174650+00:00", + "locked": false, + "links": { + "self": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794", + "uploads": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794?uploads", + "versions": "http://localhost:5000/files/4 + 36ac279-d85f-4500-8217-295804c14794?versions" + }, + "created": "2019-05-24T08:59:40.356195+00:00", + "quota_size": null, + "id": "436ac279-d85f-4500-8217-295804c14794", + "contents": [ + { + "mimetype": "text/plain", + "updated": "2019-05-24T09:58:11.907546+00:00", + "links": { + "self": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/example.txt", + "version": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/example.txt? + versionId=8d17d5ff-65c8-4339-ae83-4d4527f34fe7", + "uploads": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/example.txt? + uploads" + }, + "is_head": true, + "tags": {}, + "checksum": "md5:e7e63425ce6f05c796d05adb6b5f94be", + "created": "2019-05-24T09:58:11.904366+00:00", + "version_id": "8d17d5ff-65c8-4339-ae83-4d4527f34fe7", + "delete_marker": false, + "key": "example.txt", + "size": 15 + }, + { + "mimetype": "text/plain", + "updated": "2019-05-24T09:58:11.903395+00:00", + "links": { + "self": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/example.txt? + versionId=39075b38-b354-4ce9-bd36-2425495e6a7a", + "version": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/example.txt? + versionId=39075b38-b354-4ce9-bd36-2425495e6a7a" + }, + "is_head": false, + "tags": {}, + "checksum": "md5:2cad20c19a8eb9bb11a9f76527aec9bc", + "created": "2019-05-24T09:20:36.341621+00:00", + "version_id": "39075b38-b354-4ce9-bd36-2425495e6a7a", + "delete_marker": false, + "key": "example.txt", + "size": 12 + }, + { + "mimetype": "text/plain", + "updated": "2019-05-24T10:08:34.172575+00:00", + "links": { + "self": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/foo.txt", + "version": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/foo.txt? + versionId=ca1b9724-cc29-428c-a5d4-b06e1694eb14", + "uploads": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/foo.txt? + uploads" + }, + "is_head": true, + "tags": {}, + "checksum": "md5:ff702f10bebfa2f1508deb475ded2d65", + "created": "2019-05-24T10:08:34.170827+00:00", + "version_id": "ca1b9724-cc29-428c-a5d4-b06e1694eb14", + "delete_marker": false, + "key": "foo.txt", + "size": 7 + } + ], + "size": 34 + } + + +Download file +^^^^^^^^^^^^^ + +**Description** + Downloads a file. + +**Parameters** + +.. code-block:: console + + No parameters + +**Request** + +.. code-block:: console + + GET /files// + +**Response** + +.. code-block:: console + + File contents + +**Errors** + +.. code-block:: json + + { + "message": "Object does not exists.", + "status": 404 + } + + +Delete file version +^^^^^^^^^^^^^^^^^^^ + +**Description** + Permanently erases the object version. + +**Parameters** + +.. code-block:: console + + No parameters + +**Request** + +.. code-block:: console + + DELETE /files//?versionId= + +**Response** + +.. code-block:: console + + Status: 204 NO CONTENT + +**Errors** + +.. code-block:: json + + { + "message":"Object does not exists.", + "status":404 + } + + +Delete file +^^^^^^^^^^^ + +**Description** + Marks whole file as deleted. + +**Parameters** + +.. code-block:: console + + No parameters + +**Request** + +.. code-block:: console + + DELETE /files// + +**Response** + +.. code-block:: console + + Status: 204 NO CONTENT + +**Errors** + +.. code-block:: json + + { + "message":"Object does not exists.", + "status":404 + } + + +Working with multipart files: +----------------------------- + +Initiate multipart upload +^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Initiates a multipart upload. + +**Parameters** + +.. code-block:: console + + No parameters + +**Request** + +.. code-block:: console + + POST /files//? + uploads&size=&partSize= + +**Response** + +.. code-block:: json + + { + "updated": "2019-05-24T10:30:02.969221+00:00", + "links": { + "self": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/bar.txt? + uploadId=650dd4cb-4f7a-4671-a1e4-fd1dfd1d926c", + "object": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/bar.txt", + "bucket": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794" + }, + "last_part_size": 5242880, + "created": "2019-05-24T10:30:02.969216+00:00", + "bucket": "436ac279-d85f-4500-8217-295804c14794", + "completed": false, + "part_size": 6291456, + "key": "bar.txt", + "last_part_number": 1, + "id": "650dd4cb-4f7a-4671-a1e4-fd1dfd1d926c", + "size": 11534336 + } + +**Errors** + +.. code-block:: json + + { + "message": "The request was well-formed but was unable to be followed + due to semantic errors.", + "status": 422 + } + + + { + "status": 400, + "message": "Invalid part size." + } + + + { + "status": 400, + "message": "Invalid file size." + } + + + { + "message": "Bucket does not exist.", + "status": 404 + } + + +Add to multipart upload +^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Uploads a part of an in-progress multipart upload. + +**Parameters** + +.. code-block:: console + + binary: + +**Request** + +.. code-block:: console + + PUT /files//?uploadId=&part= + +**Response** + +.. code-block:: json + + { + "updated": "2019-05-24T10:37:36.734901+00:00", + "created": "2019-05-24T10:37:36.708936+00:00", + "checksum": "md5:bd3c485ea77f37d3cb04501ea6000e63", + "part_number": 0, + "end_byte": 6291456, + "start_byte": 0 + } + +**Errors** + +.. code-block:: json + + { + "status": 400, + "message": null + } + + { + "status": 400, + "message": "No upload part detected in request." + } + + +List in-progress multipart uploads +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Returns a list of all in-progress multipart uploads. + +**Parameters** + +.. code-block:: console + + No parameters + +**Request** + +.. code-block:: console + + GET /files/?uploads + +**Response** + +.. code-block:: json + + [ + { + "updated": "2019-05-24T10:30:02.969221+00:00", + "links": { + "self": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/bar.txt? + uploadId=650dd4cb-4f7a-4671-a1e4-fd1dfd1d926c", + "object": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/bar.txt", + "bucket": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794" + }, + "last_part_size": 5242880, + "created": "2019-05-24T10:30:02.969216+00:00", + "bucket": "436ac279-d85f-4500-8217-295804c14794", + "completed": false, + "part_size": 6291456, + "key": "bar.txt", + "last_part_number": 1, + "id": "650dd4cb-4f7a-4671-a1e4-fd1dfd1d926c", + "size": 11534336 + } + ] + +**Errors** + +.. code-block:: json + + { + "message": "Bucket does not exist.", + "status": 404 + } + + +List uploaded parts +^^^^^^^^^^^^^^^^^^^ + +**Description** + Returns a list of all the uploaded parts of a multipart upload. + +**Parameters** + +.. code-block:: console + + No parameters + +**Request** + +.. code-block:: console + + GET /files//?uploadId= + +**Response** + +.. code-block:: json + + { + "updated": "2019-05-24T10:37:36.707887+00:00", + "last_part_size": 5242880, + "links": { + "self": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/bar.txt? + uploadId=650dd4cb-4f7a-4671-a1e4-fd1dfd1d926c", + "object": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794/bar.txt", + "bucket": "http://localhost:5000/files/ + 436ac279-d85f-4500-8217-295804c14794" + }, + "created": "2019-05-24T10:30:02.969216+00:00", + "part_size": 6291456, + "completed": false, + "bucket": "436ac279-d85f-4500-8217-295804c14794", + "parts": [ + { + "updated": "2019-05-24T10:37:36.734901+00:00", + "created": "2019-05-24T10:37:36.708936+00:00", + "checksum": "md5:bd3c485ea77f37d3cb04501ea6000e63", + "part_number": 0, + "end_byte": 6291456, + "start_byte": 0 + } + ], + "key": "bar.txt", + "last_part_number": 1, + "id": "650dd4cb-4f7a-4671-a1e4-fd1dfd1d926c", + "size": 11534336 + } + +**Errors** + +.. code-block:: json + + { + "message": "The request was well-formed but was unable to be followed + due to semantic errors.", + "status": 422 + } + + { + "message": "uploadId does not exists.", + "status": 404 + } + + { + "message": "Bucket does not exist.", + "status": 404 + } + + +Complete multipart upload +^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Finalizes the multipart upload, merging all parts into one. + +**Parameters** + +.. code-block:: console + + No parameters + +**Request** + +.. code-block:: console + + POST /files//?uploadId= + +**Response** + +.. code-block:: console + + Status 200 OK + +**Errors** + +.. code-block:: json + + { + "status": 400, + "message": "Not all parts have been uploaded." + } + + +Abort multipart upload +^^^^^^^^^^^^^^^^^^^^^^ + +**Description** + Aborts a multipart upload. + +**Parameters** + +.. code-block:: console + + No parameters + +**Request** + +.. code-block:: console + + DELETE /files//?uploadId= + +**Response** + +.. code-block:: console + + Status: 204 NO CONTENT + +**Errors** + +.. code-block:: json + + { + "message": "uploadId does not exists.", + "status": 404 + } + + +Storage Backends +---------------- + +In order to get started let's setup and configure a storage backend. +Storage will serve as an interface for the actual file access. + +In the configuration of the application, the variable +:py:data:`invenio_files_rest.config.FILES_REST_STORAGE_FACTORY` +defines the path of the factory that will be used to create a storage instance. + +Invenio-Files-REST comes with a default storage implementation +`PyFilesystem `_ to save files locally. + +The module provides an abstract layer for storage implementation that allows +to swap storages easily. +For example the storage backend can be a cloud service, such as +`Invenio-S3 `_ which offers integration +with any S3 REST API compatible object storage. + + +Build your own Storage Backend +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Advanced topic on how to implement and connect your own storage Backend for +Invenio-Files-REST. + +In order to use a different storage backend, it is required to subclass the +:py:data:`invenio_files_rest.storage.FileStorage` class, and provide +implementations for some of its methods. + +Mandatory methods to implement: + +* :code:`initialize` +* :code:`open` +* :code:`save` +* :code:`update` +* :code:`delete` + +Optional methods to implement: + +* :code:`send_file` +* :code:`checksum` +* :code:`copy` +* :code:`_init_hash` +* :code:`_compute_checksum` +* :code:`_write_stream` + + +Create Buckets +-------------- + +In order to upload, modify or delete files, a bucket needs to be created first. +A bucket can be created by a :code:`POST` request to the endpoint +:code:`/files`. The response will contain the unique ID of the bucket. +A bucket can have one or more tags which store extra metadata for that bucket. +Each tag is uniquely identified by a key. + +First let's create a bucket: + +.. code-block:: console + + $ curl -X POST http://localhost:5000/files + +.. code-block:: json + + { + "max_file_size": null, + "updated": "2019-05-16T13:07:21.595398+00:00", + "locked": false, + "links": { + "self": "http://localhost:5000/files/ + cb8d0fa7-2349-484b-89cb-16573d57f09e", + "uploads": "http://localhost:5000/files/ + cb8d0fa7-2349-484b-89cb-16573d57f09e?uploads", + "versions": "http://localhost:5000/files/ + cb8d0fa7-2349-484b-89cb-16573d57f09e?versions" + }, + "created": "2019-05-16T13:07:21.595391+00:00", + "quota_size": null, + "id": "cb8d0fa7-2349-484b-89cb-16573d57f09e", + "size": 0 + } + + +Upload Files +------------ + +The REST API allows you to upload, download and modify single files. +A file is uniquely identified within a bucket by its :code:`key` (filename). +Each file can have multiple versions. + +Let's upload a file called :code:`my_file.txt` inside the bucket that was just +created. A file can be added to a bucket (uploaded) by a :code:`PUT` request, +which will create a new :code:`ObjectVersion`. The same will happen +when uplading a file with the same :code:`key` (filename). + + +Upload a file: + +.. code-block:: console + + $ B=cb8d0fa7-2349-484b-89cb-16573d57f09e + + $ curl -i -X PUT --data-binary @my_file.txt \ + "http://localhost:5000/files/$B/my_file.txt" + +.. code-block:: json + + { + "mimetype": "text/plain", + "updated": "2019-05-16T13:10:22.621533+00:00", + "links": { + "self": "http://localhost:5000/files/ + cb8d0fa7-2349-484b-89cb-16573d57f09e/my_file.txt", + + "version": "http://localhost:5000/files/ + cb8d0fa7-2349-484b-89cb-16573d57f09e/my_file.txt? + versionId=7f62676d-0b8e-4d77-9687-8465dc506ca8", + "uploads": "http://localhost:5000/files/ + cb8d0fa7-2349-484b-89cb-16573d57f09e/ + my_file.txt?uploads" + }, + "is_head": true, + "tags": {}, + "checksum": "md5:d7d02c7125bdcdd857eb70cb5f19aecc", + "created": "2019-05-16T13:10:22.617714+00:00", + "version_id": "7f62676d-0b8e-4d77-9687-8465dc506ca8", + "delete_marker": false, + "key": "my_file.txt", + "size": 14 + } + + +JS Uploaders +^^^^^^^^^^^^ + +Some JavaScript uploaders do not allow to customize the name of the request +parameters. +You can create an upload factory according to the specifications of your +JavaScript uploader and update the relevant configuration as follows: + +1. Assing your factories to the :code:`config` variables: +:py:data:`invenio_files_rest.config.FILES_REST_MULTIPART_PART_FACTORIES` and +:py:data:`invenio_files_rest.config.FILES_REST_UPLOAD_FACTORIES` + +2. Use the :code:`@use_kwargs` decorator to map your JS upoader's parameters +as in the example below, whereby the request paramater :code:`_totalSize` +is mapped to :code:`content-length`: + +.. code-block:: python + + @use_kwargs({ + 'content_length': fields.Int( + load_from='_totalSize', + location='form', + required=True, + ), + 'content_type': fields.Str( + load_from='Content-Type', + location='headers', + required=True, + ), + 'uploaded_file': fields.Raw( + load_from='file', + location='files', + required=True, + ), + }) + +Invenio-Files-REST comes with an implementation for +`ng-file-upload `_ AngularJs +uploader. + +For more details see +:py:func:`invenio_files_rest.views.ngfileupload_uploadfactory`. + +Multipart Upload +^^^^^^^^^^^^^^^^ + +In some cases, a file may be too large for a single upload. You might want to +speed up the upload process by uploading multiple parts in parallel. In these +cases, you have to use multipart uploads. + +This requires you to split the file you want to upload, in equal chunks except +from the last one which has to be smaller or equal to the chunks size. + +Then each chunck can be uploaded in parallel. Once all parts have been +uploaded, the multipart upload completes, and the parts are automatically +merged into one single file. + +When uploading a multipart file, if one of the chunks fails, it will be +discarded, and you can resubmit only the failed chunk to conclude your upload. + +As an example, let's create an 11MB file which will then be split into 2 +chunks using the linux :code:`split` command: + +.. code-block:: console + + dd if=/dev/urandom of=my_file.txt bs=1048576 count=11 + + split -b6291456 my_file.txt segment_ + +A multipart upload can be initialised with a :code:`POST` request, sending +the name of the file after merge, the chunk size and the total size. + +Then each part upload can be uploaded with a :code:`PUT` request. + +Create a new bucket: + +.. code-block:: console + + $ curl -X POST http://localhost:5000/files + +The ID is contained in the response: + +.. code-block:: json + + { + "max_file_size":null, + "updated":"2019-05-17T06:52:52.897378+00:00", + "locked":false, + "links":{ + "self":"http://localhost:5000/files/ + c896d17b-0e7d-44b3-beba-7e43b0b1a7a4", + "uploads":"http://localhost:5000/files/ + c896d17b-0e7d-44b3-beba-7e43b0b1a7a4?uploads", + "versions":"http://localhost:5000/files/ + c896d17b-0e7d-44b3-beba-7e43b0b1a7a4?versions" + }, + "created":"2019-05-17T06:52:52.897373+00:00", + "quota_size":null, + "id":"c896d17b-0e7d-44b3-beba-7e43b0b1a7a4", + "size":0 + } + +Multipart upload initialisation: + +.. code-block:: console + + $ B=c896d17b-0e7d-44b3-beba-7e43b0b1a7a4 + + $ curl -i -X POST \ + "http://localhost:5000/files/$B/my_file.txt? + uploads&size=11534336&partSize=6291456" + +The response will contain the upload :code:`id` that is needed for the +requests for the parts uploads: + +.. code-block:: json + + { + "updated":"2019-05-17T07:07:22.219002+00:00", + "links":{ + "self":"http://localhost:5000/files/ + c896d17b-0e7d-44b3-beba-7e43b0b1a7a4/my_file.txt? + uploadId=a85b1cbd-4080-4c81-a95c-b4df5d1b615f", + + "object":"http://localhost:5000/files/ + c896d17b-0e7d-44b3-beba-7e43b0b1a7a4/my_file.txt", + + "bucket":"http://localhost:5000/files/ + c896d17b-0e7d-44b3-beba-7e43b0b1a7a4" + }, + "last_part_size":5242880, + "created":"2019-05-17T07:07:22.218998+00:00", + "bucket":"c896d17b-0e7d-44b3-beba-7e43b0b1a7a4", + "completed":false, + "part_size":6291456, + "key":"my_file.txt", + "last_part_number":1, + "id":"a85b1cbd-4080-4c81-a95c-b4df5d1b615f", + "size":11534336 + } + +Continue uploading parts, by using a PUT request and specifying the upload +:code:`id` of the bucket: + +.. code-block:: console + + $ U=a85b1cbd-4080-4c81-a95c-b4df5d1b615f + + $ curl -i -X PUT --data-binary @segment_aa \ + "http://localhost:5000/files/$B/my_file.txt?uploadId=$U&partNumber=0" + + { + "updated":"2019-05-17T07:08:27.069504+00:00", + "created":"2019-05-17T07:08:27.048028+00:00", + "checksum":"md5:876ae993a752f38b1850668be7e3fe9a", + "part_number":0, + "end_byte":6291456, + "start_byte":0 + } + + $ curl -i -X PUT --data-binary @segment_ab \ + "http://localhost:5000/files/$B/my_file.txt?uploadId=$U&partNumber=1" + +Complete a multipart upload, by submitting a :code:`POST` request, with the +upload id. + +.. code-block:: console + + $ curl -i -X POST \ + "http://localhost:5000/files/$B/my_file.txt?uploadId=$U" + +Abort a multipart upload (deletes all uploaded parts); it will return a 204 +code if it succeeds: + +.. code-block:: console + + $ curl -i -X DELETE "http://localhost:5000/files/$B/my_file.txt?uploadId=$U" + + +Large Files +^^^^^^^^^^^ +The maximum file size for upload is defined by :code:`MAX_CONTENT_LENGTH` +header. In addition your webserver i.e. Nginx will apply a limitation on the +body size of the request. + +1. You can modify the maximum allowed file size by changing the +:code:`MAX_CONTENT_LENGTH` configuration variable. Flask +will reject any incoming requests with a greater content length by returning a +:code:`413 (Request Entity Too Large)`. For security if it is not set and +the request does not specify a :code:`CONTENT_LENGTH` header, no data will be +read. The example below configues :code:`MAX_CONTENT_LENGTH` to :code:`25MB`. + +>>> app.config['MAX_CONTENT_LENGTH'] = 25 * 1024 * 1024 + +.. note:: + + Special note on the :code:`get_data()` method: Calling this loads the full + request data into memory. This is only safe to do if the + :code:`MAX_CONTENT_LENGTH` is set. + +2. In case of using Nginx, the request body size is limitd by the configuration +variable :code:`client_max_body_size`. For files with size greater than that, +it will return :code:`413 (Request Entity Too Large)`. The following example +configures Nginx to accept up to :code:`25MB`. + +.. code-block:: console + + http { + ... + client_max_body_size 25M; + } + + +Retrieve Files +-------------- + +Once the bucket is created and a file is uploaded, it is possible +to retrieve it with a :code:`GET` request. + +By default, the latest version will be retrieved. To retrieve a specific +version of the file, the :code:`versionId` query parameter can be used, as in +the example below: + +Download the latest version of the file: + +.. code-block:: console + + $ curl -i http://localhost:5000/files/$B/my_file.txt + +Download a specific version of the file: + +.. code-block:: console + + $ curl -i http://localhost:5000/files/$B/my_file.txt?versionId= + +.. note:: + By default, the file is returned with the header + :code:`'Content-Disposition': 'inline'`, so that the browser will try to + preview it. In case you want to trigger a download of the file, use the + :code:`download` query parameter, which will change the + :code:`'Content-Disposition'` header to :code:`'attachment'` + +.. code-block:: console + + $ curl -i http://localhost:5000/files/$B/my_file.txt?download + + +Security +^^^^^^^^ + +It is very easy to be exposed to Cross-Site Scripting (XSS) attacks if you +serve user uploaded files. Here are some recommendations: + +1. Serve user uploaded files from a separate domain (not a subdomain). This +way a malicious file can only attack other user uploaded files. + +2. Prevent the browser from rendering and executing HTML files by setting +:code:`trusted=False` in the :code:`send_file()` method of your +:code:`FileStorage` implementation. + +3. Force the browser to download the file as an attachment +:code:`as_attachment=True` by adding the :code:`download` keyword in the query +parameters. + + +Delete Files +------------ + +If you want to delete a file there are two options: + +1. You can mark the file as deleted. This will create a new +:code:`ObjectVersion` without content (creates a delete marker and makes the +file inaccessible): + +.. code-block:: console + + $ curl -i -X DELETE http://localhost:5000/files/$B/my_file.txt + + +2. Permanently delete a specific object version, by specifying +the version id. This will completely remove the :code:`ObjectVersion`: + +.. code-block:: console + + $ curl -i -X DELETE \ + http://localhost:5000/files/$B/my_file.txt?versionId= + + +.. :note:: + :code:`ObjectVersion` that are marked as deleted can be retrieved only by + providing an explicit :code:`versionId` as query parameter. + + +The file instance on disk cannot be removed through REST API. You can use +the provided task via CLI +:py:func:`invenio_files_rest.tasks.remove_file_data`. + + +Access control +-------------- + +Invenio-Files-REST depends on `Invenio-Access +`_ module, to control the files access. + +It comes with a default permission factory implementation which can be found +at :py:data:`invenio_files_rest.permissions.permission_factory` and can be +customized further, by providing your custom implementation in the relevant +config variable +:py:data:`invenio_files_rest.config.FILES_REST_PERMISSION_FACTORY`. + +The module also comes with a list of predefined actions for the most common +operations: + + - location-update + - bucket-read + - bucket-read-versions + - bucket-update + - bucket-listmultiparts + - object-read + - object-read-version + - object-delete + - object-delete-version + - multipart-read + - multipart-delete + + +For example, to verify that the contents of a bucket can be read, you should +add the decorator with :code:`bucket-read` action which takes the bucket as the +argument. + +.. code-block:: python + + @need_permissions( + lambda self, bucket, versions: bucket, + 'bucket-read', + ) + def foo(): + print("Function foo can read the content of the bucket") + + +By default when try perform an action and the permission check fails, the +returned http status code will be :code:`404` instead of :code:`401` or +:code:`403` to hide the existence or non, of objects. + +See :mod:`invenio_files_rest.permissions` for extensive documentation. + + +Integrity +--------- + +Invenio-Files-REST stores file checksums and regularly revalidates them, in +order to verify the data integrity of all data at rest, as well as to detect +corruption of data in transit. + +For the computation of the checksum you can provide the desired algorithm, +otherwise :code:`MD5` will be used. + +When uploading a file a checksum is computed on the fly and stored in the +database. + +For all existing files there is a predefined task :code:`verify_checksum` +which can be configured to run periodically (default is every 30 days) and +iterates all files in your storage and validates their checksum. + +When removing a file from disk, the operation is a combination of two steps, +first delete the :code:`FileInstance` from the database, and if it succeeds +will try to delete the File from disk. This leaves the possibility of having +a file on disk dangling in case the database removal works, and the disk file +removal doesn't work. + + +Signals +------- + +Invenio-Files-REST supports signals that can be used to react to events. + +Events are sent in case of: + +* file downloaded + +Let's request to download a file, and capture the signal: + +.. code-block:: python + + from invenio_files_rest.signals import file_downloaded + + def after_file_downloaded(send, *args, *kwargs): + print('Signal file_downloaded emitted') + + listener = file_downloaded.connect(after_file_downloaded) + # Request to dowload a file for the event to trigger + +You can read more about the `Flask Signals +`_. + + +Data Migration +-------------- + +:code:`Locations` are used to represent different storage systems and possibly +different geographical locations. :code:`Buckets` but also +:code:`ObjectVersions` are assigned a Location. This approach provides extra +flexibility when there's a need to migrate the data. + +When a bucket is created, a Location needs to be provided, otherwise the +default one is used. + +.. note:: + Before updating our records to point to the new :code:`Location`, the + actual files need to be copied in the new storage with the new location. + Then a bulk update needs to be performed on the FileInstance objects + to point to the new bucket. + +Invenio-Files-REST provides a celery task +:py:func:`invenio_files_rest.tasks.migrate_file` to migrate existing files +from current location to a new location. A new location might be in remote +system on a different bucket, even on a different storage backend. This task +can be used to migrate all files or a subset of files in case of location +change. Given a :code:`file_id` and the name of the new location (which should +have been already created), it will: + + 1. create a new empty :code:`FileInstance` in the destination location + 2. copy the file content in the newly created :code:`FileInstance` + 3. re-link all ObjectVersions pointing to the previous :code:`FileInstance` + to the new one, and optionally with :code:`post_fixity_check` argument, + re-compute the file checksum + +In case process does not complete successfully, destination +:code:`FileInstance` is removed completely and the process has to be repeated. + +See :doc:`api` for an extensive API documentation. +""" from __future__ import absolute_import, print_function diff --git a/invenio_files_rest/models.py b/invenio_files_rest/models.py index 87b2db39..c51155ae 100644 --- a/invenio_files_rest/models.py +++ b/invenio_files_rest/models.py @@ -23,8 +23,7 @@ The location of the file is specified via a URI. A file instance can have many object versions. * **Locations** - A bucket belongs to a specific location. Locations can be - used to represent e.g. different storage systems and/or geographical - locations. + used to represent e.g. different storage systems. * **Multipart Objects** - Identified by UUIDs and belongs to a specific bucket and key. * **Part object** - Identified by their multipart object and a part number. @@ -1712,9 +1711,11 @@ def set_contents(self, stream, progress_callback=None): __all__ = ( 'Bucket', + 'BucketTag', 'FileInstance', 'Location', 'MultipartObject', 'ObjectVersion', + 'ObjectVersionTag', 'Part', )