Compare commits

4 Commits
Author SHA1 Message Date
clawbot c5a588177c Thumbnail test on main: re-encode a small image so it cannot time out (closes #153)
check / check (push) Successful in 1m19s
The `main` half of the fix for the timed-out thumbnail test, carrying only the test change from `next` (d788c54). The test "re-encodes smaller until the thumbnail fits the recorded size" built a noisy 400x300 JPEG, which came close to vitest's 5 s limit and made `script/cibuild` fail on a busy host. It now uses a noisy 64x48 JPEG, keeps every assertion, and runs in about 1 s.

Model: opus-5-5
2026-10-02 04:22:27 +02:00
sneak 9e6e038545 Merge pull request 'next → main: pre-1.0 milestone' (#31) from next into main
check / check (push) Successful in 52s
Reviewed-on: #31
2026-09-29 03:12:31 +02:00
clawbot 1b7158fcd3 Test the live-photo writer's fsyncs (closes #130)
check / check (push) Successful in 43s
A test checks that the live-photo writer fsyncs the image's and the
video's temp files before renaming either into place, and fsyncs the
directory after both renames, as the writeAtomic test does for one file.

Model: opus-5-5
2026-09-28 18:45:48 +02:00
clawbot 9e6deb21eb Store live photos as their image and their video (closes #107)
check / check (push) Successful in 41s
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 18:06:01 +02:00
3 changed files with 28 additions and 4 deletions
+5
View File
@@ -21,6 +21,11 @@ declares one.
# Completed Steps # 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). - 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 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 `<fileID>.<ext>` for the image and for the video, each with its extension from
+18
View File
@@ -1909,6 +1909,24 @@ 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 () => { it("stores neither part when the video cannot be renamed into place", async () => {
const t = setup(livePhotoZip(), livePhoto); const t = setup(livePhotoZip(), livePhoto);
renameHook.failWith = new Error("simulated rename failure"); renameHook.failWith = new Error("simulated rename failure");
+5 -4
View File
@@ -787,12 +787,13 @@ describe("fixMissingThumbnails", () => {
}); });
it("re-encodes smaller until the thumbnail fits the recorded size", async () => { it("re-encodes smaller until the thumbnail fits the recorded size", async () => {
// A noisy 400x300 JPEG, which the default encoding (quality 50, not // A noisy 64x48 JPEG, which the default encoding (quality 50, not
// resized because it is under 720 px) cannot compress below the size // resized because it is under 720 px) cannot compress below the size
// recorded here: one byte less than that encoding's ciphertext. // recorded here: one byte less than that encoding's ciphertext. It is
// small so that each encode is quick even on a busy host.
const fixMock = await buildThumbMock(); const fixMock = await buildThumbMock();
const w = 400; const w = 64;
const h = 300; const h = 48;
const noisy = new Uint8Array( const noisy = new Uint8Array(
jpegJs.encode( jpegJs.encode(
{ {