Skip to content

Commit

Permalink
Gateway/delete end point (#1554)
Browse files Browse the repository at this point in the history
* add support to new delete files endpoints

* fix get parameter

* fix tests

* delete programDelete fixture

* update file_storage/remove_file description
  • Loading branch information
korgan00 authored Dec 17, 2024
1 parent 47c3064 commit 41be2f0
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 56 deletions.
27 changes: 27 additions & 0 deletions gateway/api/services/file_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,30 @@ def get_file(self, file_name: str) -> Optional[Tuple[FileWrapper, str, int]]:
file_size = os.path.getsize(path_to_file)

return file_wrapper, file_type, file_size

def remove_file(self, file_name: str) -> bool:
"""
This method remove a file in the path of file_name
Returns:
- True if it was deleted
- False otherwise
"""

file_name_path = os.path.basename(file_name)
path_to_file = sanitize_file_path(os.path.join(self.file_path, file_name_path))

try:
os.remove(path_to_file)
except FileNotFoundError:
logger.warning(
"Directory %s does not exist for file %s.",
path_to_file,
file_name_path,
)
return False
except OSError as ex:
logger.warning("OSError: %s.", ex.strerror)
return False

return True
65 changes: 53 additions & 12 deletions gateway/api/v1/views/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,23 +125,64 @@ def provider_download(self, request):

@swagger_auto_schema(
operation_description="Deletes file uploaded or produced by the programs",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
"file": openapi.Schema(
type=openapi.TYPE_STRING, description="file name"
),
"provider": openapi.Schema(
type=openapi.TYPE_STRING, description="provider name"
),
},
required=["file"],
),
manual_parameters=[
openapi.Parameter(
"file",
openapi.IN_QUERY,
description="File name",
type=openapi.TYPE_STRING,
required=True,
),
openapi.Parameter(
"function",
openapi.IN_QUERY,
description="Qiskit Function title",
type=openapi.TYPE_STRING,
required=True,
),
openapi.Parameter(
"provider",
openapi.IN_QUERY,
description="Provider name",
type=openapi.TYPE_STRING,
required=False,
),
],
)
@action(methods=["DELETE"], detail=False)
def delete(self, request):
return super().delete(request)

@swagger_auto_schema(
operation_description="Deletes file uploaded or produced by the programs",
manual_parameters=[
openapi.Parameter(
"file",
openapi.IN_QUERY,
description="File name",
type=openapi.TYPE_STRING,
required=True,
),
openapi.Parameter(
"function",
openapi.IN_QUERY,
description="Qiskit Function title",
type=openapi.TYPE_STRING,
required=True,
),
openapi.Parameter(
"provider",
openapi.IN_QUERY,
description="Provider name",
type=openapi.TYPE_STRING,
required=True,
),
],
)
@action(methods=["DELETE"], detail=False, url_path="provider/delete")
def provider_delete(self, request):
return super().provider_delete(request)

