From 7fba28f7d8bfa6356f608f67bf8bb538da7f6691 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sun, 20 Sep 2026 00:12:33 +0200 Subject: [PATCH] Upload a snapshot of a thumbnail shard, never the live file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every shard is in WAL mode and every put opens its own connection, so while thumbnails are being generated on several threads — which is when the first sync pass runs — the log is never checkpointed and the main file holds whatever the last quiet moment left in it. For a shard created seconds earlier that is nothing: a zero-byte file with the schema still in the log. The sync read that file and uploaded it, and every other device merging it failed with "no such table: thumbs" on every pass. Copy the shard through SQLite's backup API into scratch first, which serialises against writers and carries the log, and upload that. --- core/dr-thumbs/src/lib.rs | 63 ++++++++++++++++++++++++++++++++++++ docs/gestures.md | 16 ++++----- docs/traceability.md | 8 ++--- ui/dr-ui/src/derived_sync.rs | 17 ++++++++-- 4 files changed, 89 insertions(+), 15 deletions(-) diff --git a/core/dr-thumbs/src/lib.rs b/core/dr-thumbs/src/lib.rs index e443cad..6b77960 100644 --- a/core/dr-thumbs/src/lib.rs +++ b/core/dr-thumbs/src/lib.rs @@ -481,6 +481,30 @@ impl ThumbStore { self.dir.join(format!("shard-{shard:04}.sqlite")) } + /// Write a coherent copy of one shard to `dest`, ready to upload. + /// + /// Not a file copy. Every shard is in WAL mode and every `put` opens its + /// own connection, so while thumbnails are being generated on several + /// threads at once — which is exactly when the first sync pass runs — + /// there is nearly always a connection open and the log is never + /// checkpointed. The main file then holds whatever the *last* quiet + /// moment left in it, which for a shard created seconds ago is nothing: + /// zero bytes, the schema still in the log. Reading it uploaded an empty + /// file, and every other device merging it failed with "no such table: + /// thumbs". The backup API serialises against writers and copies the + /// database as it is, log included. + pub fn snapshot_shard(&self, shard: u32, dest: &Path) -> Result<(), ThumbError> { + let source = self.open_shard(shard, false)?; + let _ = std::fs::remove_file(dest); + let mut out = Connection::open(dest)?; + let backup = rusqlite::backup::Backup::new(&source, &mut out)?; + // rusqlite asserts a positive page count where SQLite would take -1 + // for "everything"; a shard is capped well under this many pages. + backup.run_to_completion(i32::MAX, std::time::Duration::ZERO, None)?; + drop(backup); + Ok(()) + } + pub fn index_path(&self) -> PathBuf { self.dir.join("index.sqlite") } @@ -726,6 +750,45 @@ mod tests { } } + #[test] + fn a_snapshot_carries_what_the_shard_file_does_not_yet() { + // A thumbnail worker holding the shard open keeps the log from being + // checkpointed; the file on disk is then not the database. The + // upload used to read that file. + let (mut s, _d) = store(); + s.put(1, ThumbSize::Grid, &thumb(1024)).unwrap(); + let path = s.shard_path(0); + let worker = Connection::open(&path).unwrap(); + worker.pragma_update(None, "journal_mode", "WAL").unwrap(); + s.put(2, ThumbSize::Grid, &thumb(2048)).unwrap(); + s.put(3, ThumbSize::Grid, &thumb(2048)).unwrap(); + + // What a byte-for-byte reader sees is at most what the last + // checkpoint left; the log is the part a copy misses. + let copy = _d.join("copied.sqlite"); + std::fs::copy(&path, ©).unwrap(); + let file_rows = Connection::open(©) + .ok() + .and_then(|c| { + c.query_row("SELECT count(*) FROM thumbs", [], |r| r.get::<_, i64>(0)) + .ok() + }) + .unwrap_or(0); + + let snap = _d.join("upload.sqlite"); + s.snapshot_shard(0, &snap).unwrap(); + let snapshot = Connection::open(&snap).unwrap(); + let rows: i64 = snapshot + .query_row("SELECT count(*) FROM thumbs", [], |r| r.get(0)) + .unwrap(); + assert_eq!(rows, 3, "the snapshot is the whole shard"); + assert!( + file_rows < 3, + "the file alone lagged ({file_rows} rows), which is what the snapshot exists for" + ); + drop(worker); + } + #[test] fn a_forgotten_thumbnail_is_no_longer_served() { // The point of the whole method: a purged photograph must not keep a diff --git a/docs/gestures.md b/docs/gestures.md index 0464d09..506f08f 100644 --- a/docs/gestures.md +++ b/docs/gestures.md @@ -326,7 +326,7 @@ Face indexing reads each face's eyes. The chip drops frames where the chosen peo There is no wheel on a tablet, so without the pinch the cell size could only be changed by a control a finger cannot reach. -`ui/dr-ui/ui/library.slint:2789` +`ui/dr-ui/ui/library.slint:2795` ### File photographs in a collection @@ -335,7 +335,7 @@ There is no wheel on a tablet, so without the pinch the cell size could only be The selection is what the drag carries, which is why selecting several is worth the mode: forty photographs file in one gesture. -`ui/dr-ui/ui/library.slint:2986` +`ui/dr-ui/ui/library.slint:2992` ### Open a photograph @@ -344,7 +344,7 @@ The selection is what the drag carries, which is why selecting several is worth A tap opens; a tap that *moved* does not. Travel is what separates a deliberate tap from a hand brushing past, and it is the only thing that does: the two are the same length. An earlier version required the finger to dwell 120 ms instead, and that rejected ordinary taps — a real tap is often quicker than a brush. -`ui/dr-ui/ui/library.slint:3254` +`ui/dr-ui/ui/library.slint:3260` ### Rate a photograph without opening it @@ -354,7 +354,7 @@ A tap opens; a tap that *moved* does not. Travel is what separates a deliberate A star has to take the press without it also reaching the cell, or every rating throws the user into develop. -`ui/dr-ui/ui/library.slint:3374` +`ui/dr-ui/ui/library.slint:3380` ### Choose the frame a folded burst shows @@ -363,7 +363,7 @@ A star has to take the press without it also reaching the cell, or every rating A folded burst draws its earliest frame, which is a fact about the clock and not a judgement about the photograph — nothing in this application ranks a frame (FR-CULL-5). But the point of a burst is that one of the twelve is better than the other eleven, and the photographer is the only one who knows which. So the choice is offered on the frames themselves, while they are open and side by side, which is the one moment the alternatives are on screen to be compared. -`ui/dr-ui/ui/library.slint:3505` +`ui/dr-ui/ui/library.slint:3511` ### Drop the selection but keep selecting @@ -372,7 +372,7 @@ A folded burst draws its earliest frame, which is a fact about the clock and not Distinct from Done, which leaves the mode entirely. Clearing keeps it, so the next selection can start straight away. -`ui/dr-ui/ui/library.slint:4166` +`ui/dr-ui/ui/library.slint:4177` ### Select everything the grid is showing @@ -381,7 +381,7 @@ Distinct from Done, which leaves the mode entirely. Clearing keeps it, so the ne A scoped grid of two hundred frames is two hundred taps otherwise, and "all of them, except those three" is a far more common shape than the taps it took to say it. -`ui/dr-ui/ui/library.slint:4183` +`ui/dr-ui/ui/library.slint:4194` ### Take photographs out of a collection @@ -390,4 +390,4 @@ A scoped grid of two hundred frames is two hundred taps otherwise, and "all of t The badge on a cell says a photograph is filed in three collections and never which. This is the sheet that names them, and the only way out of one the grid is not currently scoped to. -`ui/dr-ui/ui/library.slint:4307` +`ui/dr-ui/ui/library.slint:4318` diff --git a/docs/traceability.md b/docs/traceability.md index 256d5fe..84de72c 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -121,13 +121,13 @@ A tag naming an ID `requirements.md` does not define — what renumbering produc | FR-NC-6 | [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1) | | FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-catalog/src/schema.rs:1048`](../core/dr-catalog/src/schema.rs#L1048), [`core/dr-catalog/src/schema.rs:1503`](../core/dr-catalog/src/schema.rs#L1503), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:2940`](../ui/dr-ui/src/collections_ui.rs#L2940), [`ui/dr-ui/src/collections_ui.rs:3182`](../ui/dr-ui/src/collections_ui.rs#L3182), [`ui/dr-ui/src/collections_ui.rs:4083`](../ui/dr-ui/src/collections_ui.rs#L4083), [`ui/dr-ui/src/collections_ui.rs:785`](../ui/dr-ui/src/collections_ui.rs#L785), [`ui/dr-ui/src/collections_ui.rs:856`](../ui/dr-ui/src/collections_ui.rs#L856), [`ui/dr-ui/src/lib.rs:1350`](../ui/dr-ui/src/lib.rs#L1350), [`ui/dr-ui/src/lib.rs:2487`](../ui/dr-ui/src/lib.rs#L2487), [`ui/dr-ui/src/lib.rs:2558`](../ui/dr-ui/src/lib.rs#L2558), [`ui/dr-ui/src/lib.rs:3854`](../ui/dr-ui/src/lib.rs#L3854), [`ui/dr-ui/src/lib.rs:3855`](../ui/dr-ui/src/lib.rs#L3855), [`ui/dr-ui/src/lib.rs:3894`](../ui/dr-ui/src/lib.rs#L3894), [`ui/dr-ui/src/library.rs:2173`](../ui/dr-ui/src/library.rs#L2173), [`ui/dr-ui/src/library.rs:2196`](../ui/dr-ui/src/library.rs#L2196), [`ui/dr-ui/src/library.rs:2471`](../ui/dr-ui/src/library.rs#L2471), [`ui/dr-ui/src/library.rs:2652`](../ui/dr-ui/src/library.rs#L2652), [`ui/dr-ui/src/library.rs:2789`](../ui/dr-ui/src/library.rs#L2789), [`ui/dr-ui/src/library.rs:2804`](../ui/dr-ui/src/library.rs#L2804), [`ui/dr-ui/src/library_ui.rs:1482`](../ui/dr-ui/src/library_ui.rs#L1482), [`ui/dr-ui/src/library_ui.rs:1555`](../ui/dr-ui/src/library_ui.rs#L1555), [`ui/dr-ui/src/library_ui.rs:1656`](../ui/dr-ui/src/library_ui.rs#L1656), [`ui/dr-ui/src/library_ui.rs:1711`](../ui/dr-ui/src/library_ui.rs#L1711), [`ui/dr-ui/src/library_ui.rs:1846`](../ui/dr-ui/src/library_ui.rs#L1846), [`ui/dr-ui/src/library_ui.rs:1964`](../ui/dr-ui/src/library_ui.rs#L1964), [`ui/dr-ui/src/library_ui.rs:2731`](../ui/dr-ui/src/library_ui.rs#L2731), [`ui/dr-ui/src/library_ui.rs:344`](../ui/dr-ui/src/library_ui.rs#L344), [`ui/dr-ui/src/library_ui.rs:355`](../ui/dr-ui/src/library_ui.rs#L355), [`ui/dr-ui/src/library_ui.rs:370`](../ui/dr-ui/src/library_ui.rs#L370), [`ui/dr-ui/src/library_ui.rs:379`](../ui/dr-ui/src/library_ui.rs#L379), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:517`](../ui/dr-ui/src/library_ui.rs#L517), [`ui/dr-ui/src/library_ui.rs:527`](../ui/dr-ui/src/library_ui.rs#L527), [`ui/dr-ui/src/library_ui.rs:542`](../ui/dr-ui/src/library_ui.rs#L542), [`ui/dr-ui/src/library_ui.rs:589`](../ui/dr-ui/src/library_ui.rs#L589), [`ui/dr-ui/src/library_ui.rs:604`](../ui/dr-ui/src/library_ui.rs#L604), [`ui/dr-ui/src/library_ui.rs:678`](../ui/dr-ui/src/library_ui.rs#L678), [`ui/dr-ui/src/library_ui.rs:6902`](../ui/dr-ui/src/library_ui.rs#L6902), [`ui/dr-ui/src/library_ui.rs:6920`](../ui/dr-ui/src/library_ui.rs#L6920), [`ui/dr-ui/src/library_ui.rs:709`](../ui/dr-ui/src/library_ui.rs#L709), [`ui/dr-ui/src/library_ui.rs:721`](../ui/dr-ui/src/library_ui.rs#L721), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:157`](../ui/dr-ui/src/settings_ui.rs#L157), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:524`](../ui/dr-ui/src/settings_ui.rs#L524), [`ui/dr-ui/ui/app.slint:1610`](../ui/dr-ui/ui/app.slint#L1610), [`ui/dr-ui/ui/app.slint:3163`](../ui/dr-ui/ui/app.slint#L3163), [`ui/dr-ui/ui/app.slint:541`](../ui/dr-ui/ui/app.slint#L541), [`ui/dr-ui/ui/app.slint:549`](../ui/dr-ui/ui/app.slint#L549), [`ui/dr-ui/ui/app.slint:555`](../ui/dr-ui/ui/app.slint#L555), [`ui/dr-ui/ui/collections.slint:1037`](../ui/dr-ui/ui/collections.slint#L1037), [`ui/dr-ui/ui/collections.slint:1048`](../ui/dr-ui/ui/collections.slint#L1048), [`ui/dr-ui/ui/collections.slint:1127`](../ui/dr-ui/ui/collections.slint#L1127), [`ui/dr-ui/ui/collections.slint:272`](../ui/dr-ui/ui/collections.slint#L272), [`ui/dr-ui/ui/collections.slint:424`](../ui/dr-ui/ui/collections.slint#L424), [`ui/dr-ui/ui/collections.slint:433`](../ui/dr-ui/ui/collections.slint#L433), [`ui/dr-ui/ui/collections.slint:436`](../ui/dr-ui/ui/collections.slint#L436), [`ui/dr-ui/ui/collections.slint:482`](../ui/dr-ui/ui/collections.slint#L482), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:720`](../ui/dr-ui/ui/collections.slint#L720), [`ui/dr-ui/ui/collections.slint:877`](../ui/dr-ui/ui/collections.slint#L877), [`ui/dr-ui/ui/collections.slint:91`](../ui/dr-ui/ui/collections.slint#L91), [`ui/dr-ui/ui/icons.slint:262`](../ui/dr-ui/ui/icons.slint#L262), [`ui/dr-ui/ui/settings.slint:114`](../ui/dr-ui/ui/settings.slint#L114), [`ui/dr-ui/ui/settings.slint:656`](../ui/dr-ui/ui/settings.slint#L656) | | FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1711`](../ui/dr-ui/src/library_ui.rs#L1711), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | -| FR-NC-6c | [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync-folder/src/lib.rs:818`](../core/dr-sync-folder/src/lib.rs#L818), [`core/dr-sync-folder/src/lib.rs:857`](../core/dr-sync-folder/src/lib.rs#L857), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1), [`core/dr-sync-nextcloud/src/desktop_client.rs:172`](../core/dr-sync-nextcloud/src/desktop_client.rs#L172), [`core/dr-sync-nextcloud/src/desktop_client.rs:35`](../core/dr-sync-nextcloud/src/desktop_client.rs#L35), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-sync/src/error.rs:39`](../core/dr-sync/src/error.rs#L39), [`core/dr-sync/src/lib.rs:158`](../core/dr-sync/src/lib.rs#L158), [`core/dr-sync/src/types.rs:101`](../core/dr-sync/src/types.rs#L101), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`core/dr-types/src/lib.rs:204`](../core/dr-types/src/lib.rs#L204), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:4083`](../ui/dr-ui/src/collections_ui.rs#L4083), [`ui/dr-ui/src/collections_ui.rs:785`](../ui/dr-ui/src/collections_ui.rs#L785), [`ui/dr-ui/src/collections_ui.rs:856`](../ui/dr-ui/src/collections_ui.rs#L856), [`ui/dr-ui/src/derived_sync.rs:1031`](../ui/dr-ui/src/derived_sync.rs#L1031), [`ui/dr-ui/src/derived_sync.rs:631`](../ui/dr-ui/src/derived_sync.rs#L631), [`ui/dr-ui/src/library.rs:1084`](../ui/dr-ui/src/library.rs#L1084), [`ui/dr-ui/src/library.rs:2293`](../ui/dr-ui/src/library.rs#L2293), [`ui/dr-ui/src/library.rs:2383`](../ui/dr-ui/src/library.rs#L2383), [`ui/dr-ui/src/library.rs:4060`](../ui/dr-ui/src/library.rs#L4060), [`ui/dr-ui/src/library_ui.rs:1482`](../ui/dr-ui/src/library_ui.rs#L1482), [`ui/dr-ui/src/library_ui.rs:1555`](../ui/dr-ui/src/library_ui.rs#L1555), [`ui/dr-ui/src/library_ui.rs:1754`](../ui/dr-ui/src/library_ui.rs#L1754), [`ui/dr-ui/src/remote.rs:40`](../ui/dr-ui/src/remote.rs#L40), [`ui/dr-ui/src/repairs.rs:1049`](../ui/dr-ui/src/repairs.rs#L1049), [`ui/dr-ui/ui/collections.slint:272`](../ui/dr-ui/ui/collections.slint#L272), [`ui/dr-ui/ui/collections.slint:877`](../ui/dr-ui/ui/collections.slint#L877), [`ui/dr-ui/ui/icons.slint:262`](../ui/dr-ui/ui/icons.slint#L262) | +| FR-NC-6c | [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync-folder/src/lib.rs:818`](../core/dr-sync-folder/src/lib.rs#L818), [`core/dr-sync-folder/src/lib.rs:857`](../core/dr-sync-folder/src/lib.rs#L857), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1), [`core/dr-sync-nextcloud/src/desktop_client.rs:172`](../core/dr-sync-nextcloud/src/desktop_client.rs#L172), [`core/dr-sync-nextcloud/src/desktop_client.rs:35`](../core/dr-sync-nextcloud/src/desktop_client.rs#L35), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-sync/src/error.rs:39`](../core/dr-sync/src/error.rs#L39), [`core/dr-sync/src/lib.rs:158`](../core/dr-sync/src/lib.rs#L158), [`core/dr-sync/src/types.rs:101`](../core/dr-sync/src/types.rs#L101), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`core/dr-types/src/lib.rs:204`](../core/dr-types/src/lib.rs#L204), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:4083`](../ui/dr-ui/src/collections_ui.rs#L4083), [`ui/dr-ui/src/collections_ui.rs:785`](../ui/dr-ui/src/collections_ui.rs#L785), [`ui/dr-ui/src/collections_ui.rs:856`](../ui/dr-ui/src/collections_ui.rs#L856), [`ui/dr-ui/src/derived_sync.rs:1042`](../ui/dr-ui/src/derived_sync.rs#L1042), [`ui/dr-ui/src/derived_sync.rs:642`](../ui/dr-ui/src/derived_sync.rs#L642), [`ui/dr-ui/src/library.rs:1084`](../ui/dr-ui/src/library.rs#L1084), [`ui/dr-ui/src/library.rs:2293`](../ui/dr-ui/src/library.rs#L2293), [`ui/dr-ui/src/library.rs:2383`](../ui/dr-ui/src/library.rs#L2383), [`ui/dr-ui/src/library.rs:4060`](../ui/dr-ui/src/library.rs#L4060), [`ui/dr-ui/src/library_ui.rs:1482`](../ui/dr-ui/src/library_ui.rs#L1482), [`ui/dr-ui/src/library_ui.rs:1555`](../ui/dr-ui/src/library_ui.rs#L1555), [`ui/dr-ui/src/library_ui.rs:1754`](../ui/dr-ui/src/library_ui.rs#L1754), [`ui/dr-ui/src/remote.rs:40`](../ui/dr-ui/src/remote.rs#L40), [`ui/dr-ui/src/repairs.rs:1049`](../ui/dr-ui/src/repairs.rs#L1049), [`ui/dr-ui/ui/collections.slint:272`](../ui/dr-ui/ui/collections.slint#L272), [`ui/dr-ui/ui/collections.slint:877`](../ui/dr-ui/ui/collections.slint#L877), [`ui/dr-ui/ui/icons.slint:262`](../ui/dr-ui/ui/icons.slint#L262) | | FR-NC-6d | [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1) | | FR-NC-7 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:105`](../core/dr-sync-nextcloud/src/lib.rs#L105), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:3940`](../ui/dr-ui/src/library.rs#L3940), [`ui/dr-ui/src/library_ui.rs:4536`](../ui/dr-ui/src/library_ui.rs#L4536), [`ui/dr-ui/ui/settings.slint:435`](../ui/dr-ui/ui/settings.slint#L435) | | FR-NC-7a | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`core/dr-types/src/settings.rs:377`](../core/dr-types/src/settings.rs#L377), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1559`](../ui/dr-ui/src/lib.rs#L1559), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | FR-NC-7b | [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`ui/dr-ui/src/import.rs:122`](../ui/dr-ui/src/import.rs#L122), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import.rs:584`](../ui/dr-ui/src/import.rs#L584), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1559`](../ui/dr-ui/src/lib.rs#L1559) | | FR-NC-8 | [`core/dr-catalog/src/rating.rs:204`](../core/dr-catalog/src/rating.rs#L204), [`core/dr-catalog/src/rating.rs:66`](../core/dr-catalog/src/rating.rs#L66), [`core/dr-catalog/src/rating.rs:929`](../core/dr-catalog/src/rating.rs#L929), [`core/dr-catalog/src/schema.rs:231`](../core/dr-catalog/src/schema.rs#L231), [`core/dr-pipeline/src/sidecar.rs:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`core/dr-pipeline/src/sidecar.rs:2991`](../core/dr-pipeline/src/sidecar.rs#L2991), [`core/dr-pipeline/src/sidecar.rs:660`](../core/dr-pipeline/src/sidecar.rs#L660), [`core/dr-pipeline/src/sidecar.rs:735`](../core/dr-pipeline/src/sidecar.rs#L735), [`core/dr-pipeline/src/sidecar.rs:96`](../core/dr-pipeline/src/sidecar.rs#L96), [`ui/dr-ui/src/lib.rs:2457`](../ui/dr-ui/src/lib.rs#L2457), [`ui/dr-ui/src/library.rs:1126`](../ui/dr-ui/src/library.rs#L1126), [`ui/dr-ui/src/library.rs:2582`](../ui/dr-ui/src/library.rs#L2582), [`ui/dr-ui/src/library.rs:564`](../ui/dr-ui/src/library.rs#L564), [`ui/dr-ui/src/library.rs:7554`](../ui/dr-ui/src/library.rs#L7554), [`ui/dr-ui/src/library.rs:824`](../ui/dr-ui/src/library.rs#L824), [`ui/dr-ui/src/library_ui.rs:630`](../ui/dr-ui/src/library_ui.rs#L630), [`ui/dr-ui/src/presets.rs:288`](../ui/dr-ui/src/presets.rs#L288), [`ui/dr-ui/src/presets.rs:336`](../ui/dr-ui/src/presets.rs#L336) | -| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:204`](../core/dr-catalog/src/rating.rs#L204), [`core/dr-catalog/src/rating.rs:66`](../core/dr-catalog/src/rating.rs#L66), [`core/dr-catalog/src/rating.rs:929`](../core/dr-catalog/src/rating.rs#L929), [`core/dr-catalog/src/schema.rs:231`](../core/dr-catalog/src/schema.rs#L231), [`core/dr-catalog/src/schema.rs:644`](../core/dr-catalog/src/schema.rs#L644), [`core/dr-catalog/src/schema.rs:991`](../core/dr-catalog/src/schema.rs#L991), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:192`](../core/dr-pipeline/src/sidecar.rs#L192), [`core/dr-pipeline/src/sidecar.rs:219`](../core/dr-pipeline/src/sidecar.rs#L219), [`core/dr-pipeline/src/sidecar.rs:2520`](../core/dr-pipeline/src/sidecar.rs#L2520), [`core/dr-pipeline/src/sidecar.rs:2757`](../core/dr-pipeline/src/sidecar.rs#L2757), [`core/dr-pipeline/src/sidecar.rs:2991`](../core/dr-pipeline/src/sidecar.rs#L2991), [`core/dr-pipeline/src/sidecar.rs:395`](../core/dr-pipeline/src/sidecar.rs#L395), [`core/dr-pipeline/src/sidecar.rs:493`](../core/dr-pipeline/src/sidecar.rs#L493), [`core/dr-pipeline/src/sidecar.rs:660`](../core/dr-pipeline/src/sidecar.rs#L660), [`core/dr-pipeline/src/sidecar.rs:710`](../core/dr-pipeline/src/sidecar.rs#L710), [`core/dr-pipeline/src/sidecar.rs:735`](../core/dr-pipeline/src/sidecar.rs#L735), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/mask_sidecar.rs:1075`](../core/dr-pipeline/tests/mask_sidecar.rs#L1075), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:124`](../core/dr-sync-nextcloud/src/lib.rs#L124), [`core/dr-sync/src/scan.rs:31`](../core/dr-sync/src/scan.rs#L31), [`core/dr-sync/src/scan.rs:56`](../core/dr-sync/src/scan.rs#L56), [`core/dr-sync/src/scan.rs:926`](../core/dr-sync/src/scan.rs#L926), [`core/dr-xmp/src/lib.rs:733`](../core/dr-xmp/src/lib.rs#L733), [`core/dr-xmp/src/lib.rs:746`](../core/dr-xmp/src/lib.rs#L746), [`ui/dr-ui/src/derived_sync.rs:1059`](../ui/dr-ui/src/derived_sync.rs#L1059), [`ui/dr-ui/src/derived_sync.rs:602`](../ui/dr-ui/src/derived_sync.rs#L602), [`ui/dr-ui/src/derived_sync.rs:631`](../ui/dr-ui/src/derived_sync.rs#L631), [`ui/dr-ui/src/derived_sync.rs:664`](../ui/dr-ui/src/derived_sync.rs#L664), [`ui/dr-ui/src/derived_sync.rs:810`](../ui/dr-ui/src/derived_sync.rs#L810), [`ui/dr-ui/src/library.rs:1126`](../ui/dr-ui/src/library.rs#L1126), [`ui/dr-ui/src/library.rs:1146`](../ui/dr-ui/src/library.rs#L1146), [`ui/dr-ui/src/library.rs:1455`](../ui/dr-ui/src/library.rs#L1455), [`ui/dr-ui/src/library.rs:1490`](../ui/dr-ui/src/library.rs#L1490), [`ui/dr-ui/src/library.rs:2582`](../ui/dr-ui/src/library.rs#L2582), [`ui/dr-ui/src/library.rs:7386`](../ui/dr-ui/src/library.rs#L7386), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:7554`](../ui/dr-ui/src/library.rs#L7554), [`ui/dr-ui/src/library.rs:824`](../ui/dr-ui/src/library.rs#L824), [`ui/dr-ui/src/library.rs:896`](../ui/dr-ui/src/library.rs#L896), [`ui/dr-ui/src/library.rs:980`](../ui/dr-ui/src/library.rs#L980), [`ui/dr-ui/src/library_ui.rs:1339`](../ui/dr-ui/src/library_ui.rs#L1339), [`ui/dr-ui/src/presets.rs:288`](../ui/dr-ui/src/presets.rs#L288), [`ui/dr-ui/src/presets.rs:336`](../ui/dr-ui/src/presets.rs#L336), [`ui/dr-ui/src/xmp_sync.rs:1`](../ui/dr-ui/src/xmp_sync.rs#L1), [`ui/dr-ui/src/xmp_sync.rs:287`](../ui/dr-ui/src/xmp_sync.rs#L287) | +| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:204`](../core/dr-catalog/src/rating.rs#L204), [`core/dr-catalog/src/rating.rs:66`](../core/dr-catalog/src/rating.rs#L66), [`core/dr-catalog/src/rating.rs:929`](../core/dr-catalog/src/rating.rs#L929), [`core/dr-catalog/src/schema.rs:231`](../core/dr-catalog/src/schema.rs#L231), [`core/dr-catalog/src/schema.rs:644`](../core/dr-catalog/src/schema.rs#L644), [`core/dr-catalog/src/schema.rs:991`](../core/dr-catalog/src/schema.rs#L991), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:192`](../core/dr-pipeline/src/sidecar.rs#L192), [`core/dr-pipeline/src/sidecar.rs:219`](../core/dr-pipeline/src/sidecar.rs#L219), [`core/dr-pipeline/src/sidecar.rs:2520`](../core/dr-pipeline/src/sidecar.rs#L2520), [`core/dr-pipeline/src/sidecar.rs:2757`](../core/dr-pipeline/src/sidecar.rs#L2757), [`core/dr-pipeline/src/sidecar.rs:2991`](../core/dr-pipeline/src/sidecar.rs#L2991), [`core/dr-pipeline/src/sidecar.rs:395`](../core/dr-pipeline/src/sidecar.rs#L395), [`core/dr-pipeline/src/sidecar.rs:493`](../core/dr-pipeline/src/sidecar.rs#L493), [`core/dr-pipeline/src/sidecar.rs:660`](../core/dr-pipeline/src/sidecar.rs#L660), [`core/dr-pipeline/src/sidecar.rs:710`](../core/dr-pipeline/src/sidecar.rs#L710), [`core/dr-pipeline/src/sidecar.rs:735`](../core/dr-pipeline/src/sidecar.rs#L735), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/mask_sidecar.rs:1075`](../core/dr-pipeline/tests/mask_sidecar.rs#L1075), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:124`](../core/dr-sync-nextcloud/src/lib.rs#L124), [`core/dr-sync/src/scan.rs:31`](../core/dr-sync/src/scan.rs#L31), [`core/dr-sync/src/scan.rs:56`](../core/dr-sync/src/scan.rs#L56), [`core/dr-sync/src/scan.rs:926`](../core/dr-sync/src/scan.rs#L926), [`core/dr-xmp/src/lib.rs:733`](../core/dr-xmp/src/lib.rs#L733), [`core/dr-xmp/src/lib.rs:746`](../core/dr-xmp/src/lib.rs#L746), [`ui/dr-ui/src/derived_sync.rs:1070`](../ui/dr-ui/src/derived_sync.rs#L1070), [`ui/dr-ui/src/derived_sync.rs:613`](../ui/dr-ui/src/derived_sync.rs#L613), [`ui/dr-ui/src/derived_sync.rs:642`](../ui/dr-ui/src/derived_sync.rs#L642), [`ui/dr-ui/src/derived_sync.rs:675`](../ui/dr-ui/src/derived_sync.rs#L675), [`ui/dr-ui/src/derived_sync.rs:821`](../ui/dr-ui/src/derived_sync.rs#L821), [`ui/dr-ui/src/library.rs:1126`](../ui/dr-ui/src/library.rs#L1126), [`ui/dr-ui/src/library.rs:1146`](../ui/dr-ui/src/library.rs#L1146), [`ui/dr-ui/src/library.rs:1455`](../ui/dr-ui/src/library.rs#L1455), [`ui/dr-ui/src/library.rs:1490`](../ui/dr-ui/src/library.rs#L1490), [`ui/dr-ui/src/library.rs:2582`](../ui/dr-ui/src/library.rs#L2582), [`ui/dr-ui/src/library.rs:7386`](../ui/dr-ui/src/library.rs#L7386), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:7554`](../ui/dr-ui/src/library.rs#L7554), [`ui/dr-ui/src/library.rs:824`](../ui/dr-ui/src/library.rs#L824), [`ui/dr-ui/src/library.rs:896`](../ui/dr-ui/src/library.rs#L896), [`ui/dr-ui/src/library.rs:980`](../ui/dr-ui/src/library.rs#L980), [`ui/dr-ui/src/library_ui.rs:1339`](../ui/dr-ui/src/library_ui.rs#L1339), [`ui/dr-ui/src/presets.rs:288`](../ui/dr-ui/src/presets.rs#L288), [`ui/dr-ui/src/presets.rs:336`](../ui/dr-ui/src/presets.rs#L336), [`ui/dr-ui/src/xmp_sync.rs:1`](../ui/dr-ui/src/xmp_sync.rs#L1), [`ui/dr-ui/src/xmp_sync.rs:287`](../ui/dr-ui/src/xmp_sync.rs#L287) | | FR-PLAT-AND-2 | [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-sync-folder/src/lib.rs:242`](../core/dr-sync-folder/src/lib.rs#L242), [`core/dr-sync-folder/src/tests.rs:51`](../core/dr-sync-folder/src/tests.rs#L51), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/error.rs:195`](../core/dr-sync/src/error.rs#L195), [`core/dr-sync/src/scan.rs:211`](../core/dr-sync/src/scan.rs#L211), [`core/dr-sync/src/scan.rs:519`](../core/dr-sync/src/scan.rs#L519), [`core/dr-sync/src/scan.rs:545`](../core/dr-sync/src/scan.rs#L545), [`core/dr-sync/src/scan.rs:574`](../core/dr-sync/src/scan.rs#L574), [`ui/dr-ui/src/library.rs:1355`](../ui/dr-ui/src/library.rs#L1355), [`ui/dr-ui/src/library.rs:1408`](../ui/dr-ui/src/library.rs#L1408), [`ui/dr-ui/src/library.rs:1439`](../ui/dr-ui/src/library.rs#L1439), [`ui/dr-ui/src/library.rs:2019`](../ui/dr-ui/src/library.rs#L2019), [`ui/dr-ui/src/library_ui.rs:1310`](../ui/dr-ui/src/library_ui.rs#L1310), [`ui/dr-ui/src/library_ui.rs:1406`](../ui/dr-ui/src/library_ui.rs#L1406), [`ui/dr-ui/src/library_ui.rs:2005`](../ui/dr-ui/src/library_ui.rs#L2005), [`ui/dr-ui/src/library_ui.rs:328`](../ui/dr-ui/src/library_ui.rs#L328), [`ui/dr-ui/src/library_ui.rs:573`](../ui/dr-ui/src/library_ui.rs#L573) | | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/runner.rs:1`](../core/dr-catalog/src/runner.rs#L1) | | FR-PLAT-AND-4 | [`core/dr-catalog/src/runner.rs:1`](../core/dr-catalog/src/runner.rs#L1) | @@ -149,7 +149,7 @@ A tag naming an ID `requirements.md` does not define — what renumbering produc | FR-UI-5 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/lib.rs:3331`](../ui/dr-ui/src/lib.rs#L3331), [`ui/dr-ui/src/lib.rs:4239`](../ui/dr-ui/src/lib.rs#L4239), [`ui/dr-ui/src/lib.rs:4430`](../ui/dr-ui/src/lib.rs#L4430), [`ui/dr-ui/src/masks_ui.rs:1518`](../ui/dr-ui/src/masks_ui.rs#L1518), [`ui/dr-ui/ui/app.slint:2569`](../ui/dr-ui/ui/app.slint#L2569), [`ui/dr-ui/ui/app.slint:2609`](../ui/dr-ui/ui/app.slint#L2609), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/session.slint:75`](../ui/dr-ui/ui/session.slint#L75) | | FR-UI-6 | [`ui/dr-ui/ui/widgets.slint:1`](../ui/dr-ui/ui/widgets.slint#L1) | | FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:195`](../core/dr-pipeline/src/descriptor.rs#L195), [`core/dr-pipeline/src/framing.rs:395`](../core/dr-pipeline/src/framing.rs#L395), [`core/dr-types/src/settings.rs:492`](../core/dr-types/src/settings.rs#L492), [`ui/dr-ui/src/develop.rs:1330`](../ui/dr-ui/src/develop.rs#L1330), [`ui/dr-ui/src/lib.rs:4064`](../ui/dr-ui/src/lib.rs#L4064), [`ui/dr-ui/ui/adjust.slint:974`](../ui/dr-ui/ui/adjust.slint#L974), [`ui/dr-ui/ui/app.slint:2325`](../ui/dr-ui/ui/app.slint#L2325), [`ui/dr-ui/ui/settings.slint:681`](../ui/dr-ui/ui/settings.slint#L681), [`ui/dr-ui/ui/toolrail.slint:89`](../ui/dr-ui/ui/toolrail.slint#L89) | -| FR-UI-8 | [`core/dr-catalog/src/collections.rs:654`](../core/dr-catalog/src/collections.rs#L654), [`core/dr-catalog/src/collections.rs:675`](../core/dr-catalog/src/collections.rs#L675), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`ui/dr-ui/src/derived_sync.rs:216`](../ui/dr-ui/src/derived_sync.rs#L216), [`ui/dr-ui/src/derived_sync.rs:81`](../ui/dr-ui/src/derived_sync.rs#L81), [`ui/dr-ui/src/derived_sync.rs:883`](../ui/dr-ui/src/derived_sync.rs#L883), [`ui/dr-ui/src/derived_sync.rs:893`](../ui/dr-ui/src/derived_sync.rs#L893), [`ui/dr-ui/src/derived_sync.rs:975`](../ui/dr-ui/src/derived_sync.rs#L975), [`ui/dr-ui/src/library.rs:1187`](../ui/dr-ui/src/library.rs#L1187), [`ui/dr-ui/src/library.rs:4708`](../ui/dr-ui/src/library.rs#L4708), [`ui/dr-ui/src/library.rs:5476`](../ui/dr-ui/src/library.rs#L5476), [`ui/dr-ui/src/library.rs:5542`](../ui/dr-ui/src/library.rs#L5542), [`ui/dr-ui/src/library_ui.rs:1007`](../ui/dr-ui/src/library_ui.rs#L1007), [`ui/dr-ui/src/library_ui.rs:195`](../ui/dr-ui/src/library_ui.rs#L195), [`ui/dr-ui/src/library_ui.rs:2192`](../ui/dr-ui/src/library_ui.rs#L2192), [`ui/dr-ui/src/library_ui.rs:2381`](../ui/dr-ui/src/library_ui.rs#L2381), [`ui/dr-ui/src/library_ui.rs:2398`](../ui/dr-ui/src/library_ui.rs#L2398), [`ui/dr-ui/src/library_ui.rs:2496`](../ui/dr-ui/src/library_ui.rs#L2496), [`ui/dr-ui/src/library_ui.rs:5832`](../ui/dr-ui/src/library_ui.rs#L5832), [`ui/dr-ui/src/library_ui.rs:5872`](../ui/dr-ui/src/library_ui.rs#L5872), [`ui/dr-ui/src/library_ui.rs:5901`](../ui/dr-ui/src/library_ui.rs#L5901), [`ui/dr-ui/src/library_ui.rs:6164`](../ui/dr-ui/src/library_ui.rs#L6164), [`ui/dr-ui/src/library_ui.rs:6387`](../ui/dr-ui/src/library_ui.rs#L6387), [`ui/dr-ui/src/library_ui.rs:7141`](../ui/dr-ui/src/library_ui.rs#L7141), [`ui/dr-ui/src/library_ui.rs:935`](../ui/dr-ui/src/library_ui.rs#L935), [`ui/dr-ui/src/library_ui.rs:968`](../ui/dr-ui/src/library_ui.rs#L968), [`ui/dr-ui/src/place.rs:1`](../ui/dr-ui/src/place.rs#L1) | +| FR-UI-8 | [`core/dr-catalog/src/collections.rs:654`](../core/dr-catalog/src/collections.rs#L654), [`core/dr-catalog/src/collections.rs:675`](../core/dr-catalog/src/collections.rs#L675), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`ui/dr-ui/src/derived_sync.rs:216`](../ui/dr-ui/src/derived_sync.rs#L216), [`ui/dr-ui/src/derived_sync.rs:81`](../ui/dr-ui/src/derived_sync.rs#L81), [`ui/dr-ui/src/derived_sync.rs:894`](../ui/dr-ui/src/derived_sync.rs#L894), [`ui/dr-ui/src/derived_sync.rs:904`](../ui/dr-ui/src/derived_sync.rs#L904), [`ui/dr-ui/src/derived_sync.rs:986`](../ui/dr-ui/src/derived_sync.rs#L986), [`ui/dr-ui/src/library.rs:1187`](../ui/dr-ui/src/library.rs#L1187), [`ui/dr-ui/src/library.rs:4708`](../ui/dr-ui/src/library.rs#L4708), [`ui/dr-ui/src/library.rs:5476`](../ui/dr-ui/src/library.rs#L5476), [`ui/dr-ui/src/library.rs:5542`](../ui/dr-ui/src/library.rs#L5542), [`ui/dr-ui/src/library_ui.rs:1007`](../ui/dr-ui/src/library_ui.rs#L1007), [`ui/dr-ui/src/library_ui.rs:195`](../ui/dr-ui/src/library_ui.rs#L195), [`ui/dr-ui/src/library_ui.rs:2192`](../ui/dr-ui/src/library_ui.rs#L2192), [`ui/dr-ui/src/library_ui.rs:2381`](../ui/dr-ui/src/library_ui.rs#L2381), [`ui/dr-ui/src/library_ui.rs:2398`](../ui/dr-ui/src/library_ui.rs#L2398), [`ui/dr-ui/src/library_ui.rs:2496`](../ui/dr-ui/src/library_ui.rs#L2496), [`ui/dr-ui/src/library_ui.rs:5832`](../ui/dr-ui/src/library_ui.rs#L5832), [`ui/dr-ui/src/library_ui.rs:5872`](../ui/dr-ui/src/library_ui.rs#L5872), [`ui/dr-ui/src/library_ui.rs:5901`](../ui/dr-ui/src/library_ui.rs#L5901), [`ui/dr-ui/src/library_ui.rs:6164`](../ui/dr-ui/src/library_ui.rs#L6164), [`ui/dr-ui/src/library_ui.rs:6387`](../ui/dr-ui/src/library_ui.rs#L6387), [`ui/dr-ui/src/library_ui.rs:7141`](../ui/dr-ui/src/library_ui.rs#L7141), [`ui/dr-ui/src/library_ui.rs:935`](../ui/dr-ui/src/library_ui.rs#L935), [`ui/dr-ui/src/library_ui.rs:968`](../ui/dr-ui/src/library_ui.rs#L968), [`ui/dr-ui/src/place.rs:1`](../ui/dr-ui/src/place.rs#L1) | | NFR-A11Y-2 | [`ui/dr-ui/tests/ui_controls_are_accessible.rs:1`](../ui/dr-ui/tests/ui_controls_are_accessible.rs#L1), [`ui/dr-ui/ui/adjust.slint:171`](../ui/dr-ui/ui/adjust.slint#L171), [`ui/dr-ui/ui/app.slint:2742`](../ui/dr-ui/ui/app.slint#L2742), [`ui/dr-ui/ui/app.slint:2780`](../ui/dr-ui/ui/app.slint#L2780) | | NFR-A11Y-3 | [`ui/dr-ui/src/histogram.rs:356`](../ui/dr-ui/src/histogram.rs#L356), [`ui/dr-ui/ui/histogram.slint:137`](../ui/dr-ui/ui/histogram.slint#L137), [`ui/dr-ui/ui/library.slint:761`](../ui/dr-ui/ui/library.slint#L761), [`ui/dr-ui/ui/library.slint:909`](../ui/dr-ui/ui/library.slint#L909), [`ui/dr-ui/ui/peaking.slint:1`](../ui/dr-ui/ui/peaking.slint#L1) | | NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/repairs.rs:1`](../ui/dr-ui/src/repairs.rs#L1) | @@ -169,7 +169,7 @@ A tag naming an ID `requirements.md` does not define — what renumbering produc | NFR-PORT-2 | [`core/dr-gpu/src/lib.rs:1`](../core/dr-gpu/src/lib.rs#L1) | | NFR-PORT-3 | [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1) | | NFR-R1 | [`core/dr-catalog/src/schema.rs:265`](../core/dr-catalog/src/schema.rs#L265), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-gpu/src/lib.rs:194`](../core/dr-gpu/src/lib.rs#L194), [`core/dr-sync/src/account.rs:240`](../core/dr-sync/src/account.rs#L240), [`ui/dr-ui/src/library.rs:1234`](../ui/dr-ui/src/library.rs#L1234) | -| NFR-R2 | [`core/dr-catalog/src/recovery.rs:1`](../core/dr-catalog/src/recovery.rs#L1), [`core/dr-catalog/src/recovery.rs:222`](../core/dr-catalog/src/recovery.rs#L222), [`core/dr-catalog/src/sync.rs:61`](../core/dr-catalog/src/sync.rs#L61), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`ui/dr-ui/src/derived_sync.rs:602`](../ui/dr-ui/src/derived_sync.rs#L602), [`ui/dr-ui/src/derived_sync.rs:725`](../ui/dr-ui/src/derived_sync.rs#L725), [`ui/dr-ui/src/library_ui.rs:4195`](../ui/dr-ui/src/library_ui.rs#L4195), [`ui/dr-ui/src/library_ui.rs:4513`](../ui/dr-ui/src/library_ui.rs#L4513) | +| NFR-R2 | [`core/dr-catalog/src/recovery.rs:1`](../core/dr-catalog/src/recovery.rs#L1), [`core/dr-catalog/src/recovery.rs:222`](../core/dr-catalog/src/recovery.rs#L222), [`core/dr-catalog/src/sync.rs:61`](../core/dr-catalog/src/sync.rs#L61), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`ui/dr-ui/src/derived_sync.rs:613`](../ui/dr-ui/src/derived_sync.rs#L613), [`ui/dr-ui/src/derived_sync.rs:736`](../ui/dr-ui/src/derived_sync.rs#L736), [`ui/dr-ui/src/library_ui.rs:4195`](../ui/dr-ui/src/library_ui.rs#L4195), [`ui/dr-ui/src/library_ui.rs:4513`](../ui/dr-ui/src/library_ui.rs#L4513) | | NFR-R3 | [`core/dr-decode/src/error.rs:30`](../core/dr-decode/src/error.rs#L30) | | NFR-R4 | [`core/dr-types/src/settings.rs:92`](../core/dr-types/src/settings.rs#L92), [`ui/dr-ui/src/library.rs:1627`](../ui/dr-ui/src/library.rs#L1627), [`ui/dr-ui/src/library.rs:1643`](../ui/dr-ui/src/library.rs#L1643), [`ui/dr-ui/src/library_ui.rs:3448`](../ui/dr-ui/src/library_ui.rs#L3448), [`ui/dr-ui/src/library_ui.rs:392`](../ui/dr-ui/src/library_ui.rs#L392), [`ui/dr-ui/src/library_ui.rs:537`](../ui/dr-ui/src/library_ui.rs#L537), [`ui/dr-ui/src/settings_ui.rs:540`](../ui/dr-ui/src/settings_ui.rs#L540), [`ui/dr-ui/src/xmp_sync.rs:1`](../ui/dr-ui/src/xmp_sync.rs#L1), [`ui/dr-ui/ui/settings.slint:1093`](../ui/dr-ui/ui/settings.slint#L1093), [`ui/dr-ui/ui/settings.slint:199`](../ui/dr-ui/ui/settings.slint#L199) | | NFR-R5 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1) | diff --git a/ui/dr-ui/src/derived_sync.rs b/ui/dr-ui/src/derived_sync.rs index 04032b8..078769c 100644 --- a/ui/dr-ui/src/derived_sync.rs +++ b/ui/dr-ui/src/derived_sync.rs @@ -286,9 +286,20 @@ async fn sync_shards( // ---- upload ---------------------------------------------------------- for shard in &local { - let path = store.shard_path(shard.id); - let Ok(bytes) = std::fs::read(&path) else { - continue; + // A snapshot, never the live file — see `ThumbStore::snapshot_shard` + // for the zero-byte upload that reading the file produced. + let snapshot = scratch.join(format!("thumb-shard-{:04}-upload.sqlite", shard.id)); + let bytes = match store.snapshot_shard(shard.id, &snapshot) { + Ok(()) => std::fs::read(&snapshot), + Err(e) => Err(std::io::Error::other(e.to_string())), + }; + let _ = std::fs::remove_file(&snapshot); + let bytes = match bytes { + Ok(b) => b, + Err(e) => { + log::warn!("snapshotting thumbnail shard {}: {e}", shard.id); + continue; + } }; let name = shard_name(&client, shard.id);