Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ..folders import folders_trash_service
from ..products import products_service
from ..projects import projects_trash_service
from ..workspaces import workspaces_trash_service
from .settings import get_plugin_settings

_logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -100,6 +101,43 @@ async def _empty_explicitly_trashed_folders_and_content(
)


async def _empty_trashed_workspaces_and_content(
app: web.Application, product_name: ProductName, user_id: UserID
):
trashed_workspaces_ids = await workspaces_trash_service.list_trashed_workspaces(
app=app, product_name=product_name, user_id=user_id
)

with log_context(
_logger,
logging.DEBUG,
"Deleting %s trashed workspaces (and all its content)",
len(trashed_workspaces_ids),
):
for workspace_id in trashed_workspaces_ids:
try:
await workspaces_trash_service.delete_trashed_workspace(
app,
product_name=product_name,
user_id=user_id,
workspace_id=workspace_id,
)

except Exception as exc: # pylint: disable=broad-exception-caught
_logger.warning(
**create_troubleshotting_log_kwargs(
"Error deleting a trashed workspace (and content) while emptying trash.",
error=exc,
error_context={
"workspace_id": workspace_id,
"product_name": product_name,
"user_id": user_id,
},
tip=_TIP,
)
)


async def safe_empty_trash(
app: web.Application,
*,
Expand All @@ -115,6 +153,8 @@ async def safe_empty_trash(
# Delete explicitly trashed folders (and all implicitly trashed sub-folders and projects)
await _empty_explicitly_trashed_folders_and_content(app, product_name, user_id)

await _empty_trashed_workspaces_and_content(app, product_name, user_id)


async def safe_delete_expired_trash_as_admin(app: web.Application) -> None:
settings = get_plugin_settings(app)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from ..login.decorators import get_user_id, login_required
from ..products import products_web
from ..security.decorators import permission_required
from . import _trash_services
from . import _trash_service
from ._common.exceptions_handlers import handle_plugin_requests_exceptions
from ._common.models import WorkspacesPathParams, WorkspaceTrashQueryParams

Expand All @@ -33,7 +33,7 @@ async def trash_workspace(request: web.Request):
WorkspaceTrashQueryParams, request
)

await _trash_services.trash_workspace(
await _trash_service.trash_workspace(
request.app,
product_name=product_name,
user_id=user_id,
Expand All @@ -53,7 +53,7 @@ async def untrash_workspace(request: web.Request):
product_name = products_web.get_product_name(request)
path_params = parse_request_path_parameters_as(WorkspacesPathParams, request)

await _trash_services.untrash_workspace(
await _trash_service.untrash_workspace(
request.app,
product_name=product_name,
user_id=user_id,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
import logging
from datetime import datetime

import arrow
from aiohttp import web
from common_library.pagination_tools import iter_pagination_params
from models_library.basic_types import IDStr
from models_library.folders import FolderID
from models_library.products import ProductName
from models_library.projects import Project, ProjectID
from models_library.rest_ordering import OrderBy, OrderDirection
from models_library.rest_pagination import MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE
from models_library.users import UserID
from models_library.workspaces import (
UserWorkspaceWithAccessRights,
WorkspaceID,
WorkspaceUpdates,
)
from simcore_postgres_database.utils_repos import transaction_context

from ..db.plugin import get_asyncpg_engine
from ..folders._folders_service import list_folders
from ..folders._trash_service import trash_folder, untrash_folder
from ..projects._crud_api_read import list_projects
from ..projects._trash_service import trash_project, untrash_project
from ..projects.models import ProjectTypeAPI
from . import _workspaces_repository, _workspaces_service
from .errors import WorkspaceNotTrashedError

_logger = logging.getLogger(__name__)


async def _check_exists_and_access(
app: web.Application,
*,
product_name: ProductName,
user_id: UserID,
workspace_id: WorkspaceID,
):
await _workspaces_service.check_user_workspace_access(
app=app,
user_id=user_id,
workspace_id=workspace_id,
product_name=product_name,
permission="delete",
)


async def _list_child_folders(
app: web.Application,
*,
product_name: ProductName,
user_id: UserID,
workspace_id: WorkspaceID,
) -> list[FolderID]:

child_folders: list[FolderID] = []
for page_params in iter_pagination_params(limit=MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE):
(
folders,
page_params.total_number_of_items,
) = await list_folders(
app,
user_id=user_id,
product_name=product_name,
folder_id=None,
workspace_id=workspace_id,
trashed=None,
offset=page_params.offset,
limit=page_params.limit,
order_by=OrderBy(field=IDStr("trashed"), direction=OrderDirection.ASC),
)

child_folders.extend([folder.folder_db.folder_id for folder in folders])

return child_folders


async def _list_child_projects(
app: web.Application,
*,
product_name: ProductName,
user_id: UserID,
workspace_id: WorkspaceID,
) -> list[ProjectID]:

child_projects: list[ProjectID] = []
for page_params in iter_pagination_params(limit=MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE):
(
projects,
page_params.total_number_of_items,
) = await list_projects(
app,
user_id=user_id,
product_name=product_name,
show_hidden=True,
workspace_id=workspace_id,
project_type=ProjectTypeAPI.all,
folder_id=None,
trashed=None,
offset=page_params.offset,
limit=page_params.limit,
order_by=OrderBy(field=IDStr("trashed"), direction=OrderDirection.ASC),
)

child_projects.extend([Project(**project).uuid for project in projects])

return child_projects


async def trash_workspace(
app: web.Application,
*,
product_name: ProductName,
user_id: UserID,
workspace_id: WorkspaceID,
force_stop_first: bool,
):
await _check_exists_and_access(
app, product_name=product_name, user_id=user_id, workspace_id=workspace_id
)

trashed_at = arrow.utcnow().datetime

async with transaction_context(get_asyncpg_engine(app)) as connection:
# EXPLICIT trash
await _workspaces_repository.update_workspace(
app,
connection,
product_name=product_name,
workspace_id=workspace_id,
updates=WorkspaceUpdates(trashed=trashed_at, trashed_by=user_id),
)

# IMPLICIT trash
child_folders: list[FolderID] = await _list_child_folders(
app,
product_name=product_name,
user_id=user_id,
workspace_id=workspace_id,
)

for folder_id in child_folders:
await trash_folder(
app,
product_name=product_name,
user_id=user_id,
folder_id=folder_id,
force_stop_first=force_stop_first,
)

child_projects: list[ProjectID] = await _list_child_projects(
app,
product_name=product_name,
user_id=user_id,
workspace_id=workspace_id,
)

for project_id in child_projects:
await trash_project(
app,
product_name=product_name,
user_id=user_id,
project_id=project_id,
force_stop_first=force_stop_first,
explicit=False,
)


async def untrash_workspace(
app: web.Application,
*,
product_name: ProductName,
user_id: UserID,
workspace_id: WorkspaceID,
):
await _check_exists_and_access(
app, product_name=product_name, user_id=user_id, workspace_id=workspace_id
)

async with transaction_context(get_asyncpg_engine(app)) as connection:
# EXPLICIT UNtrash
await _workspaces_repository.update_workspace(
app,
connection,
product_name=product_name,
workspace_id=workspace_id,
updates=WorkspaceUpdates(trashed=None, trashed_by=None),
)

child_folders: list[FolderID] = await _list_child_folders(
app,
product_name=product_name,
user_id=user_id,
workspace_id=workspace_id,
)

for folder_id in child_folders:
await untrash_folder(
app,
product_name=product_name,
user_id=user_id,
folder_id=folder_id,
)

child_projects: list[ProjectID] = await _list_child_projects(
app,
product_name=product_name,
user_id=user_id,
workspace_id=workspace_id,
)

for project_id in child_projects:
await untrash_project(
app, product_name=product_name, user_id=user_id, project_id=project_id
)


def _can_delete(
workspace: UserWorkspaceWithAccessRights,
user_id: UserID,
until_equal_datetime: datetime | None,
) -> bool:
return bool(
workspace.trashed
and (until_equal_datetime is None or workspace.trashed < until_equal_datetime)
and workspace.my_access_rights.delete
and workspace.trashed_by == user_id
)


async def delete_trashed_workspace(
app: web.Application,
*,
product_name: ProductName,
user_id: UserID,
workspace_id: WorkspaceID,
until_equal_datetime: datetime | None = None,
) -> None:

workspace = await _workspaces_service.get_workspace(
app,
user_id=user_id,
product_name=product_name,
workspace_id=workspace_id,
)

if not _can_delete(
workspace,
user_id=user_id,
until_equal_datetime=until_equal_datetime,
):
raise WorkspaceNotTrashedError(
workspace_id=workspace_id,
user_id=user_id,
reason="Cannot delete trashed workspace since it does not fit current criteria",
)

# NOTE: this function deletes workspace AND its content recursively!
await _workspaces_service.delete_workspace(
app,
user_id=user_id,
product_name=product_name,
workspace_id=workspace_id,
)


async def list_trashed_workspaces(
app: web.Application,
product_name: ProductName,
user_id: UserID,
until_equal_datetime: datetime | None = None,
) -> list[WorkspaceID]:
trashed_workspace_ids: list[WorkspaceID] = []

for page_params in iter_pagination_params(limit=MAXIMUM_NUMBER_OF_ITEMS_PER_PAGE):
(
page_params.total_number_of_items,
workspaces,
) = await _workspaces_service.list_workspaces(
app,
user_id=user_id,
product_name=product_name,
filter_trashed=True,
filter_by_text=None,
offset=page_params.offset,
limit=page_params.limit,
order_by=OrderBy(field=IDStr("trashed"), direction=OrderDirection.ASC),
)

# NOTE: Applying POST-FILTERING
trashed_workspace_ids.extend(
[
ws.workspace_id
for ws in workspaces
if _can_delete(
ws,
user_id=user_id,
until_equal_datetime=until_equal_datetime,
)
]
)

return trashed_workspace_ids
Loading
Loading