Skip to content

Commit 3df89fc

Browse files
improve delete workspace
1 parent ce4c77a commit 3df89fc

File tree

12 files changed

+328
-14
lines changed

12 files changed

+328
-14
lines changed

services/web/server/src/simcore_service_webserver/folders/_folders_repository.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,14 @@ async def list_( # pylint: disable=too-many-arguments,too-many-branches
260260
return cast(int, total_count), folders
261261

262262

263-
async def list_trashed_folders(
263+
async def list_folders_db_as_admin(
264264
app: web.Application,
265265
connection: AsyncConnection | None = None,
266266
*,
267267
# filter
268268
trashed_explicitly: bool | UnSet = UnSet.VALUE,
269269
trashed_before: datetime | UnSet = UnSet.VALUE,
270+
shared_workspace_id: WorkspaceID | UnSet = UnSet.VALUE, # <-- Workspace filter
270271
# pagination
271272
offset: NonNegativeInt,
272273
limit: int,
@@ -275,7 +276,6 @@ async def list_trashed_folders(
275276
) -> tuple[int, list[FolderDB]]:
276277
"""
277278
NOTE: this is app-wide i.e. no product, user or workspace filtered
278-
TODO: check with MD about workspaces
279279
"""
280280
base_query = sql.select(*_FOLDER_DB_MODEL_COLS).where(
281281
folders_v2.c.trashed.is_not(None)
@@ -291,6 +291,10 @@ async def list_trashed_folders(
291291
assert isinstance(trashed_before, datetime) # nosec
292292
base_query = base_query.where(folders_v2.c.trashed < trashed_before)
293293

294+
if is_set(shared_workspace_id):
295+
assert isinstance(shared_workspace_id, int) # nosec
296+
base_query = base_query.where(folders_v2.c.workspace_id == shared_workspace_id)
297+
294298
# Select total count from base_query
295299
count_query = sql.select(sql.func.count()).select_from(base_query.subquery())
296300

services/web/server/src/simcore_service_webserver/folders/_trash_service.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from models_library.rest_ordering import OrderBy, OrderDirection
1313
from models_library.rest_pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE
1414
from models_library.users import UserID
15+
from models_library.workspaces import WorkspaceID
1516
from simcore_postgres_database.utils_repos import transaction_context
1617
from sqlalchemy.ext.asyncio import AsyncConnection
1718

@@ -229,7 +230,7 @@ async def list_explicitly_trashed_folders(
229230
user_id=user_id,
230231
product_name=product_name,
231232
text=None,
232-
trashed=True, # NOTE: lists only expliclty trashed!
233+
trashed=True, # NOTE: lists only explicitly trashed!
233234
offset=page_params.offset,
234235
limit=page_params.limit,
235236
order_by=OrderBy(field=IDStr("trashed"), direction=OrderDirection.ASC),
@@ -300,7 +301,7 @@ async def batch_delete_trashed_folders_as_admin(
300301
(
301302
page_params.total_number_of_items,
302303
expired_trashed_folders,
303-
) = await _folders_repository.list_trashed_folders(
304+
) = await _folders_repository.list_folders_db_as_admin(
304305
app,
305306
trashed_explicitly=True,
306307
trashed_before=trashed_before,
@@ -326,3 +327,48 @@ async def batch_delete_trashed_folders_as_admin(
326327
raise FolderBatchDeleteError(
327328
errors=errors, trashed_before=trashed_before, product_name=product_name
328329
)
330+
331+
332+
async def batch_delete_folders_with_content_in_root_workspace_as_admin(
333+
app: web.Application,
334+
*,
335+
workspace_id: WorkspaceID,
336+
product_name: ProductName,
337+
fail_fast: bool,
338+
) -> None:
339+
"""
340+
Deletes all projects in the workspace root.
341+
342+
Raises:
343+
ProjectsBatchDeleteError: If there are errors during the deletion process.
344+
"""
345+
deleted_folder_ids: list[FolderID] = []
346+
errors: list[tuple[FolderID, Exception]] = []
347+
348+
for page_params in iter_pagination_params(limit=MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE):
349+
(
350+
page_params.total_number_of_items,
351+
folders_for_deletion,
352+
) = await _folders_repository.list_folders_db_as_admin(
353+
app,
354+
shared_workspace_id=workspace_id, # <-- Workspace filter
355+
offset=page_params.offset,
356+
limit=page_params.limit,
357+
order_by=OrderBy(field=IDStr("id")),
358+
)
359+
# BATCH delete
360+
for folder in folders_for_deletion:
361+
try:
362+
await _folders_repository.delete_recursively(
363+
app, folder_id=folder.folder_id, product_name=product_name
364+
)
365+
deleted_folder_ids.append(folder.folder_id)
366+
except Exception as err: # pylint: disable=broad-exception-caught
367+
if fail_fast:
368+
raise
369+
errors.append((folder.folder_id, err))
370+
371+
if errors:
372+
raise FolderBatchDeleteError(
373+
errors=errors,
374+
)

services/web/server/src/simcore_service_webserver/projects/_projects_repository.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from models_library.projects import ProjectID
1212
from models_library.rest_ordering import OrderBy, OrderDirection
1313
from models_library.rest_pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE
14+
from models_library.workspaces import WorkspaceID
1415
from pydantic import NonNegativeInt, PositiveInt
1516
from simcore_postgres_database.models.projects import projects
1617
from simcore_postgres_database.models.users import users
@@ -48,18 +49,19 @@ def _to_sql_expression(table: sa.Table, order_by: OrderBy):
4849
return direction_func(table.columns[order_by.field])
4950

5051

51-
async def list_trashed_projects(
52+
async def list_projects_db_get_as_admin(
5253
app: web.Application,
5354
connection: AsyncConnection | None = None,
5455
*,
5556
# filter
5657
trashed_explicitly: bool | UnSet = UnSet.VALUE,
5758
trashed_before: datetime | UnSet = UnSet.VALUE,
59+
shared_workspace_id: WorkspaceID | UnSet = UnSet.VALUE,
5860
# pagination
5961
offset: NonNegativeInt = 0,
6062
limit: PositiveInt = MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE,
6163
# order
62-
order_by: OrderBy = OLDEST_TRASHED_FIRST,
64+
order_by: OrderBy,
6365
) -> tuple[int, list[ProjectDBGet]]:
6466

6567
base_query = sql.select(*PROJECT_DB_COLS).where(projects.c.trashed.is_not(None))
@@ -74,12 +76,16 @@ async def list_trashed_projects(
7476
assert isinstance(trashed_before, datetime) # nosec
7577
base_query = base_query.where(projects.c.trashed < trashed_before)
7678

79+
if is_set(shared_workspace_id):
80+
assert isinstance(shared_workspace_id, int) # nosec
81+
base_query = base_query.where(projects.c.workspace_id == shared_workspace_id)
82+
7783
# Select total count from base_query
7884
count_query = sql.select(sql.func.count()).select_from(base_query.subquery())
7985

8086
# Ordering and pagination
8187
list_query = (
82-
base_query.order_by(_to_sql_expression(projects, order_by))
88+
base_query.order_by(_to_sql_expression(projects, order_by), projects.c.id)
8389
.offset(offset)
8490
.limit(limit)
8591
)

services/web/server/src/simcore_service_webserver/projects/_trash_service.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from models_library.rest_ordering import OrderBy, OrderDirection
1111
from models_library.rest_pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE
1212
from models_library.users import UserID
13+
from models_library.workspaces import WorkspaceID
1314
from servicelib.aiohttp.application_keys import APP_FIRE_AND_FORGET_TASKS_KEY
1415
from servicelib.utils import fire_and_forget_task
1516

@@ -241,7 +242,7 @@ async def batch_delete_trashed_projects_as_admin(
241242
(
242243
page_params.total_number_of_items,
243244
expired_trashed_projects,
244-
) = await _projects_repository.list_trashed_projects(
245+
) = await _projects_repository.list_projects_db_get_as_admin(
245246
app,
246247
# both implicit and explicitly trashed
247248
trashed_before=trashed_before,
@@ -273,3 +274,51 @@ async def batch_delete_trashed_projects_as_admin(
273274
)
274275

275276
return deleted_project_ids
277+
278+
279+
async def batch_delete_projects_in_root_workspace_as_admin(
280+
app: web.Application,
281+
*,
282+
workspace_id: WorkspaceID,
283+
fail_fast: bool,
284+
) -> list[ProjectID]:
285+
"""
286+
Deletes all projects in the workspace root.
287+
288+
Raises:
289+
ProjectsBatchDeleteError: If there are errors during the deletion process.
290+
"""
291+
deleted_project_ids: list[ProjectID] = []
292+
errors: list[tuple[ProjectID, Exception]] = []
293+
294+
for page_params in iter_pagination_params(limit=MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE):
295+
(
296+
page_params.total_number_of_items,
297+
projects_for_deletion,
298+
) = await _projects_repository.list_projects_db_get_as_admin(
299+
app,
300+
shared_workspace_id=workspace_id, # <-- Workspace filter
301+
offset=page_params.offset,
302+
limit=page_params.limit,
303+
order_by=OrderBy(field=IDStr("id")),
304+
)
305+
# BATCH delete
306+
for project in projects_for_deletion:
307+
try:
308+
await _projects_service_delete.delete_project_as_admin(
309+
app,
310+
project_uuid=project.uuid,
311+
)
312+
deleted_project_ids.append(project.uuid)
313+
except Exception as err: # pylint: disable=broad-exception-caught
314+
if fail_fast:
315+
raise
316+
errors.append((project.uuid, err))
317+
318+
if errors:
319+
raise ProjectsBatchDeleteError(
320+
errors=errors,
321+
deleted_project_ids=deleted_project_ids,
322+
)
323+
324+
return deleted_project_ids

services/web/server/src/simcore_service_webserver/trash/_service.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,23 @@ async def safe_delete_expired_trash_as_admin(app: web.Application) -> None:
218218
error_context=ctx,
219219
)
220220
)
221+
222+
try:
223+
deleted_workspace_ids = await workspaces_trash_service.batch_delete_trashed_workspaces_as_admin(
224+
app,
225+
trashed_before=delete_until,
226+
fail_fast=False,
227+
)
228+
229+
_logger.info(
230+
"Deleted %d trashed workspaces", len(deleted_workspace_ids)
231+
)
232+
233+
except Exception as exc: # pylint: disable=broad-exception-caught
234+
_logger.warning(
235+
**create_troubleshotting_log_kwargs(
236+
"Error batch deleting expired projects as admin.",
237+
error=exc,
238+
error_context=ctx,
239+
)
240+
)

services/web/server/src/simcore_service_webserver/workspaces/_trash_service.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,19 @@
2020

2121
from ..db.plugin import get_asyncpg_engine
2222
from ..folders._folders_service import list_folders
23-
from ..folders._trash_service import trash_folder, untrash_folder
23+
from ..folders._trash_service import (
24+
batch_delete_folders_with_content_in_root_workspace_as_admin,
25+
trash_folder,
26+
untrash_folder,
27+
)
2428
from ..projects._crud_api_read import ProjectTypeAPI, list_projects
25-
from ..projects._trash_service import trash_project, untrash_project
29+
from ..projects._trash_service import (
30+
batch_delete_projects_in_root_workspace_as_admin,
31+
trash_project,
32+
untrash_project,
33+
)
2634
from . import _workspaces_repository, _workspaces_service, _workspaces_service_crud_read
27-
from .errors import WorkspaceNotTrashedError
35+
from .errors import WorkspaceBatchDeleteError, WorkspaceNotTrashedError
2836

2937
_logger = logging.getLogger(__name__)
3038

@@ -301,3 +309,63 @@ async def list_trashed_workspaces(
301309
)
302310

303311
return trashed_workspace_ids
312+
313+
314+
async def batch_delete_trashed_workspaces_as_admin(
315+
app: web.Application,
316+
*,
317+
trashed_before: datetime,
318+
fail_fast: bool,
319+
) -> list[WorkspaceID]:
320+
321+
deleted_workspace_ids: list[WorkspaceID] = []
322+
errors: list[tuple[WorkspaceID, Exception]] = []
323+
324+
for page_params in iter_pagination_params(limit=MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE):
325+
(
326+
page_params.total_number_of_items,
327+
expired_trashed_workspaces,
328+
) = await _workspaces_repository.list_workspaces_db_get_as_admin(
329+
app,
330+
trashed_before=trashed_before,
331+
offset=page_params.offset,
332+
limit=page_params.limit,
333+
order_by=OrderBy(field=IDStr("trashed"), direction=OrderDirection.ASC),
334+
)
335+
# BATCH delete
336+
for trashed_workspace in expired_trashed_workspaces:
337+
assert trashed_workspace.trashed # nosec
338+
deleted_workspace_ids.append(trashed_workspace.workspace_id)
339+
340+
try:
341+
await batch_delete_projects_in_root_workspace_as_admin(
342+
app, workspace_id=trashed_workspace.workspace_id, fail_fast=False
343+
)
344+
except Exception as err: # pylint: disable=broad-exception-caught
345+
if fail_fast:
346+
raise
347+
errors.append((trashed_workspace.workspace_id, err))
348+
349+
try:
350+
workspace_db_get = await _workspaces_repository.get_workspace_db_get(
351+
app, workspace_id=trashed_workspace.workspace_id
352+
)
353+
354+
await batch_delete_folders_with_content_in_root_workspace_as_admin(
355+
app,
356+
workspace_id=trashed_workspace.workspace_id,
357+
product_name=workspace_db_get.product_name,
358+
fail_fast=False,
359+
)
360+
except Exception as err: # pylint: disable=broad-exception-caught
361+
if fail_fast:
362+
raise
363+
errors.append((trashed_workspace.workspace_id, err))
364+
365+
if errors:
366+
raise WorkspaceBatchDeleteError(
367+
errors=errors,
368+
deleted_workspace_ids=deleted_workspace_ids,
369+
)
370+
371+
return deleted_workspace_ids
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from datetime import datetime
2+
3+
from models_library.groups import GroupID
4+
from models_library.products import ProductName
5+
from models_library.users import UserID
6+
from models_library.workspaces import WorkspaceID
7+
from pydantic import (
8+
BaseModel,
9+
ConfigDict,
10+
Field,
11+
)
12+
13+
14+
class WorkspaceDBGet(BaseModel):
15+
workspace_id: WorkspaceID
16+
name: str
17+
description: str | None
18+
owner_primary_gid: GroupID = Field(
19+
...,
20+
description="GID of the group that owns this wallet",
21+
)
22+
thumbnail: str | None
23+
created: datetime = Field(
24+
...,
25+
description="Timestamp on creation",
26+
)
27+
modified: datetime = Field(
28+
...,
29+
description="Timestamp of last modification",
30+
)
31+
trashed: datetime | None
32+
trashed_by: UserID | None
33+
product_name: ProductName
34+
35+
model_config = ConfigDict(from_attributes=True)

0 commit comments

Comments
 (0)