"""Blueprint for file upload API.""" from http import HTTPStatus from abacus_common_logic.utils.request import BooleanFilter from abacus_common_logic.views.create_view import CreateView from common_apispec import doc, marshal_with, use_kwargs from common_apispec.views import MethodResource from flask import Blueprint, request from owsrequest import flask_request from owsresponse import response from owsresponse.adaptors.flask import flaskify from abacus_file_upload import logic from abacus_file_upload.schemas import ( FileUploadDetailSchema, FileUploadDetailVerboseSchema, FileUploadStatusPutSchema, InitiateUploadRequestSchema, InitiateUploadResponseSchema, ) file_upload_api = Blueprint('file_upload_api', __name__) class FileUploadInitiateView(CreateView, MethodResource): """Handles file upload initiation.""" post_schema = InitiateUploadRequestSchema() def post(self, **kwargs): """Initiate file upload. Args: **kwargs: Additional keyword arguments passed to parent class """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code='authorization_error', message='Unauthorized', status=401, ) ) return super().post(**kwargs) def create_handler(self, **params): """Initiate file upload and return presigned URLs. Args: **params: Upload parameters to pass to logic layer """ return logic.initiate_upload(**params) @file_upload_api.route('/file-upload/', methods=['GET']) @doc( summary='Get file upload record', params={ 'file_key': { 'description': 'Unique file key (UUID)', }, 'verbose': { 'description': 'Include audit fields (created_at, created_by, last_modified, last_modified_by, deleted_at, deleted_by)', 'in': 'query', 'schema': {'type': 'boolean'}, 'required': False, }, }, ) @marshal_with( FileUploadDetailVerboseSchema, code=HTTPStatus.OK, description='Returns file upload record with status and metadata (audit fields included only when verbose=true)', ) @marshal_with( None, code=HTTPStatus.UNAUTHORIZED, description=HTTPStatus.UNAUTHORIZED.phrase, ) @marshal_with( None, code=HTTPStatus.NOT_FOUND, description='Upload not found', ) @marshal_with( None, code=HTTPStatus.INTERNAL_SERVER_ERROR, description='Internal server error', ) def get_file_upload(file_key): """Get file upload record. Args: file_key: Unique file key (UUID) Query Params: verbose (bool): Include audit fields (created_at, created_by, etc.) Accepts: true/false, yes/no, 1/0, on/off, t/f, y/n Default: false Returns: File upload record """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code='authorization_error', message='Unauthorized', status=401, ) ) # Read verbose query parameter verbose_filter = BooleanFilter.parse( request.args.get('verbose'), BooleanFilter.FALSE ) verbose = verbose_filter.to_bool() return flaskify(logic.get_file_upload(file_key, verbose=verbose)) @file_upload_api.route('/file-upload//complete', methods=['POST']) @doc( summary='Complete file upload', params={ 'file_key': { 'description': 'Unique file key (UUID)', }, }, ) @marshal_with( FileUploadDetailSchema, code=HTTPStatus.OK, description='Upload successfully completed and verified', ) @marshal_with( None, code=HTTPStatus.UNAUTHORIZED, description=HTTPStatus.UNAUTHORIZED.phrase, ) @marshal_with( None, code=HTTPStatus.BAD_REQUEST, description='Invalid operation (e.g., completing a cancelled upload)', ) @marshal_with( None, code=HTTPStatus.NOT_FOUND, description='Upload not found', ) @marshal_with( None, code=HTTPStatus.INTERNAL_SERVER_ERROR, description='Internal server error', ) def complete_upload(file_key): """Complete file upload. Args: file_key: Unique file key (UUID) Returns: Completion confirmation """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code='authorization_error', message='Unauthorized', status=401, ) ) return flaskify(logic.complete_upload(file_key)) @file_upload_api.route('/file-upload//cancel', methods=['POST']) @doc( summary='Cancel file upload', params={ 'file_key': { 'description': 'Unique file key (UUID)', }, }, ) @marshal_with( FileUploadDetailSchema, code=HTTPStatus.OK, description='Upload successfully cancelled', ) @marshal_with( None, code=HTTPStatus.UNAUTHORIZED, description=HTTPStatus.UNAUTHORIZED.phrase, ) @marshal_with( None, code=HTTPStatus.BAD_REQUEST, description='Invalid operation (e.g., cancelling a completed upload)', ) @marshal_with( None, code=HTTPStatus.NOT_FOUND, description='Upload not found', ) @marshal_with( None, code=HTTPStatus.INTERNAL_SERVER_ERROR, description='Internal server error', ) def cancel_upload(file_key): """Cancel file upload. Args: file_key: Unique file key (UUID) Returns: Cancellation confirmation """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code='authorization_error', message='Unauthorized', status=401, ) ) return flaskify(logic.cancel_upload(file_key)) @file_upload_api.route('/file-upload//download', methods=['GET']) @doc( summary='Generate presigned download URL for uploaded file', params={ 'file_key': { 'description': 'Unique file key (UUID)', }, }, ) @marshal_with( None, code=HTTPStatus.OK, description='Returns download URL with expiration and file metadata', ) @marshal_with( None, code=HTTPStatus.BAD_REQUEST, description='File not available for download (e.g., upload not completed)', ) @marshal_with( None, code=HTTPStatus.UNAUTHORIZED, description=HTTPStatus.UNAUTHORIZED.phrase, ) @marshal_with( None, code=HTTPStatus.NOT_FOUND, description='Upload not found', ) @marshal_with( None, code=HTTPStatus.INTERNAL_SERVER_ERROR, description='Internal server error', ) def download_file(file_key): """Generate presigned download URL for uploaded file. Args: file_key: Unique file key (UUID) Returns: Presigned download URL with fixed expiration time """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code='authorization_error', message='Unauthorized', status=401, ) ) return flaskify(logic.download_file(file_key)) @file_upload_api.route('/file-upload//quarantine', methods=['POST']) @doc( summary='Files that fail the AV scan are moved to the quarantine s3 bucket.', params={ 'file_key': { 'description': 'Unique file key (UUID)', }, }, ) @marshal_with( FileUploadDetailSchema, code=HTTPStatus.OK, description='Infected uploads are successfully moved.', ) @marshal_with( None, code=HTTPStatus.UNAUTHORIZED, description=HTTPStatus.UNAUTHORIZED.phrase, ) @marshal_with( None, code=HTTPStatus.BAD_REQUEST, description='Invalid operation (e.g., completing a cancelled upload)', ) @marshal_with( None, code=HTTPStatus.NOT_FOUND, description='Upload not found', ) @marshal_with( None, code=HTTPStatus.INTERNAL_SERVER_ERROR, description='Internal server error', ) def quarantine_infected_upload(file_key): """Files that fail the AV scan are moved to the quarantine bucket. Args: file_key: Unique file key (UUID) Returns: Confirmation of file transfer. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code='authorization_error', message='Unauthorized', status=401, ) ) return flaskify(logic.quarantine_infected_upload(file_key)) # Register routes file_upload_initiate_view = FileUploadInitiateView.as_view('file_upload_initiate') file_upload_initiate_view = doc( summary='Initiate file upload', )(file_upload_initiate_view) file_upload_initiate_view = use_kwargs( InitiateUploadRequestSchema, location='json', required=True, apply=False )(file_upload_initiate_view) file_upload_initiate_view = marshal_with( InitiateUploadResponseSchema, code=HTTPStatus.CREATED, description='Upload initiated successfully with presigned URLs', )(file_upload_initiate_view) file_upload_initiate_view = marshal_with( None, code=HTTPStatus.BAD_REQUEST, description='Invalid request body or validation error', )(file_upload_initiate_view) file_upload_initiate_view = marshal_with( None, code=HTTPStatus.UNAUTHORIZED, description=HTTPStatus.UNAUTHORIZED.phrase, )(file_upload_initiate_view) file_upload_initiate_view = marshal_with( None, code=HTTPStatus.NOT_FOUND, description='Upload configuration not found for the specified upload type', )(file_upload_initiate_view) file_upload_initiate_view = marshal_with( None, code=HTTPStatus.INTERNAL_SERVER_ERROR, description='Internal server error', )(file_upload_initiate_view) file_upload_api.add_url_rule( '/file-upload', view_func=file_upload_initiate_view, methods=['POST'], ) @file_upload_api.route('/file-upload//status', methods=['PUT']) @doc( summary='Updated the file_upload status', params={ 'file_key': { 'description': 'Unique file key (UUID)', }, }, ) @use_kwargs(FileUploadStatusPutSchema, location='json', required=True, apply=False) @marshal_with( FileUploadDetailSchema, code=HTTPStatus.OK, description='Updated the upload status successfully.', ) @marshal_with( None, code=HTTPStatus.UNAUTHORIZED, description=HTTPStatus.UNAUTHORIZED.phrase, ) @marshal_with( None, code=HTTPStatus.BAD_REQUEST, description='Invalid operation (e.g., invalid upload status transition)', ) @marshal_with( None, code=HTTPStatus.NOT_FOUND, description='Upload not found', ) @marshal_with( None, code=HTTPStatus.INTERNAL_SERVER_ERROR, description='Internal server error', ) def update_file_upload_status(file_key): """Update the file_upload status. Args: file_key: Unique file key (UUID) Returns: Completion confirmation """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code='authorization_error', message='Unauthorized', status=401, ) ) return flaskify(logic.update_file_upload_status(file_key, request.get_json()))