diff --git a/cloud_storage_handler/api/elixircloud/csh/controllers.py b/cloud_storage_handler/api/elixircloud/csh/controllers.py index fb23a34..8bc2d58 100644 --- a/cloud_storage_handler/api/elixircloud/csh/controllers.py +++ b/cloud_storage_handler/api/elixircloud/csh/controllers.py @@ -1,15 +1,84 @@ """ELIXIR's Cloud Storage Handler controllers.""" import logging +import os +import uuid from http import HTTPStatus -from flask import jsonify +from flask import current_app, jsonify, request +from minio.error import S3Error logger = logging.getLogger(__name__) +UPLOAD_DIRECTORY = "/tmp/upload" +CHUNK_SIZE = 5 * 1024 * 1024 + def home(): """Endpoint to return a welcome message.""" return jsonify( {"message": "Welcome to the Cloud Storage Handler server!"} ), HTTPStatus.OK + + +def upload_object(): + """Handles file uploads to cloud storage. + + Retrieves files from the request, processes each file into chunks, + and uploads them to the specified storage bucket. Returns a response + with a success message and details about the uploaded file(s). + """ + files = request.files.getlist("files") + responses = [] + minio_config = current_app.config.foca.custom.minio + bucket_name = minio_config.bucket_name + minio_client = current_app.config.foca.custom.minio.client.client + + for file in files: + if not file: + return jsonify({"error": "No file provided"}), 400 + + file_id = str(uuid.uuid4()) + file_path = os.path.join(UPLOAD_DIRECTORY, f"{file_id}.temp") + + file.save(file_path) + + total_size = os.path.getsize(file_path) + total_chunks = (total_size // CHUNK_SIZE) + ( + 1 if total_size % CHUNK_SIZE > 0 else 0 + ) + + file_dir = os.path.join(UPLOAD_DIRECTORY, file_id) + os.makedirs(file_dir, exist_ok=True) + + with open(file_path, "rb") as f: + for i in range(total_chunks): + chunk_data = f.read(CHUNK_SIZE) + chunk_filename = os.path.join(file_dir, f"chunk_{i}") + with open(chunk_filename, "wb") as chunk_file: + chunk_file.write(chunk_data) + + try: + minio_client.fput_object( + bucket_name, + f"{file_id}/chunk_{i}", + chunk_filename, + metadata={ + "description": "Chunk upload via Flask", + }, + ) + except S3Error as e: + return jsonify({"error": str(e)}), 500 + + os.remove(file_path) + os.rmdir(file_dir) + + responses.append( + { + "message": "File uploaded successfully", + "file_id": file_id, + "total_chunks": total_chunks, + } + ) + + return jsonify(responses), 200 diff --git a/cloud_storage_handler/api/specs/specs.yaml b/cloud_storage_handler/api/specs/specs.yaml index 2c61d7f..6698206 100644 --- a/cloud_storage_handler/api/specs/specs.yaml +++ b/cloud_storage_handler/api/specs/specs.yaml @@ -35,4 +35,36 @@ paths: description: The request is malformed. '500': description: An unexpected error occurred. -... + /object: + post: + description: | + Create a new object upload using the TUS protocol. The request creates an empty object and returns an upload URL. + operationId: upload_object + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + description: The file to be uploaded (supports .bib, .txt, .docx, and other file types). + responses: + '201': + description: Object upload created successfully. + headers: + Location: + schema: + type: string + description: URL to upload object chunks using PATCH requests. + Tus-Resumable: + schema: + type: string + description: Version of the TUS protocol used. + '400': + description: Bad request or malformed request body. + '500': + description: An unexpected error occurred. +... \ No newline at end of file