diff --git a/extension/src/service-worker/vault.ts b/extension/src/service-worker/vault.ts index 044ede8..2c10a74 100644 --- a/extension/src/service-worker/vault.ts +++ b/extension/src/service-worker/vault.ts @@ -3,7 +3,7 @@ import type { SessionHandle } from '../../wasm/relicario_wasm'; import type { GitHost } from './git-host'; -import type { Item, ItemId, Manifest, ManifestEntry, VaultSettings } from '../shared/types'; +import type { AttachmentRef, Item, ItemId, Manifest, ManifestEntry, VaultSettings } from '../shared/types'; // eslint-disable-next-line @typescript-eslint/no-explicit-any let wasm: any = null; @@ -140,3 +140,83 @@ export function findByHostname( .filter(([, e]) => e.trashed_at === undefined) .filter(([, e]) => (e.icon_hint ?? '').toLowerCase() === h); } + +// --- Attachment helpers --- + +/// Sync the manifest entry's attachment_summaries for a single item. +/// Reads the current manifest + item; if either is missing, does nothing. +async function syncManifestAttachments( + git: GitHost, + handle: SessionHandle, + itemId: ItemId, + item: Item, + message: string, +): Promise { + const manifest = await fetchAndDecryptManifest(git, handle); + const entry = manifest.items[itemId]; + if (!entry) return; + entry.attachment_summaries = item.attachments.map((a) => ({ + id: a.id, + filename: a.filename, + mime_type: a.mime_type, + size: a.size, + })); + await encryptAndWriteManifest(git, handle, manifest, message); +} + +/// Add an AttachmentRef to an existing item. +/// Reads + decrypts the item, appends to attachments[], re-encrypts and +/// writes both items/.enc and the manifest entry's attachment_summaries. +/// Throws if the item is not found. +export async function addAttachmentToItem( + git: GitHost, + handle: SessionHandle, + itemId: ItemId, + ref: AttachmentRef, +): Promise { + let item: Item; + try { + item = await fetchAndDecryptItem(git, handle, itemId); + } catch { + throw new Error(`addAttachmentToItem: item ${itemId} not found`); + } + item.attachments = [...item.attachments, ref]; + item.modified = Math.floor(Date.now() / 1000); + const commitMsg = `attach ${ref.filename} to item ${itemId}`; + await encryptAndWriteItem(git, handle, itemId, item, commitMsg); + await syncManifestAttachments(git, handle, itemId, item, `manifest: sync attachments for ${itemId}`); +} + +/// 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 +/// silently ignored. +export async function removeAttachmentsFromItem( + git: GitHost, + handle: SessionHandle, + itemId: ItemId, + idsToRemove: string[], +): Promise { + let item: Item; + try { + item = await fetchAndDecryptItem(git, handle, itemId); + } catch { + throw new Error(`removeAttachmentsFromItem: item ${itemId} not found`); + } + const removeSet = new Set(idsToRemove); + const removed: AttachmentRef[] = []; + const kept: AttachmentRef[] = []; + for (const att of item.attachments) { + if (removeSet.has(att.id)) { + removed.push(att); + } else { + kept.push(att); + } + } + item.attachments = kept; + item.modified = Math.floor(Date.now() / 1000); + const commitMsg = `remove ${removed.length} attachment(s) from item ${itemId}`; + await encryptAndWriteItem(git, handle, itemId, item, commitMsg); + await syncManifestAttachments(git, handle, itemId, item, `manifest: sync attachments for ${itemId}`); + return removed; +}