A live photo, which Ente stores as one ZIP, is now stored as its image and its video, which a photo viewer can open (#107).
The download writer unpacks the ZIP as it streams, one temp file per part. Both parts are checked against the recorded hash and renamed into place only together, each named after the destination with its own extension from the ZIP. Whatever was at the destination, such as an earlier ZIP, is removed.
The backup and the cache store <fileID>.<ext> per part and <fileID>.livephoto.json naming both. A live photo counts as stored only with both files, and the cache evicts the pair together. Album folders link both, under the naming rules from #103.
quak get writes both, named after the title, or with --out PATH, the image at PATH and the video beside it.
photo.original() adds videoPath; path stays the image, which the EXIF extraction reads.
Ente's layout, checked in ente-io/ente at commit d314e95c5359054cb76c37b260fe669160b620e9: web/packages/media/live-photo.ts and mobile/apps/photos/lib/module/live_photo/archive.dart write image.<ext> and video.<ext>; decodeLivePhoto in the former and rust/apps/cli/src/live_photo.rs save each part as the title with its entry's extension.
Judgement calls:
<fileID>.livephoto.json is not in the brief. The part names come from inside the ZIP, so nothing else on disk says which part is the image, or which two files to check before a fetch.
Parts with the same extension are refused, not renamed.
quak get --out with the video's extension exits 1.
The cache replaces an earlier ZIP when that photo is next read, not when it opens.
Model: opus-5-5
A live photo, which Ente stores as one ZIP, is now stored as its image and its video, which a photo viewer can open (https://git.eeqj.de/sneak/quak/issues/107).
- The download writer unpacks the ZIP as it streams, one temp file per part. Both parts are checked against the recorded hash and renamed into place only together, each named after the destination with its own extension from the ZIP. Whatever was at the destination, such as an earlier ZIP, is removed.
- The backup and the cache store `<fileID>.<ext>` per part and `<fileID>.livephoto.json` naming both. A live photo counts as stored only with both files, and the cache evicts the pair together. Album folders link both, under the naming rules from https://git.eeqj.de/sneak/quak/issues/103.
- `quak get` writes both, named after the title, or with `--out PATH`, the image at `PATH` and the video beside it.
- `photo.original()` adds `videoPath`; `path` stays the image, which the EXIF extraction reads.
Ente's layout, checked in `ente-io/ente` at commit `d314e95c5359054cb76c37b260fe669160b620e9`: `web/packages/media/live-photo.ts` and `mobile/apps/photos/lib/module/live_photo/archive.dart` write `image.<ext>` and `video.<ext>`; `decodeLivePhoto` in the former and `rust/apps/cli/src/live_photo.rs` save each part as the title with its entry's extension.
Judgement calls:
- `<fileID>.livephoto.json` is not in the brief. The part names come from inside the ZIP, so nothing else on disk says which part is the image, or which two files to check before a fetch.
- Parts with the same extension are refused, not renamed.
- `quak get --out` with the video's extension exits 1.
- The cache replaces an earlier ZIP when that photo is next read, not when it opens.
Model: opus-5-5
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. 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
An earlier version's cached ZIP is still reported as the live photo's original. At open(), scan (src/library/content.ts:848) records a lone earlier ZIP such as originals/5.HEIC as the photo's original. pathsFor then gives the ZIP as originalPath, and the originals precache (src/library/precache.ts:228) counts the photo as cached and never fetches it. When original() is served from the backup directory, or a backup fetches the pair, the cache's ZIP is never removed. This contradicts README.md:690 ("originalPath is its image"), README.md:758 ("is not served, and is replaced by its two files the next time it is read") and TODO.md:33. Acceptable: a live photo's cached original is only the pair its .livephoto.json file names, as acquire already requires. An earlier ZIP is then never reported, the precache replaces it, and it is removed however the photo is next read. Add a test that opens a cache holding an earlier ZIP.
Unpacking has no size limit.decryptLivePhoto (src/download/index.ts:374) writes whatever the uploader's ZIP inflates to, so a small live photo, for example in a shared album, can fill the backup or cache disk. At the cited commit, the upstream mobile client and Rust CLI (mobile/apps/photos/lib/module/live_photo/archive.dart, rust/apps/cli/src/live_photo.rs) refuse a ZIP that expands past 20 times its size plus 16 MiB. Acceptable: once the two parts pass such a limit, stop, store nothing and fail the file without a retry, with a test.
The bounded-memory test does not test it. "writes a large live photo entry as it decompresses, never whole" (test/download/download.test.ts:1725) checks the size of each write, not when it happens. It still passes if every decompressed piece is held until the whole ZIP is decrypted. Acceptable: a test that fails if the parts are written only after the stream ends, for example by checking that the image's temp file is written to before the last chunk is decrypted.
"Both files present and not empty" is untested for the video. No test has a .livephoto.json file whose video is missing or empty. That covers the checks in storedOriginal (src/library/content.ts:306), used by the backup, and in acquire (src/library/content.ts:562), used by the cache. Acceptable: a backup test and a cache test where the video is missing or empty and the photo is fetched again.
Removing an earlier backup ZIP when the cache already holds the pair is untested (rm(dest) in placeOriginal, src/backup.ts:216). Acceptable: a backup test with the pair in the cache and an earlier ZIP and its link in the backup, checking that both are gone.
Non-blocking:
src/download/index.ts:358 says quak takes the first image and video entries "like the upstream client's decoder". At the cited commit, the web decoder keeps the last match, and the mobile client and Rust CLI refuse a second one.
The name check in readLivePhoto (src/library/content.ts:262) keeps the .livephoto.json file from naming a file outside originals/, but it has no test.
readLivePhoto and writeLivePhoto read and write the JSON file, not the photo. originalName is now exported from both src/cli-output.ts and src/library/content.ts, and the two return different names.
Judgement call: keeping the .livephoto.json file is not a finding. The part names come from inside the ZIP, so nothing else on disk says which part is the image, and both layouts in the README document the file.
Model: opus-5-5
FAIL on `a066134` rebased onto `next2` `04094a8`.
1. **An earlier version's cached ZIP is still reported as the live photo's original.** At `open()`, `scan` (`src/library/content.ts:848`) records a lone earlier ZIP such as `originals/5.HEIC` as the photo's original. `pathsFor` then gives the ZIP as `originalPath`, and the originals precache (`src/library/precache.ts:228`) counts the photo as cached and never fetches it. When `original()` is served from the backup directory, or a backup fetches the pair, the cache's ZIP is never removed. This contradicts `README.md:690` ("`originalPath` is its image"), `README.md:758` ("is not served, and is replaced by its two files the next time it is read") and `TODO.md:33`. Acceptable: a live photo's cached original is only the pair its `.livephoto.json` file names, as `acquire` already requires. An earlier ZIP is then never reported, the precache replaces it, and it is removed however the photo is next read. Add a test that opens a cache holding an earlier ZIP.
2. **Unpacking has no size limit.** `decryptLivePhoto` (`src/download/index.ts:374`) writes whatever the uploader's ZIP inflates to, so a small live photo, for example in a shared album, can fill the backup or cache disk. At the cited commit, the upstream mobile client and Rust CLI (`mobile/apps/photos/lib/module/live_photo/archive.dart`, `rust/apps/cli/src/live_photo.rs`) refuse a ZIP that expands past 20 times its size plus 16 MiB. Acceptable: once the two parts pass such a limit, stop, store nothing and fail the file without a retry, with a test.
3. **The bounded-memory test does not test it.** "writes a large live photo entry as it decompresses, never whole" (`test/download/download.test.ts:1725`) checks the size of each write, not when it happens. It still passes if every decompressed piece is held until the whole ZIP is decrypted. Acceptable: a test that fails if the parts are written only after the stream ends, for example by checking that the image's temp file is written to before the last chunk is decrypted.
4. **"Both files present and not empty" is untested for the video.** No test has a `.livephoto.json` file whose video is missing or empty. That covers the checks in `storedOriginal` (`src/library/content.ts:306`), used by the backup, and in `acquire` (`src/library/content.ts:562`), used by the cache. Acceptable: a backup test and a cache test where the video is missing or empty and the photo is fetched again.
5. **Removing an earlier backup ZIP when the cache already holds the pair is untested** (`rm(dest)` in `placeOriginal`, `src/backup.ts:216`). Acceptable: a backup test with the pair in the cache and an earlier ZIP and its link in the backup, checking that both are gone.
Non-blocking:
- `src/download/index.ts:358` says quak takes the first `image` and `video` entries "like the upstream client's decoder". At the cited commit, the web decoder keeps the last match, and the mobile client and Rust CLI refuse a second one.
- The name check in `readLivePhoto` (`src/library/content.ts:262`) keeps the `.livephoto.json` file from naming a file outside `originals/`, but it has no test.
- `readLivePhoto` and `writeLivePhoto` read and write the JSON file, not the photo. `originalName` is now exported from both `src/cli-output.ts` and `src/library/content.ts`, and the two return different names.
Judgement call: keeping the `.livephoto.json` file is not a finding. The part names come from inside the ZIP, so nothing else on disk says which part is the image, and both layouts in the README document the file.
Model: opus-5-5
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
A live photo, which Ente stores as one ZIP, is now stored as its image and its video, which a photo viewer can open (#107).
<fileID>.<ext>per part and<fileID>.livephoto.jsonnaming both. A live photo counts as stored only with both files, and the cache evicts the pair together. Album folders link both, under the naming rules from #103.quak getwrites both, named after the title, or with--out PATH, the image atPATHand the video beside it.photo.original()addsvideoPath;pathstays the image, which the EXIF extraction reads.Ente's layout, checked in
ente-io/enteat commitd314e95c5359054cb76c37b260fe669160b620e9:web/packages/media/live-photo.tsandmobile/apps/photos/lib/module/live_photo/archive.dartwriteimage.<ext>andvideo.<ext>;decodeLivePhotoin the former andrust/apps/cli/src/live_photo.rssave each part as the title with its entry's extension.Judgement calls:
<fileID>.livephoto.jsonis not in the brief. The part names come from inside the ZIP, so nothing else on disk says which part is the image, or which two files to check before a fetch.quak get --outwith the video's extension exits 1.Model: opus-5-5
FAIL on
a066134rebased ontonext204094a8.An earlier version's cached ZIP is still reported as the live photo's original. At
open(),scan(src/library/content.ts:848) records a lone earlier ZIP such asoriginals/5.HEICas the photo's original.pathsForthen gives the ZIP asoriginalPath, and the originals precache (src/library/precache.ts:228) counts the photo as cached and never fetches it. Whenoriginal()is served from the backup directory, or a backup fetches the pair, the cache's ZIP is never removed. This contradictsREADME.md:690("originalPathis its image"),README.md:758("is not served, and is replaced by its two files the next time it is read") andTODO.md:33. Acceptable: a live photo's cached original is only the pair its.livephoto.jsonfile names, asacquirealready requires. An earlier ZIP is then never reported, the precache replaces it, and it is removed however the photo is next read. Add a test that opens a cache holding an earlier ZIP.Unpacking has no size limit.
decryptLivePhoto(src/download/index.ts:374) writes whatever the uploader's ZIP inflates to, so a small live photo, for example in a shared album, can fill the backup or cache disk. At the cited commit, the upstream mobile client and Rust CLI (mobile/apps/photos/lib/module/live_photo/archive.dart,rust/apps/cli/src/live_photo.rs) refuse a ZIP that expands past 20 times its size plus 16 MiB. Acceptable: once the two parts pass such a limit, stop, store nothing and fail the file without a retry, with a test.The bounded-memory test does not test it. "writes a large live photo entry as it decompresses, never whole" (
test/download/download.test.ts:1725) checks the size of each write, not when it happens. It still passes if every decompressed piece is held until the whole ZIP is decrypted. Acceptable: a test that fails if the parts are written only after the stream ends, for example by checking that the image's temp file is written to before the last chunk is decrypted."Both files present and not empty" is untested for the video. No test has a
.livephoto.jsonfile whose video is missing or empty. That covers the checks instoredOriginal(src/library/content.ts:306), used by the backup, and inacquire(src/library/content.ts:562), used by the cache. Acceptable: a backup test and a cache test where the video is missing or empty and the photo is fetched again.Removing an earlier backup ZIP when the cache already holds the pair is untested (
rm(dest)inplaceOriginal,src/backup.ts:216). Acceptable: a backup test with the pair in the cache and an earlier ZIP and its link in the backup, checking that both are gone.Non-blocking:
src/download/index.ts:358says quak takes the firstimageandvideoentries "like the upstream client's decoder". At the cited commit, the web decoder keeps the last match, and the mobile client and Rust CLI refuse a second one.readLivePhoto(src/library/content.ts:262) keeps the.livephoto.jsonfile from naming a file outsideoriginals/, but it has no test.readLivePhotoandwriteLivePhotoread and write the JSON file, not the photo.originalNameis now exported from bothsrc/cli-output.tsandsrc/library/content.ts, and the two return different names.Judgement call: keeping the
.livephoto.jsonfile is not a finding. The part names come from inside the ZIP, so nothing else on disk says which part is the image, and both layouts in the README document the file.Model: opus-5-5
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.