Compare commits

...
1 Commits
Author SHA1 Message Date
sneak 3d78aacac2 Carry file size, thumbnail size, and deletion flag through decryptFile (closes #37)
check / check (push) Successful in 25s
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
2026-09-22 09:43:28 +00:00
4 changed files with 69 additions and 3 deletions
+7
View File
@@ -18,6 +18,13 @@ Update the README API reference section to match the current implementation.
# Completed Steps # 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 - 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 `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 `script/cibuild` — and a second `prettier --check .` could be added there with
+9 -2
View File
@@ -120,9 +120,16 @@ export const decryptFile = (
metadata, metadata,
magicMetadata, magicMetadata,
pubMagicMetadata, pubMagicMetadata,
file: { decryptionHeader: raw.file.decryptionHeader }, file: {
thumbnail: { decryptionHeader: raw.thumbnail.decryptionHeader }, decryptionHeader: raw.file.decryptionHeader,
size: raw.info?.fileSize,
},
thumbnail: {
decryptionHeader: raw.thumbnail.decryptionHeader,
size: raw.info?.thumbSize,
},
updationTime: raw.updationTime, updationTime: raw.updationTime,
isDeleted: raw.isDeleted,
}; };
}; };
+3
View File
@@ -48,6 +48,9 @@ export interface EnteFile {
file: FileBlob; file: FileBlob;
thumbnail: FileBlob; thumbnail: FileBlob;
updationTime: Microseconds; 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 // The key material a logged-in client holds, everything needed to decrypt
+50 -1
View File
@@ -145,7 +145,12 @@ const buildSharedRawCollection = (
const buildRawFile = ( const buildRawFile = (
collectionKey: Uint8Array, collectionKey: Uint8Array,
opts?: { title?: string; fileType?: number; creationTime?: number }, opts?: {
title?: string;
fileType?: number;
creationTime?: number;
info?: { fileSize?: number; thumbSize?: number };
},
): RawEnteFile => { ): RawEnteFile => {
const fileKey = sodium.crypto_secretbox_keygen(); const fileKey = sodium.crypto_secretbox_keygen();
const { ciphertext: encFileKey, nonce: fileKeyNonce } = secretboxEncrypt( const { ciphertext: encFileKey, nonce: fileKeyNonce } = secretboxEncrypt(
@@ -186,6 +191,7 @@ const buildRawFile = (
}, },
file: { decryptionHeader: toBase64(sodium.randombytes_buf(24)) }, file: { decryptionHeader: toBase64(sodium.randombytes_buf(24)) },
thumbnail: { decryptionHeader: toBase64(sodium.randombytes_buf(24)) }, thumbnail: { decryptionHeader: toBase64(sodium.randombytes_buf(24)) },
info: opts?.info,
updationTime: 1700000000000000, updationTime: 1700000000000000,
}; };
}; };
@@ -357,4 +363,47 @@ describe("model.decryptFile", () => {
expect(() => decryptFile(raw, wrongKey)).toThrow(); 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);
});
}); });