feat(ext/sw): trash operations — listTrashed, restoreItem, purgeItem, purgeAllTrash
listTrashed filters manifest for trashed_at != null, sorted newest-first. restoreItem clears trashed_at. purgeItem deletes item + attachments. purgeAllTrash also scans for orphan blobs in attachments/ directory. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -187,6 +187,101 @@ export async function addAttachmentToItem(
|
||||
await syncManifestAttachments(git, handle, itemId, item, `manifest: sync attachments for ${itemId}`);
|
||||
}
|
||||
|
||||
// --- Trash operations ---
|
||||
|
||||
export function listTrashed(manifest: Manifest): Array<[ItemId, ManifestEntry]> {
|
||||
return Object.entries(manifest.items)
|
||||
.filter(([, entry]) => entry.trashed_at != null)
|
||||
.sort(([, a], [, b]) => (b.trashed_at ?? 0) - (a.trashed_at ?? 0));
|
||||
}
|
||||
|
||||
export async function restoreItem(
|
||||
git: GitHost,
|
||||
handle: SessionHandle,
|
||||
manifest: Manifest,
|
||||
itemId: ItemId,
|
||||
): Promise<void> {
|
||||
const item = await fetchAndDecryptItem(git, handle, itemId);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const restored: Item = { ...item, trashed_at: undefined, modified: now };
|
||||
await encryptAndWriteItem(git, handle, itemId, restored, `restore: ${item.title}`);
|
||||
manifest.items[itemId] = { ...manifest.items[itemId], trashed_at: undefined, modified: now };
|
||||
await encryptAndWriteManifest(git, handle, manifest, `manifest: restore ${item.title}`);
|
||||
}
|
||||
|
||||
export async function purgeItem(
|
||||
git: GitHost,
|
||||
itemId: ItemId,
|
||||
manifest: Manifest,
|
||||
): Promise<string[]> {
|
||||
const entry = manifest.items[itemId];
|
||||
const deletedBlobs: string[] = [];
|
||||
|
||||
// Delete attachments
|
||||
for (const att of entry?.attachment_summaries ?? []) {
|
||||
try {
|
||||
await git.deleteBlob(`attachments/${att.id}.bin`, `purge attachment: ${att.filename}`);
|
||||
deletedBlobs.push(att.id);
|
||||
} catch { /* blob may not exist */ }
|
||||
}
|
||||
|
||||
// Delete item file
|
||||
await git.deleteFile(`items/${itemId}.enc`, `purge: ${entry?.title ?? itemId}`);
|
||||
|
||||
// Update manifest
|
||||
delete manifest.items[itemId];
|
||||
|
||||
return deletedBlobs;
|
||||
}
|
||||
|
||||
export async function purgeAllTrash(
|
||||
git: GitHost,
|
||||
handle: SessionHandle,
|
||||
manifest: Manifest,
|
||||
): Promise<{ itemCount: number; orphanCount: number }> {
|
||||
const trashed = listTrashed(manifest);
|
||||
const allDeletedBlobs = new Set<string>();
|
||||
|
||||
// Purge each trashed item
|
||||
for (const [id] of trashed) {
|
||||
const deleted = await purgeItem(git, id, manifest);
|
||||
deleted.forEach((b) => allDeletedBlobs.add(b));
|
||||
}
|
||||
|
||||
// Collect all referenced attachment IDs from remaining items
|
||||
const referenced = new Set<string>();
|
||||
for (const entry of Object.values(manifest.items)) {
|
||||
for (const att of entry.attachment_summaries ?? []) {
|
||||
referenced.add(att.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Scan for orphan blobs
|
||||
let orphanCount = 0;
|
||||
try {
|
||||
const blobFiles = await git.listDir('attachments');
|
||||
for (const filename of blobFiles) {
|
||||
const id = filename.replace(/\.bin$/, '');
|
||||
if (!referenced.has(id) && !allDeletedBlobs.has(id)) {
|
||||
try {
|
||||
await git.deleteBlob(`attachments/${filename}`, `purge orphan: ${id}`);
|
||||
orphanCount++;
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
} catch { /* attachments dir may not exist */ }
|
||||
|
||||
// Write manifest once
|
||||
if (trashed.length > 0 || orphanCount > 0) {
|
||||
await encryptAndWriteManifest(
|
||||
git, handle, manifest,
|
||||
`trash: purge ${trashed.length} items + ${orphanCount} orphan blobs`,
|
||||
);
|
||||
}
|
||||
|
||||
return { itemCount: trashed.length, orphanCount };
|
||||
}
|
||||
|
||||
/// Remove attachments matching the given IDs from the item.
|
||||
/// Returns the removed AttachmentRefs (so the caller can deleteBlob the
|
||||
/// underlying bytes). IDs not present in the item's attachments are
|
||||
|
||||
Reference in New Issue
Block a user