diff --git a/nomad/api/raw.py b/nomad/api/raw.py index 6bcc0e87f8052ec566efb1f989ed66f81303c86d..9b46f600765ed3a9da2c1afa94726b2247692f2f 100644 --- a/nomad/api/raw.py +++ b/nomad/api/raw.py @@ -23,13 +23,19 @@ from zipfile import ZIP_DEFLATED, ZIP_STORED import zipstream from flask import Response, request, send_file -from flask_restplus import abort +from flask_restplus import abort, Resource, fields from werkzeug.exceptions import HTTPException from nomad.files import RepositoryFile from nomad.utils import get_logger -from .app import app, base_path +from .app import api, base_path +from .auth import login_if_available + +ns = api.namespace( + '%s/raw' % base_path[1:] if base_path is not '' else 'raw', + description='Downloading raw data files.' +) def fix_file_paths(path): @@ -38,142 +44,118 @@ def fix_file_paths(path): return path[5:] -@app.route('%s/raw/<string:upload_hash>/<path:upload_filepath>' % base_path, methods=['GET']) -def get_raw_file(upload_hash, upload_filepath): - """ - Get a single raw calculation file from a given upload (or many files via wildcard). - - .. :quickref: raw; Get single raw calculation file. - - **Example request**: - - .. sourcecode:: http - - GET /nomad/api/raw/W36aqCzAKxOCfIiMFsBJh3nHPb4a/Si/si.out HTTP/1.1 - Accept: application/gz - - :param string upload_hash: the hash based identifier of the upload - :param path upload_filepath: the path to the desired file within the upload; - can also contain a wildcard * at the end to denote all files with path as prefix - :qparam compress: any value to use compression for wildcard downloads, default is no compression - :resheader Content-Type: application/gz - :status 200: calc raw data successfully retrieved - :status 404: upload with given hash does not exist or the given file does not exist - :returns: the gzipped raw data in the body or a zip file when wildcard was used - """ - upload_filepath = fix_file_paths(upload_filepath) - - repository_file = RepositoryFile(upload_hash) - if not repository_file.exists(): - abort(404, message='The upload with hash %s does not exist.' % upload_hash) - - if upload_filepath[-1:] == '*': - upload_filepath = upload_filepath[0:-1] - files = list( - file for file in repository_file.manifest - if file.startswith(upload_filepath)) - if len(files) == 0: - abort(404, message='There are no files for %s.' % upload_filepath) - else: - compress = request.args.get('compress', None) is not None - return respond_to_get_raw_files(upload_hash, files, compress) - - try: - the_file = repository_file.get_file(upload_filepath) - with the_file.open() as f: - rv = send_file( - f, - mimetype='application/octet-stream', - as_attachment=True, - attachment_filename=os.path.basename(upload_filepath)) - return rv - except KeyError: - files = list(file for file in repository_file.manifest if file.startswith(upload_filepath)) - if len(files) == 0: - abort(404, message='The file %s does not exist.' % upload_filepath) - else: - abort(404, message='The file %s does not exist, but there are files with matching paths' % upload_filepath, files=files) - except HTTPException as e: - raise e - except Exception as e: - logger = get_logger( - __name__, endpoint='raw', action='get', - upload_hash=upload_hash, upload_filepath=upload_filepath) - logger.error('Exception on accessing raw data', exc_info=e) - abort(500, message='Could not accessing the raw data.') - - -@app.route('%s/raw/<string:upload_hash>' % base_path, methods=['GET']) -def get_raw_files(upload_hash): - """ - Get multiple raw calculation files. - - .. :quickref: raw; Get multiple raw calculation files. - - **Example request**: - - .. sourcecode:: http - - GET /nomad/api/raw/W36aqCzAKxOCfIiMFsBJh3nHPb4a?files=Si/si.out,Si/aux.txt HTTP/1.1 - Accept: application/gz - - :param string upload_hash: the hash based identifier of the upload - :qparam string files: a comma separated list of file path - :qparam compress: any value to use compression, default is no compression - :resheader Content-Type: application/json - :status 200: calc raw data successfully retrieved - :status 404: calc with given hash does not exist or one of the given files does not exist - :returns: a streamed .zip archive with the raw data - """ - files_str = request.args.get('files', None) - compress = request.args.get('compress', None) is not None - - if files_str is None: - abort(400, message="No files argument given.") - files = [fix_file_paths(file.strip()) for file in files_str.split(',')] - - return respond_to_get_raw_files(upload_hash, files, compress) - - -@app.route('%s/raw/<string:upload_hash>' % base_path, methods=['POST']) -def get_raw_files_post(upload_hash): - """ - Get multiple raw calculation files. - - .. :quickref: raw; Get multiple raw calculation files. - - **Example request**: - - .. sourcecode:: http - - POST /nomad/api/raw/W36aqCzAKxOCfIiMFsBJh3nHPb4a HTTP/1.1 - Accept: application/gz - Content-Type: application/json - - { - "files": ["Si/si.out", "Si/aux.txt"] - } - - :param string upload_hash: the hash based identifier of the upload - :jsonparam files: a comma separated list of file paths - :jsonparam compress: boolean to enable compression (true), default is not compression (false) - :resheader Content-Type: application/json - :status 200: calc raw data successfully retrieved - :status 404: calc with given hash does not exist or one of the given files does not exist - :returns: a streamed .zip archive with the raw data - """ - json_data = request.get_json() - if json_data is None: - json_data = {} - - if 'files' not in json_data: - abort(400, message='No files given, use key "files" in json body to provide file paths.') - compress = json_data.get('compress', False) - if not isinstance(compress, bool): - abort(400, message='Compress value %s is not a bool.' % str(compress)) - files = [fix_file_paths(file.strip()) for file in json_data['files']] - - return respond_to_get_raw_files(upload_hash, files, compress) +raw_file_compress_argument = dict( + name='compress', type=bool, help='Use compression on .zip files, default is not.', + location='args') +raw_file_from_path_parser = api.parser() +raw_file_from_path_parser.add_argument(**raw_file_compress_argument) + + +@ns.route('/<string:upload_hash>/<path:path>') +@api.doc(params={ + 'upload_hash': 'The unique hash for the requested upload.', + 'path': 'The path to a file or directory.' +}) +@api.header('Content-Type', 'application/gz') +class RawFileFromPath(Resource): + @api.response(404, 'The upload or path does not exist') + @api.response(200, 'File(s) send', headers={'Content-Type': 'application/gz'}) + @api.expect(raw_file_from_path_parser, validate=True) + @login_if_available + def get(self, upload_hash: str, path: str): + """ + Get a single raw calculation file or whole directory from a given upload. + + If the given path points to a file, the file is provided. If the given path + points to an directory, the directory and all contents is provided as .zip file. + """ + upload_filepath = fix_file_paths(path) + + repository_file = RepositoryFile(upload_hash) + if not repository_file.exists(): + abort(404, message='The upload with hash %s does not exist.' % upload_hash) + + if upload_filepath[-1:] == '*': + upload_filepath = upload_filepath[0:-1] + files = list( + file for file in repository_file.manifest + if file.startswith(upload_filepath)) + if len(files) == 0: + abort(404, message='There are no files for %s.' % upload_filepath) + else: + compress = request.args.get('compress', None) is not None + return respond_to_get_raw_files(upload_hash, files, compress) + + try: + the_file = repository_file.get_file(upload_filepath) + with the_file.open() as f: + rv = send_file( + f, + mimetype='application/octet-stream', + as_attachment=True, + attachment_filename=os.path.basename(upload_filepath)) + return rv + except KeyError: + files = list(file for file in repository_file.manifest if file.startswith(upload_filepath)) + if len(files) == 0: + abort(404, message='The file %s does not exist.' % upload_filepath) + else: + abort(404, message='The file %s does not exist, but there are files with matching paths' % upload_filepath, files=files) + except HTTPException as e: + raise e + except Exception as e: + logger = get_logger( + __name__, endpoint='raw', action='get', + upload_hash=upload_hash, upload_filepath=upload_filepath) + logger.error('Exception on accessing raw data', exc_info=e) + abort(500, message='Could not accessing the raw data.') + + +raw_files_request_model = api.model('RawFilesRequest', { + 'files': fields.List( + fields.String, default=[], description='List of files to download.'), + 'compress': fields.Boolean( + default=False, + description='Enable compression, default is not compression.') +}) + +raw_files_request_parser = api.parser() +raw_files_request_parser.add_argument(**raw_file_compress_argument) +raw_files_request_parser.add_argument( + 'files', required=True, type=str, help='Comma separated list of files to download.', location='args') + + +@ns.route('/<string:upload_hash>') +@api.doc(params={ + 'upload_hash': 'The unique hash for the requested upload.' +}) +class RawFiles(Resource): + @api.response(404, 'The upload or path does not exist') + @api.response(200, 'File(s) send', headers={'Content-Type': 'application/gz'}) + @api.expect(raw_files_request_model, validate=True) + @login_if_available + def post(self, upload_hash): + """ Download multiple raw calculation files. """ + json_data = request.get_json() + compress = json_data.get('compress', False) + files = [fix_file_paths(file.strip()) for file in json_data['files']] + + return respond_to_get_raw_files(upload_hash, files, compress) + + @api.response(404, 'The upload or path does not exist') + @api.response(200, 'File(s) send', headers={'Content-Type': 'application/gz'}) + @api.expect(raw_files_request_parser, validate=True) + @login_if_available + def get(self, upload_hash): + """ Download multiple raw calculation files. """ + files_str = request.args.get('files', None) + compress = request.args.get('compress', 'false') == 'true' + + if files_str is None: + abort(400, message="No files argument given.") + files = [fix_file_paths(file.strip()) for file in files_str.split(',')] + + return respond_to_get_raw_files(upload_hash, files, compress) def respond_to_get_raw_files(upload_hash, files, compress=False): diff --git a/tests/test_api.py b/tests/test_api.py index d8fddc427d42befc2ef06f317d9205e54f1e948b..6d40e8fc1b31f317dc378f0a57183014a39e4a2e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -317,7 +317,7 @@ def test_docs(client): class TestRaw: @pytest.fixture - def example_upload_hash(self, mockmongo, no_warn): + def example_upload_hash(self, mockmongo, repository_db, no_warn): upload = Upload(id='test_upload_id', local_path=os.path.abspath(example_file)) upload.create_time = datetime.datetime.now() upload.user_id = 'does@not.exist'