about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--uploader/__init__.py2
-rw-r--r--uploader/files/views.py97
2 files changed, 99 insertions, 0 deletions
diff --git a/uploader/__init__.py b/uploader/__init__.py
index 9fdb383..cae531b 100644
--- a/uploader/__init__.py
+++ b/uploader/__init__.py
@@ -11,6 +11,7 @@ from uploader.oauth2.client import user_logged_in, authserver_authorise_uri
 
 from . import session
 from .base_routes import base
+from .files.views import files
 from .species import speciesbp
 from .oauth2.views import oauth2
 from .expression_data import exprdatabp
@@ -82,6 +83,7 @@ def create_app():
 
     # setup blueprints
     app.register_blueprint(base, url_prefix="/")
+    app.register_blueprint(files, url_prefix="/files")
     app.register_blueprint(oauth2, url_prefix="/oauth2")
     app.register_blueprint(speciesbp, url_prefix="/species")
 
diff --git a/uploader/files/views.py b/uploader/files/views.py
new file mode 100644
index 0000000..cd5f00f
--- /dev/null
+++ b/uploader/files/views.py
@@ -0,0 +1,97 @@
+"""Module for generic files endpoints."""
+import traceback
+from pathlib import Path
+
+from flask import request, jsonify, Blueprint, current_app as app
+
+from .chunks import chunk_name, chunks_directory
+
+files = Blueprint("files", __name__)
+
+@files.route("/upload/resumable", methods=["GET"])
+def resumable_upload_get():
+    """Used for checking whether **ALL** chunks have been uploaded."""
+    fileid = request.args.get("resumableIdentifier", type=str) or ""
+    filename = request.args.get("resumableFilename", type=str) or ""
+    chunk = request.args.get("resumableChunkNumber", type=int) or 0
+    if not(fileid or filename or chunk):
+        return jsonify({
+            "message": "At least one required query parameter is missing.",
+            "error": "BadRequest",
+            "statuscode": 400
+        }), 400
+
+    if Path(chunks_directory(fileid),
+            chunk_name(filename, chunk)).exists():
+        return "OK"
+
+    return jsonify({
+            "message": f"Chunk {chunk} was not found.",
+            "error": "NotFound",
+            "statuscode": 404
+        }), 404
+
+
+def __merge_chunks__(targetfile: Path, chunkpaths: tuple[Path, ...]) -> Path:
+    """Merge the chunks into a single file."""
+    with open(targetfile, "ab") as _target:
+        for chunkfile in chunkpaths:
+            with open(chunkfile, "rb") as _chunkdata:
+                _target.write(_chunkdata.read())
+
+            chunkfile.unlink()
+    return targetfile
+
+
+@files.route("/upload/resumable", methods=["POST"])
+def resumable_upload_post():
+    """Do the actual chunks upload here."""
+    _totalchunks = request.form.get("resumableTotalChunks", type=int) or 0
+    _chunk = request.form.get("resumableChunkNumber", default=1, type=int)
+    _uploadfilename = request.form.get(
+        "resumableFilename", default="", type=str) or ""
+    _fileid = request.form.get(
+        "resumableIdentifier", default="", type=str) or ""
+    _targetfile = Path(app.config["UPLOAD_FOLDER"], _fileid)
+
+    if _targetfile.exists():
+        return jsonify({
+            "message": (
+                "A file with a similar unique identifier has previously been "
+                "uploaded and possibly is/has being/been processed."),
+            "error": "BadRequest",
+            "statuscode": 400
+        }), 400
+
+    try:
+        chunks_directory(_fileid).mkdir(exist_ok=True, parents=True)
+        request.files["file"].save(Path(chunks_directory(_fileid),
+                                        chunk_name(_uploadfilename, _chunk)))
+
+        # Check whether upload is complete
+        chunkpaths = tuple(
+            Path(chunks_directory(_fileid), chunk_name(_uploadfilename, _achunk))
+            for _achunk in range(1, _totalchunks+1))
+        if all(_file.exists() for _file in chunkpaths):
+            # merge_files and clean up chunks
+            __merge_chunks__(_targetfile, chunkpaths)
+            chunks_directory(_fileid).rmdir()
+            return jsonify({
+                "uploaded-file": _targetfile.name,
+                "message": "File was uploaded successfully!",
+                "statuscode": 200
+            }), 200
+        return jsonify({
+            "message": "Some chunks were not uploaded!",
+            "error": "ChunksUploadError",
+            "error-description": "Some chunks were not uploaded!"
+        })
+    except Exception as exc:# pylint: disable=[broad-except]
+        msg = "Error processing uploaded file chunks."
+        app.logger.error(msg, exc_info=True, stack_info=True)
+        return jsonify({
+            "message": msg,
+            "error": type(exc).__name__,
+            "error-description": " ".join(str(arg) for arg in exc.args),
+            "error-trace": traceback.format_exception(exc)
+        }), 500