v0.5.0: Trash (soft-delete) + Keep folder
Delete now moves an artifact to a recoverable Trash instead of hard-removing.
- Model: Artifact.deleted_at (nullable, indexed) = in Trash.
- Soft-delete everywhere: DELETE /api/artifacts/{id} sets deleted_at
(idempotent 204); session POST /a/{id}/delete for the feed's hover Delete
icon; POST /a/{id}/undelete restores; POST /feed/empty-trash purges.
- Trashed artifacts are off-quota (deleting frees quota) and eviction-protected
(never evicted early); hidden from feed, sources, counts, and all API reads
(list/detail/raw).
- Sidebar: "Deleted" and "Keep" system folders at the bottom (counts, hidden
when empty); ?folder=deleted / ?folder=keep views reuse the row rendering.
Deleted view has hover Undelete + an Empty Trash button. Keep = view over
is_kept. Live-poll disabled in folder views.
- Docs (api.md delete section, CLAUDE.md), 10 new/updated tests. 150 passing;
API soft-delete lifecycle verified live.
Purge is manual (Empty Trash) this release; auto purge-on-delete (background,
~3-day retention) is the planned fast-follow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6c1823e65e
commit
4254164bef
14 changed files with 438 additions and 43 deletions
|
|
@ -70,8 +70,13 @@ def artifact_total_bytes(artifact):
|
|||
|
||||
|
||||
def user_usage_bytes(user):
|
||||
# Trashed artifacts are off-quota (deleting relieves quota immediately);
|
||||
# their files linger on disk until Empty Trash / purge, but don't count.
|
||||
artifact_bytes = (
|
||||
Artifact.objects.filter(owner=user).aggregate(total=Sum("size_bytes")).get("total") or 0
|
||||
Artifact.objects.filter(owner=user, deleted_at__isnull=True)
|
||||
.aggregate(total=Sum("size_bytes"))
|
||||
.get("total")
|
||||
or 0
|
||||
)
|
||||
asset_bytes = (
|
||||
Asset.objects.filter(owner=user).aggregate(total=Sum("size_bytes")).get("total") or 0
|
||||
|
|
@ -113,8 +118,10 @@ def make_room_for(user, incoming_bytes):
|
|||
if used + incoming_bytes <= quota:
|
||||
return []
|
||||
|
||||
# Trashed artifacts are eviction-protected (recovery window is guaranteed)
|
||||
# and already off-quota, so they're never eviction candidates.
|
||||
unkept = list(
|
||||
Artifact.objects.filter(owner=user, is_kept=False).order_by("created_at")
|
||||
Artifact.objects.filter(owner=user, is_kept=False, deleted_at__isnull=True).order_by("created_at")
|
||||
)
|
||||
# Check whether eviction can possibly succeed before deleting anything -
|
||||
# otherwise a doomed request would still destroy every unkept artifact on
|
||||
|
|
@ -137,6 +144,16 @@ def make_room_for(user, incoming_bytes):
|
|||
return evicted
|
||||
|
||||
|
||||
def purge_trashed(user):
|
||||
"""Permanently remove all of a user's trashed artifacts (files + rows).
|
||||
Backs the "Empty Trash" action."""
|
||||
trashed = list(Artifact.objects.filter(owner=user, deleted_at__isnull=False))
|
||||
for artifact in trashed:
|
||||
delete_artifact_files(artifact)
|
||||
artifact.delete()
|
||||
return len(trashed)
|
||||
|
||||
|
||||
ASSET_ORPHAN_GRACE = timedelta(hours=24)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue