fix(core,fuse,gui,cli): pair a Pixel raw with its JPEG, and stop refresh timing out

Three things the 12,445-photo library showed that the tests did not.

A Pixel raw capture is two files named after the role each plays —
`PXL_….RAW-01.COVER.jpg` and `PXL_….RAW-02.ORIGINAL.dng` — so their stems differ
and the pairing rule never saw one shot. The marker is now cut off before names
are compared, narrowly: digits, then one role word, so an ordinary name that
happens to contain `.raw-` keeps its stem. Fed the real library's first 40 rows
through the grouping, they collapse to 21 tiles.

A raw that has a JPEG beside it is now painted with that JPEG's thumbnail, which
the server usually already holds. The Raw tab costs no raw download and no raw
decode for those tiles. Raws are found in one batched read, so an ordinary page
pays nothing for the lookup.

Photos could not be moved to Trash at all: they live on the photo volume, which
the mount never holds, so no `nodes` row exists, every authority answered
`Unknown`, and the write check refused the whole library with "trash access: not
allowed". Membership of our own timeline is the authority that does exist for a
photo, and it now answers. An album photo on someone else's volume still fails
closed.

Refreshing the gallery then reported `io: Resource temporarily unavailable (os
error 11)` (B92). The daemon held the control reply until the whole timeline had
been re-read — 63 batches of 200 nodes for this library, minutes of work against
a 120 s read bound. The daemon now answers as soon as the refresh is running,
`PhotosRefreshStatus` reports when it is done, and both front ends follow it: the
gallery reloads the page when it clears, and `pdfs refresh photos` blocks so the
command still means what it says. Each refresh logs its photo count, batch count
and duration.

Released as 1.11.0.
This commit is contained in:
nvrl committed 2026-09-19 17:08:33 +02:00
1 parent 09c0684a5e
commit 376f3edb28
12 files changed
+378 -30

No files matched your search

+28 -1
View File
@@ -135,6 +135,12 @@ pub enum Request {
CancelThumbnailBuild,
/// Read progress for the recursive ordinary-file thumbnail build.
ThumbnailBuildStatus,
/// Whether a photos-timeline refresh is running right now. Replies with
/// [`Response::PhotosRefresh`].
///
/// [`RefreshScope::Photos`] starts the refresh and returns at once, so this
/// is how a front-end knows when the refreshed timeline is worth re-reading.
PhotosRefreshStatus,
/// Download a photo's full content into the cache; replies with its path.
OpenPhoto { uid: String },
/// Add or remove Proton's `Favorite` tag on a photo. Replies with
@@ -582,7 +588,10 @@ pub enum RefreshScope {
Dir { path: String },
/// The trash listing.
Trash,
/// The photos timeline.
/// The photos timeline. The reply comes back as soon as the refresh has
/// *started*, not when it has finished: re-reading a large library takes
/// minutes, which is far longer than a control reply may take. Poll
/// [`Request::PhotosRefreshStatus`] to know when the new timeline is there.
Photos,
}
@@ -1636,6 +1645,9 @@ pub enum Response {
FileThumbGeneration { generation: u64 },
/// Current recursive thumbnail-build progress.
ThumbnailBuild { status: ThumbnailBuildStatus },
/// Whether a photos-timeline refresh is still running (reply to
/// [`Request::PhotosRefreshStatus`]).
PhotosRefresh { running: bool },
/// An on-disk path the front-end can open (e.g. a downloaded photo).
FilePath { path: String },
/// Full-text search results (reply to [`Request::Search`]).
@@ -2065,6 +2077,21 @@ mod tests {
assert_eq!(line, serde_json::to_string(&decoded).unwrap());
}
/// A photos refresh is followed, not awaited, so the request that reports
/// its progress is part of the wire contract.
#[test]
fn photos_refresh_status_roundtrips() {
let line = serde_json::to_string(&Request::PhotosRefreshStatus).unwrap();
assert_eq!(line, r#""PhotosRefreshStatus""#);
let decoded: Request = serde_json::from_str(&line).unwrap();
assert_eq!(line, serde_json::to_string(&decoded).unwrap());
let line = serde_json::to_string(&Response::PhotosRefresh { running: true }).unwrap();
assert_eq!(line, r#"{"PhotosRefresh":{"running":true}}"#);
let decoded: Response = serde_json::from_str(&line).unwrap();
assert_eq!(line, serde_json::to_string(&decoded).unwrap());
}
#[test]
fn unified_search_request_and_response_roundtrip() {
let request = Request::SearchV2 {