@swagger_auto_schema(
operation_description="Upload selected file",
request_body=openapi.Schema(
Expand Down
125 changes: 99 additions & 26 deletions gateway/api/views/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -436,37 +436,110 @@ def provider_download(self, request):
def delete(self, request): # pylint: disable=invalid-name
"""Deletes file uploaded or produced by the programs,"""
# default response for file not found, overwritten if file is found
response = Response(
{"message": "Requested file was not found."},
status=status.HTTP_404_NOT_FOUND,
)
tracer = trace.get_tracer("gateway.tracer")
ctx = TraceContextTextMapPropagator().extract(carrier=request.headers)
with tracer.start_as_current_span("gateway.files.delete", context=ctx):
if request.data and "file" in request.data:
# look for file in user's folder
filename = os.path.basename(request.data["file"])
provider_name = request.data.get("provider")
user_dir = request.user.username
if provider_name is not None:
if self.check_user_has_provider(request.user, provider_name):
user_dir = provider_name
else:
return response
user_dir = os.path.join(
sanitize_file_path(settings.MEDIA_ROOT),
sanitize_file_path(user_dir),
# look for file in user's folder
username = request.user.username
file_name = sanitize_file_name(request.query_params.get("file", None))
provider_name = sanitize_name(request.query_params.get("provider"))
function_title = sanitize_name(request.query_params.get("function", None))
working_dir = WorkingDir.USER_STORAGE

if not all([file_name, function_title]):
return Response(
{"message": "File name and Qiskit Function title are mandatory"},
status=status.HTTP_400_BAD_REQUEST,
)

function = self.get_function(
user=request.user,
function_title=function_title,
provider_name=provider_name,
)

if not function:
if provider_name:
error_message = f"Qiskit Function {provider_name}/{function_title} doesn't exist." # pylint: disable=line-too-long
else:
error_message = f"Qiskit Function {function_title} doesn't exist."
return Response(
{"message": error_message},
status=status.HTTP_404_NOT_FOUND,
)
file_path = os.path.join(
sanitize_file_path(user_dir), sanitize_file_path(filename)

file_storage = FileStorage(
username=username,
working_dir=working_dir,
function_title=function_title,
provider_name=provider_name,
)
result = file_storage.remove_file(file_name=file_name)
if not result:
return Response(
{"message": "Requested file was not found."},
status=status.HTTP_404_NOT_FOUND,
)
if os.path.exists(user_dir) and os.path.exists(file_path) and filename:
os.remove(file_path)
response = Response(
{"message": "Requested file was deleted."},
status=status.HTTP_200_OK,
)
return response

return Response(
{"message": "Requested file was deleted."}, status=status.HTTP_200_OK
)

@action(methods=["DELETE"], detail=False, url_path="provider/delete")
def provider_delete(self, request): # pylint: disable=invalid-name
"""Deletes file uploaded or produced by the programs,"""
# default response for file not found, overwritten if file is found
tracer = trace.get_tracer("gateway.tracer")
ctx = TraceContextTextMapPropagator().extract(carrier=request.headers)
with tracer.start_as_current_span("gateway.files.delete", context=ctx):
# look for file in user's folder
username = request.user.username
file_name = sanitize_file_name(request.query_params.get("file"))
provider_name = sanitize_name(request.query_params.get("provider"))
function_title = sanitize_name(request.query_params.get("function", None))
working_dir = WorkingDir.USER_STORAGE

if not all([file_name, function_title, provider_name]):
return Response(
{"message": "File name and Qiskit Function title are mandatory"},
status=status.HTTP_400_BAD_REQUEST,
)

if not self.user_has_provider_access(request.user, provider_name):
return Response(
{"message": f"Provider {provider_name} doesn't exist."},
status=status.HTTP_404_NOT_FOUND,
)

function = self.get_function(
user=request.user,
function_title=function_title,
provider_name=provider_name,
)

if not function:
error_message = f"Qiskit Function {provider_name}/{function_title} doesn't exist." # pylint: disable=line-too-long
return Response(
{"message": error_message},
status=status.HTTP_404_NOT_FOUND,
)

file_storage = FileStorage(
username=username,
working_dir=working_dir,
function_title=function_title,
provider_name=provider_name,
)
result = file_storage.remove_file(file_name=file_name)
if not result:
return Response(
{"message": "Requested file was not found."},
status=status.HTTP_404_NOT_FOUND,
)

return Response(
{"message": "Requested file was deleted."}, status=status.HTTP_200_OK
)

@action(methods=["POST"], detail=False)
def upload(self, request): # pylint: disable=invalid-name
Expand Down
74 changes: 56 additions & 18 deletions gateway/tests/api/test_v1_files.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"""Tests files api."""

import os
from urllib.parse import quote_plus
from urllib.parse import quote_plus, urlencode

from django.urls import reverse
from pytest import mark
from rest_framework import status
from rest_framework.test import APITestCase
from django.contrib.auth import models
Expand Down Expand Up @@ -306,21 +307,28 @@ def test_file_delete(self):
"fake_media",
)
media_root = os.path.normpath(os.path.join(os.getcwd(), media_root))
function = "personal-program"
file = "artifact_delete.tar"
username = "test_user_2"
functionPath = os.path.join(media_root, username)

if not os.path.exists(functionPath):
os.makedirs(functionPath)

with open(
os.path.join(media_root, "test_user", "artifact_delete.tar"), "w"
os.path.join(functionPath, file),
"w+",
) as fp:
fp.write("This is first line")
print(fp)
fp.close()

with self.settings(MEDIA_ROOT=media_root):
user = models.User.objects.get(username="test_user")
query_params = {"function": function, "file": file}
user = models.User.objects.get(username=username)
self.client.force_authenticate(user=user)
url = reverse("v1:files-delete")
response = self.client.delete(
url, data={"file": "artifact_delete.tar"}, format="json"
)
response = self.client.delete(f"{url}?{urlencode(query_params)}")
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_provider_file_delete(self):
Expand All @@ -332,23 +340,29 @@ def test_provider_file_delete(self):
"fake_media",
)
media_root = os.path.normpath(os.path.join(os.getcwd(), media_root))
provider = "default"
function = "Program"
file = "artifact_delete.tar"
username = "test_user_2"
functionPath = os.path.join(media_root, username, provider, function)

if not os.path.exists(functionPath):
os.makedirs(functionPath)

with open(
os.path.join(media_root, "default", "artifact_delete.tar"), "w"
os.path.join(functionPath, file),
"w+",
) as fp:
fp.write("This is first line")
print(fp)
fp.close()

with self.settings(MEDIA_ROOT=media_root):
user = models.User.objects.get(username="test_user_2")
query_params = {"function": function, "provider": provider, "file": file}
user = models.User.objects.get(username=username)
self.client.force_authenticate(user=user)
url = reverse("v1:files-delete")
response = self.client.delete(
url,
data={"file": "artifact_delete.tar", "provider": "default"},
format="json",
)
url = reverse("v1:files-provider-delete")
response = self.client.delete(f"{url}?{urlencode(query_params)}")
self.assertEqual(response.status_code, status.HTTP_200_OK)

def test_non_existing_file_delete(self):
Expand All @@ -360,14 +374,38 @@ def test_non_existing_file_delete(self):
"fake_media",
)
media_root = os.path.normpath(os.path.join(os.getcwd(), media_root))
function = "personal-program"
file = "non-existing-artifact_delete.tar"
username = "test_user_2"

with self.settings(MEDIA_ROOT=media_root):
user = models.User.objects.get(username="test_user")
query_params = {"function": function, "file": file}
user = models.User.objects.get(username=username)
self.client.force_authenticate(user=user)
url = reverse("v1:files-delete")
response = self.client.delete(
url, data={"file": "artifact_delete.tar"}, format="json"
)
response = self.client.delete(f"{url}?{urlencode(query_params)}")
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_non_existing_provider_file_delete(self):
"""Tests delete file."""
media_root = os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"..",
"resources",
"fake_media",
)
media_root = os.path.normpath(os.path.join(os.getcwd(), media_root))
provider = "default"
function = "Program"
file = "non-existing-artifact_delete.tar"
username = "test_user_2"

with self.settings(MEDIA_ROOT=media_root):
query_params = {"function": function, "provider": provider, "file": file}
user = models.User.objects.get(username=username)
self.client.force_authenticate(user=user)
url = reverse("v1:files-provider-delete")
response = self.client.delete(f"{url}?{urlencode(query_params)}")
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

def test_file_upload(self):
Expand Down

0 comments on commit 41be2f0

Please sign in to comment.