Default save path ./photos/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.photoid.ext; downloads to it come from the cache when cached #143

Closed
opened 2026-10-01 19:52:06 +02:00 by clawbot · 4 comments
Collaborator

Follows #141 (merged to next as PR 142), which put the save path under {downloadDirectory}/originals/{fileID}{ext} and left it undefined when no download directory was given.

Owner's words (chat, 2026-10-01 ~17:5x UTC):

default quak download path should be ./photos/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.photoid.ext . files in the cache don’t count as downloaded but when downloaded to the save path are pulled from the cache not the network.

This also answers question 1 on 141 (comment 107489): a copy only in the cache does not count as downloaded.

Definition of done:

  • By default, a photo's save path is ./photos/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.<photoid>.<ext>. The date parts come from the photo's date, <photoid> is its id and <ext> is its original extension. savePath always has a value. A configured download directory replaces ./photos.
  • Whatever writes originals to the save path uses this layout, lib.backup() included. No second layout remains.
  • isLocal is true only when the original is at the save path. A copy only in quak's cache does not count.
  • Downloading to the save path copies the original from the cache when the cache holds it. It goes to the network only when the cache does not.
  • Before implementation, a comment on this issue states the reading of every choice the words leave open: which date and which time zone, what photoid is, the extension, live photos, what happens to the old originals/ and collections/ layout, and how relative ./photos resolves.
  • Tests cover the layout, the default, the cache-first download and isLocal. README and API docs are updated. The check gate is green on next.
  • One PR to next, independently reviewed and squash-merged.

model: opus-5-5

Follows https://git.eeqj.de/sneak/quak/issues/141 (merged to `next` as PR 142), which put the save path under `{downloadDirectory}/originals/{fileID}{ext}` and left it undefined when no download directory was given. Owner's words (chat, 2026-10-01 ~17:5x UTC): > default quak download path should be ./photos/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.photoid.ext . files in the cache don’t count as downloaded but when downloaded to the save path are pulled from the cache not the network. This also answers question 1 on 141 (comment 107489): a copy only in the cache does not count as downloaded. Definition of done: - By default, a photo's save path is `./photos/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.<photoid>.<ext>`. The date parts come from the photo's date, `<photoid>` is its id and `<ext>` is its original extension. `savePath` always has a value. A configured download directory replaces `./photos`. - Whatever writes originals to the save path uses this layout, `lib.backup()` included. No second layout remains. - `isLocal` is true only when the original is at the save path. A copy only in quak's cache does not count. - Downloading to the save path copies the original from the cache when the cache holds it. It goes to the network only when the cache does not. - Before implementation, a comment on this issue states the reading of every choice the words leave open: which date and which time zone, what `photoid` is, the extension, live photos, what happens to the old `originals/` and `collections/` layout, and how relative `./photos` resolves. - Tests cover the layout, the default, the cache-first download and `isLocal`. README and API docs are updated. The check gate is green on `next`. - One PR to `next`, independently reviewed and squash-merged. model: opus-5-5
clawbot self-assigned this 2026-10-01 19:52:06 +02:00
Author
Collaborator

Reading of each open choice.

  • Path: {root}/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.{fileID}{ext}, for example photos/2026/2026-03/2026-03-01/2026-03-01.12345.jpg.
  • Date: the photo's takenAt. That is the date the user set in Ente if they edited it, otherwise the creation time. It is the same date photo.year and the timeline use.
  • Time zone: the time zone of the machine running quak, as photo.year and the timeline use. If that time zone changes, or the date is edited in Ente, the save path changes. The next run then writes the photo at the new path and leaves the old copy, because quak never deletes an original.
  • photoid: Ente's numeric fileID. It is unique on one server and does not change when a file is shared, moved, trashed or edited (#140 (comment)).
  • Extension: taken from the file's name as uploaded, not from a later rename. This is what quak uses today: case kept, and .bin when there is none or it is unsafe.
  • Live photos: the image and the video sit side by side, for example ….12345.heic and ….12345.mov, with the extensions found inside the live photo. A ….12345.livephoto.json beside them names the two. savePath is the image's path. Before the first download it carries the title's extension, as it does today.
  • Root: ./photos, resolved against the working directory once, when the library opens. Library.open({ downloadDirectory }) replaces it, and quak backup passes the directory it is given. savePath is always a string, even in a library without a content cache.
  • Downloading: new method await photo.download(), which writes the original to savePath. If the cache holds it, it is copied from there; otherwise it is fetched straight to savePath. When the original is already there, nothing happens. lib.backup() uses the same paths and the same rule. original() and content() still fill only the cache, and they read from savePath when the original is there.
  • isLocal: true when the whole original is at savePath; for a live photo, both files. A copy in the cache does not count.
  • Old layout: originals/ is gone. quak is pre-1.0 and has no existing backups, so nothing reads, detects or moves an old tree. Each file's metadata JSON sits beside the photo as YYYY-MM-DD.{fileID}.json. collections/ stays as the album view, linking to the new paths. thumbnails/ and failures.json stay where they are.

Both questions were answered in #143 (comment). The folder date uses the machine's local time zone, and there is no migration.

Model: opus-5-5

Reading of each open choice. - **Path:** `{root}/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.{fileID}{ext}`, for example `photos/2026/2026-03/2026-03-01/2026-03-01.12345.jpg`. - **Date:** the photo's `takenAt`. That is the date the user set in Ente if they edited it, otherwise the creation time. It is the same date `photo.year` and the timeline use. - **Time zone:** the time zone of the machine running quak, as `photo.year` and the timeline use. If that time zone changes, or the date is edited in Ente, the save path changes. The next run then writes the photo at the new path and leaves the old copy, because quak never deletes an original. - **photoid:** Ente's numeric `fileID`. It is unique on one server and does not change when a file is shared, moved, trashed or edited (https://git.eeqj.de/sneak/quak/issues/140#issuecomment-107491). - **Extension:** taken from the file's name as uploaded, not from a later rename. This is what quak uses today: case kept, and `.bin` when there is none or it is unsafe. - **Live photos:** the image and the video sit side by side, for example `….12345.heic` and `….12345.mov`, with the extensions found inside the live photo. A `….12345.livephoto.json` beside them names the two. `savePath` is the image's path. Before the first download it carries the title's extension, as it does today. - **Root:** `./photos`, resolved against the working directory once, when the library opens. `Library.open({ downloadDirectory })` replaces it, and `quak backup` passes the directory it is given. `savePath` is always a string, even in a library without a content cache. - **Downloading:** new method `await photo.download()`, which writes the original to `savePath`. If the cache holds it, it is copied from there; otherwise it is fetched straight to `savePath`. When the original is already there, nothing happens. `lib.backup()` uses the same paths and the same rule. `original()` and `content()` still fill only the cache, and they read from `savePath` when the original is there. - **`isLocal`:** true when the whole original is at `savePath`; for a live photo, both files. A copy in the cache does not count. - **Old layout:** `originals/` is gone. quak is pre-1.0 and has no existing backups, so nothing reads, detects or moves an old tree. Each file's metadata JSON sits beside the photo as `YYYY-MM-DD.{fileID}.json`. `collections/` stays as the album view, linking to the new paths. `thumbnails/` and `failures.json` stay where they are. Both questions were answered in https://git.eeqj.de/sneak/quak/issues/143#issuecomment-107662. The folder date uses the machine's local time zone, and there is no migration. Model: opus-5-5
Author
Collaborator

Implementer's brief. Implement the reading in #143 (comment) exactly.

Standing rule: quak is pre-1.0, with no installed base and no data anywhere. Never write a migration or upgrade path, never move or convert old data, and never keep backward compatibility. The old originals/ layout is simply gone: no code detects, reads or moves it, and no test or doc mentions it.

One place computes the save path. A small function takes the root and an EnteFile and returns {root}/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.{fileID}{ext}.

  • The date is the photo's takenAt in the machine's local time zone. Compute takenAt by the same rule toPhotoRecord uses: export a helper from src/library/records.ts and use it in both places, so the two can never disagree.
  • {ext} is safeExtension(file.metadata.title), which is what nameInOriginals uses today.
  • Photo.savePath, isLocal, the content cache's present check, photo.download() and lib.backup() all use this one function. Nothing else builds the path.

Generalise "stored". storedOriginal and the live-photo JSON helpers in src/library/content.ts currently assume the name {fileID}{ext}. Make them take the name without its extension, so they work in both places that use them:

  • the cache keeps {fileID};
  • the save path uses YYYY-MM-DD.{fileID}.

A live photo is stored when YYYY-MM-DD.{fileID}.livephoto.json names an image and a video that both have content, and the JSON check still accepts only names with that base.

Library.

  • downloadDirectory defaults to path.resolve("photos"), computed once in Library.open.
  • PhotoContent.savePath returns string.
  • Photo.savePath is always a string, including in a library with no content cache, so do not route it only through the cache.
  • isLocal is true when the original is stored at the save path.
  • The cache serves original() from the save path when the original is stored there.

photo.download() (async), resolving to the same { path, bytes, videoPath? } as original(), at the save path:

  • If the original is already stored there, return it.
  • Otherwise get it the way backup does today: backupOriginal(fileID, savePath) writes a fetch straight to the save path and returns the cached copy when the cache holds one.
  • Then place it the way placeOriginal does: copy atomically from the cache, put a live photo's two files and its JSON beside it, and create the folders.
  • Move placeOriginal and copyAtomic out of src/backup.ts to where both can use them. Backup's originals phase becomes a call to the same code for each file.

Backup (src/backup.ts):

  • Originals go to the save path under the backup's directory. The per-file metadata JSON is written beside the original as YYYY-MM-DD.{fileID}.json.
  • Keep collections/: each link points at the new path. Removing stale links recognises the links quak made into the date folders, and nothing the user made.
  • Leave thumbnails/{fileID}.jpg and failures.json as they are.

Docs.

  • README "Backup layout": rewrite it for the new tree.
  • README "Read surface" and "Opening a library": cover savePath, isLocal, download() and the ./photos default.
  • Update TODO.md wherever it describes the backup layout.

Tests.

  • The layout and date folders, using a fixed date at noon so the time zone cannot shift the day.
  • The ./photos default resolving against the working directory.
  • savePath without a content cache.
  • isLocal: false when the copy is only in the cache, true after download().
  • download() copying from the cache when it is cached, with the source never called.
  • download() fetching when nothing is cached.
  • download() doing nothing on a second call.
  • Live photos.
  • lib.backup() writing the new layout.
  • collections/ links pointing at the new paths.

Existing backup tests change only their expected paths to the new layout. Never drop or relax one of their checks.

Out of scope: a ULID, and a year or date filter.

Process

  • Branch from current next, with one PR whose base is next. Title: Save path ./photos/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.fileID.ext; download() from the cache first (closes #143). Label it needs-review and assign clawbot.
  • Gate only with make check, and format only with make fmt. Both run in Docker. Run Docker builds under flock -w 1800 /srv/code/tmp/.docker-build.lock. A timeout there exits 1 without running anything; retry, and never read it as red. Remove every container you start.
  • Pull and rebase onto next again right before pushing, and re-run make check after any conflict resolution.
  • Edit files by hand. No sed -i, perl -pi, awk or scripted replacements.
  • Keep the change small and plain enough for a newcomer to follow in one reading. Coin no new terms.
  • Ask no interactive questions. A point only sneak can settle goes on the PR with the reading you took.
  • Commits and the PR body end with a Model: line naming your model id. Never name the company.
  • Stay under 2 GiB of RAM.

Model: opus-5-5

**Implementer's brief.** Implement the reading in https://git.eeqj.de/sneak/quak/issues/143#issuecomment-107571 exactly. **Standing rule: quak is pre-1.0, with no installed base and no data anywhere.** Never write a migration or upgrade path, never move or convert old data, and never keep backward compatibility. The old `originals/` layout is simply gone: no code detects, reads or moves it, and no test or doc mentions it. **One place computes the save path.** A small function takes the root and an `EnteFile` and returns `{root}/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.{fileID}{ext}`. - The date is the photo's `takenAt` in the machine's local time zone. Compute `takenAt` by the same rule `toPhotoRecord` uses: export a helper from `src/library/records.ts` and use it in both places, so the two can never disagree. - `{ext}` is `safeExtension(file.metadata.title)`, which is what `nameInOriginals` uses today. - `Photo.savePath`, `isLocal`, the content cache's present check, `photo.download()` and `lib.backup()` all use this one function. Nothing else builds the path. **Generalise "stored".** `storedOriginal` and the live-photo JSON helpers in `src/library/content.ts` currently assume the name `{fileID}{ext}`. Make them take the name without its extension, so they work in both places that use them: - the cache keeps `{fileID}`; - the save path uses `YYYY-MM-DD.{fileID}`. A live photo is stored when `YYYY-MM-DD.{fileID}.livephoto.json` names an image and a video that both have content, and the JSON check still accepts only names with that base. **Library.** - `downloadDirectory` defaults to `path.resolve("photos")`, computed once in `Library.open`. - `PhotoContent.savePath` returns `string`. - `Photo.savePath` is always a string, including in a library with no content cache, so do not route it only through the cache. - `isLocal` is true when the original is stored at the save path. - The cache serves `original()` from the save path when the original is stored there. **`photo.download()`** (async), resolving to the same `{ path, bytes, videoPath? }` as `original()`, at the save path: - If the original is already stored there, return it. - Otherwise get it the way backup does today: `backupOriginal(fileID, savePath)` writes a fetch straight to the save path and returns the cached copy when the cache holds one. - Then place it the way `placeOriginal` does: copy atomically from the cache, put a live photo's two files and its JSON beside it, and create the folders. - Move `placeOriginal` and `copyAtomic` out of `src/backup.ts` to where both can use them. Backup's originals phase becomes a call to the same code for each file. **Backup** (`src/backup.ts`): - Originals go to the save path under the backup's directory. The per-file metadata JSON is written beside the original as `YYYY-MM-DD.{fileID}.json`. - Keep `collections/`: each link points at the new path. Removing stale links recognises the links quak made into the date folders, and nothing the user made. - Leave `thumbnails/{fileID}.jpg` and `failures.json` as they are. **Docs.** - README "Backup layout": rewrite it for the new tree. - README "Read surface" and "Opening a library": cover `savePath`, `isLocal`, `download()` and the `./photos` default. - Update `TODO.md` wherever it describes the backup layout. **Tests.** - The layout and date folders, using a fixed date at noon so the time zone cannot shift the day. - The `./photos` default resolving against the working directory. - `savePath` without a content cache. - `isLocal`: false when the copy is only in the cache, true after `download()`. - `download()` copying from the cache when it is cached, with the source never called. - `download()` fetching when nothing is cached. - `download()` doing nothing on a second call. - Live photos. - `lib.backup()` writing the new layout. - `collections/` links pointing at the new paths. Existing backup tests change only their expected paths to the new layout. Never drop or relax one of their checks. Out of scope: a ULID, and a year or date filter. **Process** - Branch from current `next`, with one PR whose base is `next`. Title: `Save path ./photos/YYYY/YYYY-MM/YYYY-MM-DD/YYYY-MM-DD.fileID.ext; download() from the cache first (closes #143)`. Label it `needs-review` and assign `clawbot`. - Gate only with `make check`, and format only with `make fmt`. Both run in Docker. Run Docker builds under `flock -w 1800 /srv/code/tmp/.docker-build.lock`. A timeout there exits 1 without running anything; retry, and never read it as red. Remove every container you start. - Pull and rebase onto `next` again right before pushing, and re-run `make check` after any conflict resolution. - Edit files by hand. No `sed -i`, `perl -pi`, `awk` or scripted replacements. - Keep the change small and plain enough for a newcomer to follow in one reading. Coin no new terms. - Ask no interactive questions. A point only sneak can settle goes on the PR with the reading you took. - Commits and the PR body end with a `Model:` line naming your model id. Never name the company. - Stay under 2 GiB of RAM. Model: opus-5-5
Owner

local timezone yes. there are no existing backups, nobody has ever used this pre-1.0 prototype alpha software.

local timezone yes. there are no existing backups, nobody has ever used this pre-1.0 prototype alpha software.
Author
Collaborator

Owner ruling on both questions of 107571 (here as 107662, and in chat, 2026-10-01 ~18:06 UTC):

  • Question 1: the date folders use the local time zone of the machine running quak.
  • Question 2: quak is pre-1.0 alpha prototype software that nobody has ever run, so no existing backups exist. Nothing moves or migrates an old originals/ layout. The old layout is simply gone: no migration code, no detection of the old tree, and no tests or docs about it. This is the standing pre-1.0 rule: no installed base, so no migrations.

model: opus-5-5

Owner ruling on both questions of 107571 (here as 107662, and in chat, 2026-10-01 ~18:06 UTC): - Question 1: the date folders use the local time zone of the machine running quak. - Question 2: quak is pre-1.0 alpha prototype software that nobody has ever run, so no existing backups exist. Nothing moves or migrates an old `originals/` layout. The old layout is simply gone: no migration code, no detection of the old tree, and no tests or docs about it. This is the standing pre-1.0 rule: no installed base, so no migrations. model: opus-5-5
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/quak#143