Attachments stored once; nightly clean-up of unused files

Attachments are no longer stored apart from their message (they were kept
twice: base64 inside the raw message and decoded on their own). The indexer
records the MIME part (attachments.part_index, migration 0023); downloads,
zip, inline images, thumbnails and forwarding take it out of the message;
videos stream from a short-lived copy (kept a day, 1 GiB per user). A
background pass on worker start removes the copies stored before, each only
after checking it can be taken out again (spec App. D §D.6 item 1, criterion
corrected to <= 60%).

A nightly job (03:47, after the Trash/Spam purge) removes files nothing
refers to any more — deleted or purged messages, previews of attachments
gone, uploads no draft has used for a week, expired streaming copies. Files
younger than an hour are left alone; a sent message waiting for its Sent copy
is held until its job is done. Account removal keeps files a draft still uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 20:20:21 +02:00
co-authored by Claude Opus 5.5
parent 0514053252
commit 3957dd540a
17 changed files with 1063 additions and 27 deletions
+8 -1
View File
@@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.accounts.schemas import CreateAccountRequest
from app.accounts.types import ServerConfig
from app.indexer import blobstore
from app.models import Account, Attachment, Message, Thread
from app.models import Account, Attachment, ComposeAttachment, Message, Thread
from app.security.crypto import encrypt_secret
@@ -127,6 +127,13 @@ async def delete_account(
.where(Attachment.user_id == user_id, Attachment.blob_sha256 == sha)
.limit(1)
)
if still_used is None:
# A forwarded file waiting in a draft shares the hash: keep it for that.
still_used = await session.scalar(
select(ComposeAttachment.id)
.where(ComposeAttachment.user_id == user_id, ComposeAttachment.blob_sha256 == sha)
.limit(1)
)
if still_used is None:
blobstore.remove(user_id, sha)
return True