Skip to content

Commit 45ca588

Browse files
committed
new entrypoint for batch deletion
1 parent ac15068 commit 45ca588

File tree

5 files changed

+151
-12
lines changed

5 files changed

+151
-12
lines changed

api/specs/web-server/_storage.py

+13-12
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@ async def compute_path_size(_path: Annotated[StoragePathComputeSizeParams, Depen
8181
"""Compute the size of a path"""
8282

8383

84+
@router.post(
85+
"/storage/locations/{location_id}/-/paths:batchDelete",
86+
response_model=Envelope[TaskGet],
87+
status_code=status.HTTP_202_ACCEPTED,
88+
description="Deletes Paths",
89+
)
90+
async def batch_delete_paths(
91+
_path: Annotated[StorageLocationPathParams, Depends()],
92+
_body: Annotated[BatchDeletePathsBodyParams, Depends()],
93+
):
94+
"""deletes files/folders if user has the rights to"""
95+
96+
8497
@router.get(
8598
"/storage/locations/{location_id}/datasets",
8699
response_model=Envelope[list[DatasetMetaData]],
@@ -174,18 +187,6 @@ async def delete_file(location_id: LocationID, file_id: StorageFileIDStr):
174187
"""deletes file if user has the rights to"""
175188

176189

177-
@router.post(
178-
"/storage/locations/{location_id}/-/files:batchDelete",
179-
status_code=status.HTTP_204_NO_CONTENT,
180-
description="Deletes Files",
181-
)
182-
async def batch_delete_files(
183-
_path: Annotated[StorageLocationPathParams, Depends()],
184-
_body: Annotated[BatchDeletePathsBodyParams, Depends()],
185-
):
186-
"""deletes file if user has the rights to"""
187-
188-
189190
@router.post(
190191
"/storage/locations/{location_id}/files/{file_id}:abort",
191192
status_code=status.HTTP_204_NO_CONTENT,

packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/storage/paths.py

+20
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,23 @@ async def compute_path_size(
3232
path=path,
3333
)
3434
return async_job_rpc_get, job_id_data
35+
36+
37+
async def delete_paths(
38+
client: RabbitMQRPCClient,
39+
*,
40+
user_id: UserID,
41+
product_name: ProductName,
42+
location_id: LocationID,
43+
paths: set[Path],
44+
) -> tuple[AsyncJobGet, AsyncJobNameData]:
45+
job_id_data = AsyncJobNameData(user_id=user_id, product_name=product_name)
46+
async_job_rpc_get = await submit(
47+
rabbitmq_rpc_client=client,
48+
rpc_namespace=STORAGE_RPC_NAMESPACE,
49+
method_name=RPCMethodName("delete_paths"),
50+
job_id_data=job_id_data,
51+
location_id=location_id,
52+
paths=paths,
53+
)
54+
return async_job_rpc_get, job_id_data

services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

+32
Original file line numberDiff line numberDiff line change
@@ -6168,6 +6168,38 @@ paths:
61686168
application/json:
61696169
schema:
61706170
$ref: '#/components/schemas/Envelope_TaskGet_'
6171+
/v0/storage/locations/{location_id}/-/paths:batchDelete:
6172+
post:
6173+
tags:
6174+
- storage
6175+
summary: Batch Delete Paths
6176+
description: Deletes Paths
6177+
operationId: batch_delete_paths
6178+
parameters:
6179+
- name: location_id
6180+
in: path
6181+
required: true
6182+
schema:
6183+
type: integer
6184+
title: Location Id
6185+
requestBody:
6186+
required: true
6187+
content:
6188+
application/json:
6189+
schema:
6190+
type: array
6191+
uniqueItems: true
6192+
items:
6193+
type: string
6194+
format: path
6195+
title: Paths
6196+
responses:
6197+
'202':
6198+
description: Successful Response
6199+
content:
6200+
application/json:
6201+
schema:
6202+
$ref: '#/components/schemas/Envelope_TaskGet_'
61716203
/v0/storage/locations/{location_id}/datasets:
61726204
get:
61736205
tags:

services/web/server/src/simcore_service_webserver/storage/_rest.py

+40
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
LinkType,
2323
)
2424
from models_library.api_schemas_webserver.storage import (
25+
BatchDeletePathsBodyParams,
2526
DataExportPost,
27+
StorageLocationPathParams,
2628
StoragePathComputeSizeParams,
2729
)
2830
from models_library.projects_nodes_io import LocationID
@@ -42,6 +44,9 @@
4244
from servicelib.rabbitmq.rpc_interfaces.storage.paths import (
4345
compute_path_size as remote_compute_path_size,
4446
)
47+
from servicelib.rabbitmq.rpc_interfaces.storage.paths import (
48+
delete_paths as remote_delete_paths,
49+
)
4550
from servicelib.request_keys import RQT_USERID_KEY
4651
from servicelib.rest_responses import unwrap_envelope
4752
from yarl import URL
@@ -206,6 +211,41 @@ async def compute_path_size(request: web.Request) -> web.Response:
206211
)
207212

208213

214+
@routes.post(
215+
f"{_storage_locations_prefix}/{{location_id}}/-/paths:batchDelete",
216+
name="batch_delete_paths",
217+
)
218+
@login_required
219+
@permission_required("storage.files.*")
220+
async def batch_delete_paths(
221+
request: web.Request,
222+
):
223+
req_ctx = RequestContext.model_validate(request)
224+
path_params = parse_request_path_parameters_as(StorageLocationPathParams, request)
225+
body = await parse_request_body_as(BatchDeletePathsBodyParams, request)
226+
227+
rabbitmq_rpc_client = get_rabbitmq_rpc_client(request.app)
228+
async_job, _ = await remote_delete_paths(
229+
rabbitmq_rpc_client,
230+
user_id=req_ctx.user_id,
231+
product_name=req_ctx.product_name,
232+
location_id=path_params.location_id,
233+
paths=body.paths,
234+
)
235+
236+
_job_id = f"{async_job.job_id}"
237+
return create_data_response(
238+
TaskGet(
239+
task_id=_job_id,
240+
task_name=_job_id,
241+
status_href=f"{request.url.with_path(str(request.app.router['get_async_job_status'].url_for(task_id=_job_id)))}",
242+
abort_href=f"{request.url.with_path(str(request.app.router['abort_async_job'].url_for(task_id=_job_id)))}",
243+
result_href=f"{request.url.with_path(str(request.app.router['get_async_job_result'].url_for(task_id=_job_id)))}",
244+
),
245+
status=status.HTTP_202_ACCEPTED,
246+
)
247+
248+
209249
@routes.get(
210250
_storage_locations_prefix + "/{location_id}/datasets", name="list_datasets_metadata"
211251
)

services/web/server/tests/unit/with_dbs/01/storage/test_storage.py

+46
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
)
4545
from models_library.api_schemas_webserver._base import OutputSchema
4646
from models_library.api_schemas_webserver.storage import (
47+
BatchDeletePathsBodyParams,
4748
DataExportPost,
4849
)
4950
from models_library.generics import Envelope
@@ -190,6 +191,51 @@ async def test_compute_path_size(
190191
TypeAdapter(TaskGet).validate_python(data)
191192

192193

194+
@pytest.mark.parametrize(
195+
"user_role,expected",
196+
[
197+
(UserRole.ANONYMOUS, status.HTTP_401_UNAUTHORIZED),
198+
(UserRole.GUEST, status.HTTP_202_ACCEPTED),
199+
(UserRole.USER, status.HTTP_202_ACCEPTED),
200+
(UserRole.TESTER, status.HTTP_202_ACCEPTED),
201+
],
202+
)
203+
@pytest.mark.parametrize(
204+
"backend_result_or_exception",
205+
[
206+
AsyncJobGet(job_id=AsyncJobId(f"{_faker.uuid4()}")),
207+
],
208+
ids=lambda x: type(x).__name__,
209+
)
210+
async def test_batch_delete_paths(
211+
client: TestClient,
212+
logged_user: dict[str, Any],
213+
expected: int,
214+
location_id: LocationID,
215+
faker: Faker,
216+
create_storage_paths_rpc_client_mock: Callable[[str, Any], None],
217+
backend_result_or_exception: Any,
218+
):
219+
create_storage_paths_rpc_client_mock(
220+
submit.__name__,
221+
backend_result_or_exception,
222+
)
223+
224+
body = BatchDeletePathsBodyParams(
225+
paths={Path(f"{faker.file_path(absolute=False)}")}
226+
)
227+
228+
assert client.app
229+
url = client.app.router["batch_delete_paths"].url_for(
230+
location_id=f"{location_id}",
231+
)
232+
233+
resp = await client.post(f"{url}", json=body.model_dump(mode="json"))
234+
data, error = await assert_status(resp, expected)
235+
if not error:
236+
TypeAdapter(TaskGet).validate_python(data)
237+
238+
193239
@pytest.mark.parametrize(
194240
"user_role,expected",
195241
[

0 commit comments

Comments
 (0)