# -*- coding: utf-8 -*-
#
# This file is part of REANA.
# Copyright (C) 2020, 2021, 2022, 2024 CERN.
#
# REANA is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
"""REANA Workflow Controller workspaces REST API."""
import json
import os
import pathlib
from flask import (
Blueprint,
current_app,
jsonify,
request,
)
from fs.errors import CreateFailed
from werkzeug.datastructures import FileStorage
from werkzeug.exceptions import NotFound
from reana_commons import workspace
from reana_commons.errors import REANAWorkspaceError
from reana_db.models import User
from reana_db.utils import (
_get_workflow_with_uuid_or_name,
store_workflow_disk_quota,
update_users_disk_quota,
)
from reana_workflow_controller.errors import (
REANAWorkflowControllerError,
)
from reana_workflow_controller.rest.utils import (
get_workflow_name,
list_directory_files,
download_files_recursive_wildcard,
list_files_recursive_wildcard,
remove_files_recursive_wildcard,
use_paginate_args,
)
from reana_workflow_controller.rest.utils import mv_files
blueprint = Blueprint("workspaces", __name__)
[docs]@blueprint.route("/workflows/<workflow_id_or_name>/workspace", methods=["POST"])
def upload_file(workflow_id_or_name):
r"""Upload file to workspace.
---
post:
summary: Adds a file to the workspace.
description: >-
This resource is expecting a workflow UUID and a file to place in the
workspace.
operationId: upload_file
consumes:
- application/octet-stream
produces:
- application/json
parameters:
- name: user
in: query
description: Required. UUID of workflow owner.
required: true
type: string
- name: workflow_id_or_name
in: path
description: Required. Workflow UUID or name.
required: true
type: string
- name: file
in: body
description: Required. File to add to the workspace.
required: true
schema:
type: string
- name: file_name
in: query
description: Required. File name.
required: true
type: string
responses:
200:
description: >-
Request succeeded. The file has been added to the workspace.
schema:
type: object
properties:
message:
type: string
examples:
application/json:
{
"message": "`file_name` has been successfully uploaded.",
}
400:
description: >-
Request failed. The incoming data specification seems malformed
404:
description: >-
Request failed. Workflow does not exist.
schema:
type: object
properties:
message:
type: string
examples:
application/json:
{
"message": "Workflow cdcf48b1-c2f3-4693-8230-b066e088c6ac does
not exist",
}
500:
description: >-
Request failed. Internal controller error.
"""
try:
if not ("application/octet-stream" in request.headers.get("Content-Type")):
return (
jsonify(
{
"message": f"Wrong Content-Type "
f'{request.headers.get("Content-Type")} '
f"use application/octet-stream"
}
),
400,
)
user_uuid = request.args["user"]
full_file_name = request.args["file_name"]
if not full_file_name:
raise ValueError("The file transferred needs to have name.")
workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, user_uuid)
relative_path = pathlib.Path(full_file_name)
if relative_path.is_absolute():
relative_path = relative_path.relative_to("/")
workspace.makedirs(workflow.workspace_path, relative_path.parent)
with workspace.open_file(
workflow.workspace_path, relative_path, mode="wb"
) as dst:
FileStorage(request.stream).save(dst, buffer_size=32768)
# update user and workflow resource disk quota
store_workflow_disk_quota(workflow, bytes_to_sum=request.content_length)
update_users_disk_quota(workflow.owner, bytes_to_sum=request.content_length)
return (
jsonify(
{"message": "{} has been successfully uploaded.".format(full_file_name)}
),
200,
)
except ValueError:
return (
jsonify(
{
"message": "REANA_WORKON is set to {0}, but "
"that workflow does not exist. "
"Please set your REANA_WORKON environment"
"variable appropriately.".format(workflow_id_or_name)
}
),
404,
)
except KeyError as e:
return jsonify({"message": str(e)}), 400
except REANAWorkspaceError as e:
return jsonify({"message": str(e)}), 400
except Exception as e:
return jsonify({"message": str(e)}), 500
[docs]@blueprint.route(
"/workflows/<workflow_id_or_name>/workspace/<path:file_name>", methods=["GET"]
)
def download_file(workflow_id_or_name, file_name): # noqa
r"""Download a file from the workspace.
---
get:
summary: Returns the requested file.
description: >-
This resource is expecting a workflow UUID and a filename existing
inside the workspace to return its content.
operationId: download_file
produces:
- application/octet-stream
- application/json
- application/zip
- image/*
- text/html
parameters:
- name: user
in: query
description: Required. UUID of workflow owner.
required: true
type: string
- name: workflow_id_or_name
in: path
description: Required. Workflow UUID or name
required: true
type: string
- name: file_name
in: path
description: Required. Name (or path) of the file to be downloaded.
required: true
type: string
- name: preview
in: query
description: >-
Optional flag to return a previewable response of the file
(corresponding mime-type).
required: false
type: boolean
responses:
200:
description: >-
Requests succeeded. The file has been downloaded.
schema:
type: file
400:
description: >-
Request failed. The incoming data specification seems malformed.
404:
description: >-
Request failed. `file_name` does not exist.
examples:
application/json:
{
"message": "input.csv does not exist"
}
500:
description: >-
Request failed. Internal controller error.
examples:
application/json:
{
"message": "Internal workflow controller error."
}
"""
try:
user_uuid = request.args["user"]
user = User.query.filter(User.id_ == user_uuid).first()
if not user:
return jsonify({"message": "User {} does not exist".format(user)}), 404
workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, user_uuid)
workflow_name = workflow.get_full_workflow_name()
return download_files_recursive_wildcard(
workflow_name, workflow.workspace_path, file_name
)
except ValueError:
return (
jsonify(
{
"message": "REANA_WORKON is set to {0}, but "
"that workflow does not exist. "
"Please set your REANA_WORKON environment "
"variable appropriately.".format(workflow_id_or_name)
}
),
404,
)
except KeyError:
return jsonify({"message": "Malformed request."}), 400
except REANAWorkspaceError as e:
return jsonify({"message": str(e)}), 400
except NotFound:
return jsonify({"message": "{0} does not exist.".format(file_name)}), 404
except Exception as e:
return jsonify({"message": str(e)}), 500
[docs]@blueprint.route(
"/workflows/<workflow_id_or_name>/workspace/<path:file_name>", methods=["DELETE"]
)
def delete_file(workflow_id_or_name, file_name): # noqa
r"""Delete a file from the workspace.
---
delete:
summary: Delete the specified file.
description: >-
This resource is expecting a workflow UUID and a filename existing
inside the workspace to be deleted.
operationId: delete_file
produces:
- application/json
parameters:
- name: user
in: query
description: Required. UUID of workflow owner.
required: true
type: string
- name: workflow_id_or_name
in: path
description: Required. Workflow UUID or name
required: true
type: string
- name: file_name
in: path
description: Required. Name (or path) of the file to be deleted.
required: true
type: string
responses:
200:
description: >-
Request succeeded. Details about deleted files and failed deletions are returned.
schema:
type: file
404:
description: >-
Request failed. `file_name` does not exist.
examples:
application/json:
{
"message": "input.csv does not exist"
}
500:
description: >-
Request failed. Internal controller error.
examples:
application/json:
{
"message": "Internal workflow controller error."
}
"""
try:
user_uuid = request.args["user"]
user = User.query.filter(User.id_ == user_uuid).first()
if not user:
return jsonify({"message": "User {} does not exist".format(user)}), 404
workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, user_uuid)
deleted = remove_files_recursive_wildcard(workflow.workspace_path, file_name)
# update user and workflow resource disk quota
freed_up_bytes = sum(
size.get("size", 0) for size in deleted["deleted"].values()
)
store_workflow_disk_quota(workflow, bytes_to_sum=-freed_up_bytes)
update_users_disk_quota(user, bytes_to_sum=-freed_up_bytes)
return jsonify(deleted), 200
except ValueError:
return (
jsonify(
{
"message": "REANA_WORKON is set to {0}, but "
"that workflow does not exist. "
"Please set your REANA_WORKON environment "
"variable appropriately.".format(workflow_id_or_name)
}
),
404,
)
except KeyError:
return jsonify({"message": "Malformed request."}), 400
except REANAWorkspaceError as e:
return jsonify({"message": str(e)}), 400
except NotFound:
return jsonify({"message": "{0} does not exist.".format(file_name)}), 404
except OSError:
return jsonify({"message": "Error while deleting {}.".format(file_name)}), 500
except Exception as e:
return jsonify({"message": str(e)}), 500
[docs]@blueprint.route("/workflows/<workflow_id_or_name>/workspace", methods=["GET"])
@use_paginate_args()
def get_files(workflow_id_or_name, paginate=None): # noqa
r"""List all files contained in a workspace.
---
get:
summary: Returns the workspace file list.
description: >-
This resource retrieves the file list of a workspace, given
its workflow UUID.
operationId: get_files
produces:
- multipart/form-data
parameters:
- name: user
in: query
description: Required. UUID of workflow owner.
required: true
type: string
- name: workflow_id_or_name
in: path
description: Required. Workflow UUID or name.
required: true
type: string
- name: file_name
in: query
description: File name(s) (glob) to list.
required: false
type: string
- name: page
in: query
description: Results page number (pagination).
required: false
type: integer
- name: size
in: query
description: Number of results per page (pagination).
required: false
type: integer
- name: search
in: query
description: Filter workflow workspace files.
required: false
type: string
responses:
200:
description: >-
Requests succeeded. The list of code|input|output files has been
retrieved.
schema:
type: object
properties:
total:
type: integer
items:
type: array
items:
type: object
properties:
name:
type: string
last-modified:
type: string
size:
type: object
properties:
raw:
type: number
human_readable:
type: string
400:
description: >-
Request failed. The incoming data specification seems malformed.
404:
description: >-
Request failed. Workflow does not exist.
examples:
application/json:
{
"message": "Workflow 256b25f4-4cfb-4684-b7a8-73872ef455a1 does
not exist."
}
500:
description: >-
Request failed. Internal controller error.
examples:
application/json:
{
"message": "Internal workflow controller error."
}
"""
try:
user_uuid = request.args["user"]
search = request.args.get("search")
user = User.query.filter(User.id_ == user_uuid).first()
if not user:
return jsonify({"message": "User {} does not exist".format(user)}), 404
workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, user_uuid)
file_name = request.args.get("file_name")
if search:
search = json.loads(search)
if file_name:
file_list = list_files_recursive_wildcard(
workflow.workspace_path, file_name, search=search
)
else:
file_list = list_directory_files(workflow.workspace_path, search=search)
pagination_dict = paginate(file_list)
return jsonify(pagination_dict), 200
except ValueError:
return (
jsonify(
{
"message": "REANA_WORKON is set to {0}, but "
"that workflow does not exist. "
"Please set your REANA_WORKON environment "
"variable appropriately.".format(workflow_id_or_name)
}
),
404,
)
except KeyError:
return jsonify({"message": "Malformed request."}), 400
except REANAWorkspaceError as e:
return jsonify({"message": str(e)}), 400
except FileNotFoundError:
return jsonify({"message": "Workspace does not exist."}), 404
except Exception as e:
return jsonify({"message": str(e)}), 500
[docs]@blueprint.route("/workflows/move_files/<workflow_id_or_name>", methods=["PUT"])
def move_files(workflow_id_or_name): # noqa
r"""Move files within workspace.
---
put:
summary: Move files within workspace.
description: >-
This resource moves files within the workspace. Resource is expecting
a workflow UUID.
operationId: move_files
consumes:
- application/json
produces:
- application/json
parameters:
- name: workflow_id_or_name
in: path
description: Required. Analysis UUID or name.
required: true
type: string
- name: source
in: query
description: Required. Source file(s).
required: true
type: string
- name: target
in: query
description: Required. Target file(s).
required: true
type: string
- name: user
in: query
description: Required. UUID of workflow owner..
required: true
type: string
responses:
200:
description: >-
Request succeeded. Message about successfully moved files is
returned.
schema:
type: object
properties:
message:
type: string
workflow_id:
type: string
workflow_name:
type: string
examples:
application/json:
{
"message": "Files were successfully moved",
"workflow_id": "256b25f4-4cfb-4684-b7a8-73872ef455a1",
"workflow_name": "mytest.1",
}
400:
description: >-
Request failed. The incoming payload seems malformed.
examples:
application/json:
{
"message": "Malformed request."
}
403:
description: >-
Request failed. User is not allowed to access workflow.
examples:
application/json:
{
"message": "User 00000000-0000-0000-0000-000000000000
is not allowed to access workflow
256b25f4-4cfb-4684-b7a8-73872ef455a1"
}
404:
description: >-
Request failed. Either User or Workflow does not exist.
examples:
application/json:
{
"message": "Workflow 256b25f4-4cfb-4684-b7a8-73872ef455a1
does not exist"
}
500:
description: >-
Request failed. Internal controller error.
"""
try:
user_uuid = request.args["user"]
workflow = _get_workflow_with_uuid_or_name(workflow_id_or_name, user_uuid)
source = request.args["source"]
target = request.args["target"]
mv_files(source, target, workflow)
message = "File(s) {} were successfully moved".format(source)
return (
jsonify(
{
"message": message,
"workflow_id": workflow.id_,
"workflow_name": get_workflow_name(workflow),
}
),
200,
)
except ValueError:
return (
jsonify(
{
"message": "REANA_WORKON is set to {0}, but "
"that workflow does not exist. "
"Please set your REANA_WORKON environment "
"variable appropriately.".format(workflow_id_or_name)
}
),
404,
)
except REANAWorkflowControllerError as e:
return jsonify({"message": str(e)}), 409
except KeyError as e:
return jsonify({"message": str(e)}), 400
except NotImplementedError as e:
return jsonify({"message": str(e)}), 501
except Exception as e:
return jsonify({"message": str(e)}), 500