Compare commits

1 Commits
Author SHA1 Message Date
clawbot 76823bc374 Store live photos as their image and their video (closes #107)
check / check (push) Successful in 40s
A live photo, which Ente stores as one ZIP, is unpacked as it downloads
into its image and its video, each `<fileID>.<ext>` with its extension
from the ZIP, beside `<fileID>.livephoto.json`, which names the two.
Both are checked against the recorded hash and renamed into place only
when both are complete. A ZIP with a second image or video, or whose
parts come to more than 20 times its size plus 16 MiB, is refused. The
backup and the content cache count a live photo as stored only with both
files, album folders link both, `quak get` writes both, and the content
result gives the video as `videoPath`. A ZIP an earlier version stored
is replaced.

Model: opus-5-5
2026-09-28 15:13:25 +00:00
3 changed files with 4 additions and 28 deletions
-5
View File
@@ -21,11 +21,6 @@ declares one.
# Completed Steps
- 2026-09-28: Tested the live-photo writer's fsyncs (issue 130). A test checks
that the image's and the video's temp files are fsynced before either is
renamed into place, and the directory after both renames, as the `writeAtomic`
test does for one file.
- 2026-09-23: Live photos are stored as their image and their video (issue 107).
A live photo, which Ente stores as one ZIP, is unpacked as it downloads into
`<fileID>.<ext>` for the image and for the video, each with its extension from
-18
View File
@@ -1909,24 +1909,6 @@ describe("downloadFile live photos", () => {
]);
});
it("fsyncs each part's temp file before the renames and the directory after", async () => {
const t = setup(livePhotoZip(), livePhoto);
await t.run();
// The same durability contract as `writeAtomic`, for two files: both
// temp files are on disk before either is renamed into place, and the
// directory is fsynced once both renames are done.
const [imageTmp, videoTmp] = renameHook.calls.map((c) => c.from);
expect(durabilityHook.events).toEqual([
`sync:w:${imageTmp}`,
`sync:w:${videoTmp}`,
`rename:${join(t.dir, "f.heic")}`,
`rename:${join(t.dir, "f.mov")}`,
`sync:r:${t.dir}`,
]);
});
it("stores neither part when the video cannot be renamed into place", async () => {
const t = setup(livePhotoZip(), livePhoto);
renameHook.failWith = new Error("simulated rename failure");
+4 -5
View File
@@ -787,13 +787,12 @@ describe("fixMissingThumbnails", () => {
});
it("re-encodes smaller until the thumbnail fits the recorded size", async () => {
// A noisy 64x48 JPEG, which the default encoding (quality 50, not
// A noisy 400x300 JPEG, which the default encoding (quality 50, not
// resized because it is under 720 px) cannot compress below the size
// recorded here: one byte less than that encoding's ciphertext. It is
// small so that each encode is quick even on a busy host.
// recorded here: one byte less than that encoding's ciphertext.
const fixMock = await buildThumbMock();
const w = 64;
const h = 48;
const w = 400;
const h = 300;
const noisy = new Uint8Array(
jpegJs.encode(
{