Skip to content

Commit aa71d1d

Browse files
committed
safe empty trash
1 parent a8cff19 commit aa71d1d

File tree

3 files changed

+80
-40
lines changed

3 files changed

+80
-40
lines changed

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

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,33 @@ async def untrash_project(
129129
)
130130

131131

132-
def _get_trashed_fields(project: ProjectDict):
132+
def _can_delete(
133+
project: ProjectDict,
134+
user_id: UserID,
135+
until_equal_datetime: datetime.datetime | None,
136+
) -> bool:
137+
"""
138+
This is the current policy to delete trashed project
139+
140+
"""
133141
trashed_at = project.get("trashed")
134142
trashed_by = project.get("trashedBy")
135143
trashed_explicitly = project.get("trashedExplicitly")
136-
return trashed_at, trashed_by, trashed_explicitly
144+
145+
assert trashed_at is not None # nosec
146+
assert trashed_by is not None # nosec
147+
148+
is_shared = len(project["accessRights"]) > 1
149+
150+
return bool(
151+
trashed_at
152+
and (until_equal_datetime is None or trashed_at < until_equal_datetime)
153+
# NOTE: current policy is more restricted until
154+
# logic is adapted to deal with the other cases
155+
and trashed_by == user_id
156+
and not is_shared
157+
and trashed_explicitly
158+
)
137159

138160

139161
async def list_trashed_projects(
@@ -170,18 +192,11 @@ async def list_trashed_projects(
170192
# This filtering couldn't be handled at the database level when `projects_repo`
171193
# was refactored, as defining a custom trash_filter was needed to allow more
172194
# flexibility in filtering options.
173-
174-
for project in projects:
175-
trashed_at, trashed_by, trashed_explicitly = _get_trashed_fields(project)
176-
177-
if (
178-
trashed_at
179-
and (until_equal_datetime is None or trashed_at < until_equal_datetime)
180-
and trashed_by == user_id
181-
and trashed_explicitly
182-
):
183-
trashed_projects.append(project["uuid"])
184-
195+
trashed_projects = [
196+
project["uuid"]
197+
for project in projects
198+
if _can_delete(project, user_id, until_equal_datetime)
199+
]
185200
return trashed_projects
186201

187202

@@ -206,22 +221,16 @@ async def delete_trashed_project(
206221
if not project:
207222
raise ProjectNotFoundError(project_uuid=project_id, user_id=user_id)
208223

209-
trashed_at, trashed_by, trashed_explicitly = _get_trashed_fields(project)
210-
211-
if (
212-
not trashed_at
213-
or (until_equal_datetime is not None and until_equal_datetime < trashed_at)
214-
or trashed_by != user_id
215-
or not trashed_explicitly
216-
):
224+
if not _can_delete(project, user_id, until_equal_datetime):
225+
# safety check
217226
raise ProjectNotTrashedError(
218227
project_uuid=project_id,
219228
user_id=user_id,
220-
reason="Project was not trashed explicitly by the user from the specified datetime",
229+
reason="Cannot delete trashed project since it does not fit current criteria",
221230
)
222-
# FIXME: locking while deleting?
223-
# FIXME: this can be heavy. Rather call delete_trashed_project as a task
231+
224232
await projects_service.delete_project_by_user(
233+
# FIXME: locking while deleting?
225234
app,
226235
user_id=user_id,
227236
project_uuid=project_id,

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

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,77 @@
22
import logging
33
from datetime import timedelta
44

5+
import arrow
56
from aiohttp import web
67
from models_library.products import ProductName
78
from models_library.users import UserID
9+
from servicelib.logging_errors import create_troubleshotting_log_kwargs
10+
from servicelib.logging_utils import log_context
811

912
from ..projects import _trash_service
1013
from .settings import get_plugin_settings
1114

1215
_logger = logging.getLogger(__name__)
1316

1417

15-
async def empty_trash(app: web.Application, product_name: ProductName, user_id: UserID):
18+
async def empty_trash_safe(
19+
app: web.Application, product_name: ProductName, user_id: UserID
20+
):
1621
assert app # nosec
17-
# filter trashed=True and set them to False
18-
_logger.debug(
19-
"CODE PLACEHOLDER: all projects marked as trashed of %s in %s are deleted",
20-
f"{user_id=}",
21-
f"{product_name=}",
22-
)
2322

2423
trashed_projects_ids = await _trash_service.list_trashed_projects(
2524
app=app, product_name=product_name, user_id=user_id
2625
)
2726

28-
for project_id in trashed_projects_ids:
29-
# TODO: handle error. should not be fail-fast!
30-
await _trash_service.delete_trashed_project(
31-
app, user_id=user_id, project_id=project_id
32-
)
27+
with log_context(
28+
_logger,
29+
logging.DEBUG,
30+
"Deleting %s trashed projects",
31+
len(trashed_projects_ids),
32+
):
33+
for project_id in trashed_projects_ids:
34+
try:
35+
36+
await _trash_service.delete_trashed_project(
37+
app,
38+
user_id=user_id,
39+
project_id=project_id,
40+
)
41+
42+
except Exception as exc: # pylint: disable=broad-exception-caught
43+
_logger.warning(
44+
**create_troubleshotting_log_kwargs(
45+
"Error deleting a trashed project while emptying trash.",
46+
error=exc,
47+
error_context={
48+
"project_id": project_id,
49+
"product_name": product_name,
50+
"user_id": user_id,
51+
},
52+
tip="`empty_trash_safe` is set `fail_fast=False`."
53+
"\nErrors while deletion are ignored."
54+
"\nNew runs might resolve them",
55+
)
56+
)
3357

3458

3559
async def prune_trash(app: web.Application) -> list[str]:
3660
"""Deletes expired items in the trash"""
3761
settings = get_plugin_settings(app)
62+
63+
# app-wide
3864
retention = timedelta(days=settings.TRASH_RETENTION_DAYS)
65+
expiration_dt = arrow.now().datetime - retention
3966

67+
# TODO:
68+
# for each product
69+
# list_trashed_projects
70+
# sort by owner
71+
# as owner start deleting
4072
_logger.debug(
41-
"CODE PLACEHOLDER: **ALL** projects marked as trashed during %s days are deleted",
73+
"CODE PLACEHOLDER: **ALL** items marked as trashed during %s days are deleted (those marked before %s)",
4274
retention,
75+
expiration_dt,
4376
)
4477
await asyncio.sleep(5)
4578

services/web/server/tests/unit/with_dbs/03/test_trash.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -836,8 +836,6 @@ async def test_trash_project_explitictly_and_empty_trash_bin(
836836

837837
# force EMPTY trash
838838
resp = await client.delete("/v0/trash")
839-
# TODO: POST trash:empty -> logs number of elements and
840-
# starts delete in the background ? Assume many many items and can take really long
841839
await assert_status(resp, status.HTTP_204_NO_CONTENT)
842840

843841
# waits for deletion

0 commit comments

Comments
 (0)