From 3d78aacac2cfdf1f520c95cb9f26604995b2c5e1 Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 22 Sep 2026 08:54:01 +0000 Subject: [PATCH] Carry file size, thumbnail size, and deletion flag through decryptFile (closes #37) Foundation unit for the cache/API design. Three fields arrived on the wire but decryptFile dropped them: - `file.size` from `info.fileSize` and `thumbnail.size` from `info.thumbSize`, left `undefined` when the server omits `info`. - `isDeleted` carried from the diff row onto `EnteFile`. No caller change: `listFiles` still filters deleted rows before decrypt, so no deleted row reaches decryptFile here. Surfacing a deleted file through decryption belongs to the enumeration unit, issue 38, where the return shape can be designed around where such a row actually flows. Model: opus-4-8 --- TODO.md | 7 ++++++ src/model/decrypt.ts | 11 ++++++-- src/model/types.ts | 3 +++ test/model/decrypt.test.ts | 51 +++++++++++++++++++++++++++++++++++++- 4 files changed, 69 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index efc43fb..ccd4e66 100644 --- a/TODO.md +++ b/TODO.md @@ -18,6 +18,13 @@ Update the README API reference section to match the current implementation. # Completed Steps +- 2026-09-22: Carried file size, thumbnail size, and the deletion flag through + `decryptFile` (issue 37, foundation for the cache/API design). Live files now + populate `file.size`/`thumbnail.size` from the server's `info` (left + `undefined` when the server omits it), and `isDeleted` is carried from the + diff row onto `EnteFile`. No caller change: `listFiles` still filters deleted + rows before decrypting. Surfacing a tombstone through decryption belongs to + the enumeration unit (issue 38). - 2026-08-10: Made `lint-once.test.ts` enforce what its header claims. It walked `make check` only, so it never read `Dockerfile` — the image CI builds through `script/cibuild` — and a second `prettier --check .` could be added there with diff --git a/src/model/decrypt.ts b/src/model/decrypt.ts index fea8f99..253e3f0 100644 --- a/src/model/decrypt.ts +++ b/src/model/decrypt.ts @@ -120,9 +120,16 @@ export const decryptFile = ( metadata, magicMetadata, pubMagicMetadata, - file: { decryptionHeader: raw.file.decryptionHeader }, - thumbnail: { decryptionHeader: raw.thumbnail.decryptionHeader }, + file: { + decryptionHeader: raw.file.decryptionHeader, + size: raw.info?.fileSize, + }, + thumbnail: { + decryptionHeader: raw.thumbnail.decryptionHeader, + size: raw.info?.thumbSize, + }, updationTime: raw.updationTime, + isDeleted: raw.isDeleted, }; }; diff --git a/src/model/types.ts b/src/model/types.ts index ceb6c79..65cc845 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -48,6 +48,9 @@ export interface EnteFile { file: FileBlob; thumbnail: FileBlob; updationTime: Microseconds; + // Set from the diff row's flag. Live files decode with it absent/false; + // deleted rows are filtered out before decryptFile, so it is not set here. + isDeleted?: boolean; } // The key material a logged-in client holds, everything needed to decrypt diff --git a/test/model/decrypt.test.ts b/test/model/decrypt.test.ts index 13af5c2..049f92c 100644 --- a/test/model/decrypt.test.ts +++ b/test/model/decrypt.test.ts @@ -145,7 +145,12 @@ const buildSharedRawCollection = ( const buildRawFile = ( collectionKey: Uint8Array, - opts?: { title?: string; fileType?: number; creationTime?: number }, + opts?: { + title?: string; + fileType?: number; + creationTime?: number; + info?: { fileSize?: number; thumbSize?: number }; + }, ): RawEnteFile => { const fileKey = sodium.crypto_secretbox_keygen(); const { ciphertext: encFileKey, nonce: fileKeyNonce } = secretboxEncrypt( @@ -186,6 +191,7 @@ const buildRawFile = ( }, file: { decryptionHeader: toBase64(sodium.randombytes_buf(24)) }, thumbnail: { decryptionHeader: toBase64(sodium.randombytes_buf(24)) }, + info: opts?.info, updationTime: 1700000000000000, }; }; @@ -357,4 +363,47 @@ describe("model.decryptFile", () => { expect(() => decryptFile(raw, wrongKey)).toThrow(); }); + + it("carries the file and thumbnail byte sizes from info", () => { + // The server reports the encrypted-blob sizes in `info`; the cache + // needs them without a HEAD request, so decryptFile must copy them + // onto the file and thumbnail blobs. + const masterKey = sodium.crypto_secretbox_keygen(); + const { collectionKey } = buildRawCollection(masterKey); + const raw = buildRawFile(collectionKey, { + info: { fileSize: 4096, thumbSize: 512 }, + }); + + const file = decryptFile(raw, collectionKey); + + expect(file.file.size).toBe(4096); + expect(file.thumbnail.size).toBe(512); + }); + + it("leaves the sizes undefined when the server omits info", () => { + // Older files predate the info field; the sizes must stay undefined + // rather than become 0, so callers can tell "unknown" from "empty". + const masterKey = sodium.crypto_secretbox_keygen(); + const { collectionKey } = buildRawCollection(masterKey); + const raw = buildRawFile(collectionKey); + expect(raw.info).toBeUndefined(); + + const file = decryptFile(raw, collectionKey); + + expect(file.file.size).toBeUndefined(); + expect(file.thumbnail.size).toBeUndefined(); + }); + + it("carries the deletion flag from the diff row", () => { + // The diff marks a deleted row with isDeleted; decryptFile copies it + // onto the file so a caller can tell a deleted row from a live one. + const masterKey = sodium.crypto_secretbox_keygen(); + const { collectionKey } = buildRawCollection(masterKey); + const raw = buildRawFile(collectionKey); + raw.isDeleted = true; + + const file = decryptFile(raw, collectionKey); + + expect(file.isDeleted).toBe(true); + }); }); -- 2.54.0