Give every chunked upload its own directory on the server

The upload directory was named from the destination path alone, on the
reasoning that two files could then not collide. Two devices uploading
the same file could, and did: both wrote 00001…00009 into one directory,
and whichever MOVEd first assembled a mix of the two — a catalog of
exactly the right size whose pages came from two different databases.
SQLite called it malformed, every client declined to overwrite it, and
collections stopped syncing on all of them for a week. A transfer that
died on a phone's link left its chunks there for the next device to
assemble in, by the same mechanism.

The name now carries a nonce as well, so no two uploads share a
directory, and a failed transfer deletes its own directory on the way
out rather than leaving 5 MB chunks for the server to sweep eventually.
This commit is contained in:
2026-09-13 19:31:58 +02:00
parent 693195fa96
commit 6f62ac09f8
2 changed files with 122 additions and 14 deletions
+118 -10
View File
@@ -121,16 +121,24 @@ impl NextcloudBackend {
body: Vec<u8>,
) -> Result<Validator, RemoteError> {
let total = body.len() as u64;
// Named from the destination so a resumed or abandoned upload is
// identifiable, and so two uploads cannot collide in one directory.
let token: String = path
.as_str()
.bytes()
.map(|b| match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' => (b as char).to_string(),
_ => "-".to_string(),
})
.collect();
// TRACES: FR-NC-9
// Named from the destination, so an abandoned upload is identifiable,
// **and from a nonce, so no two uploads ever share a directory.**
//
// The name used to be the destination alone, on the reasoning that two
// *files* could then not collide. Two *devices* uploading the same file
// could, and did: both wrote `00001`…`00009` into one directory, and
// whichever `MOVE`d first assembled a mix of the two — a catalog of
// exactly the right size whose pages came from two different
// databases. SQLite called it malformed, every client then declined to
// overwrite it, and collections stopped syncing on all of them for a
// week. An upload that died on a phone's link left its chunks there
// for the next device to assemble in, by the same mechanism.
let token = format!(
"{}-{}",
sanitise_for_upload_dir(path.as_str()),
upload_nonce()
);
let dir = format!(
"{}/remote.php/dav/uploads/{}/{token}",
self.server, self.login
@@ -138,6 +146,27 @@ impl NextcloudBackend {
self.mkcol_url(&dir).await?;
// Whatever happens below, the directory does not outlive the attempt.
// With a unique name a leftover is only quota rather than corruption,
// but a phone that abandons uploads all day would still leave dozens
// of 5 MB chunks behind, and the server only sweeps them eventually.
let result = self.put_chunks_and_assemble(path, &dir, body, total).await;
if result.is_err() {
self.discard_upload_dir(&dir).await;
}
result
}
/// The transfer half of [`put_chunked`](Self::put_chunked): the chunks,
/// then the `MOVE` that assembles them. Split out so that a failure at any
/// point returns to one place that cleans up.
async fn put_chunks_and_assemble(
&self,
path: &RemotePath,
dir: &str,
body: Vec<u8>,
total: u64,
) -> Result<Validator, RemoteError> {
// Chunks are numbered from 1 and must sort correctly as strings, which
// is why they are zero-padded rather than bare integers.
let chunk_size = CHUNKS.min_chunk as usize;
@@ -191,6 +220,27 @@ impl NextcloudBackend {
self.dir_validator(path).await
}
/// Best-effort `DELETE` of an upload directory whose transfer failed.
///
/// Errors are logged and dropped: this runs on the way out of a failure,
/// and the failure is what the caller needs to hear about. A connection
/// that has just died will refuse this too, and that is fine — the
/// server sweeps abandoned upload directories on its own; this only
/// spares it the wait when the link is still up.
async fn discard_upload_dir(&self, dir: &str) {
let attempt = self
.client
.delete(dir)
.basic_auth(&self.login, Some(&self.password))
.send()
.await;
match attempt {
Ok(resp) if resp.status().is_success() || resp.status() == 404 => {}
Ok(resp) => log::debug!("leaving abandoned upload {dir}: {}", resp.status()),
Err(e) => log::debug!("leaving abandoned upload {dir}: {e}"),
}
}
/// `MKCOL` at an absolute URL, treating "already there" as success.
async fn mkcol_url(&self, url: &str) -> Result<(), RemoteError> {
let resp = self
@@ -725,6 +775,37 @@ fn map_send_error(e: reqwest::Error) -> RemoteError {
}
}
/// The destination path reduced to characters every WebDAV server accepts in
/// an upload directory name.
fn sanitise_for_upload_dir(path: &str) -> String {
path.bytes()
.map(|b| match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' => (b as char).to_string(),
_ => "-".to_string(),
})
.collect()
}
/// A token no other upload — on this device or any other — will produce.
///
/// Nanoseconds since the epoch, the process id, and a counter, mixed rather
/// than concatenated so the name stays short. Two devices would have to start
/// an upload in the same nanosecond from the same pid to collide, and the
/// counter separates two uploads this process starts in one tick. No
/// randomness crate is pulled in for this: unique is the requirement, not
/// unguessable.
fn upload_nonce() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let pid = u64::from(std::process::id());
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("{:016x}", nanos ^ (pid << 40) ^ n.rotate_left(20))
}
/// Translate an HTTP status into a typed error.
fn map_status(status: reqwest::StatusCode, what: &str) -> Result<(), RemoteError> {
match status.as_u16() {
@@ -765,6 +846,33 @@ fn encode_path(path: &str) -> String {
#[cfg(test)]
mod tests {
#[test]
fn two_uploads_of_one_destination_never_share_a_directory() {
// The collision that assembled two devices' chunks into one file.
// Same path, back to back, same process: still two names.
let a = format!(
"{}-{}",
super::sanitise_for_upload_dir("PhotosRaw/.darkroom-derived/catalog.sqlite"),
super::upload_nonce()
);
let b = format!(
"{}-{}",
super::sanitise_for_upload_dir("PhotosRaw/.darkroom-derived/catalog.sqlite"),
super::upload_nonce()
);
assert_ne!(a, b);
assert!(a.starts_with("PhotosRaw--darkroom-derived-catalog-sqlite-"));
}
#[test]
fn upload_directory_names_are_plain() {
assert_eq!(
super::sanitise_for_upload_dir("Photos Raw/été/x.sqlite"),
"Photos-Raw---t---x-sqlite"
);
assert!(super::upload_nonce().bytes().all(|b| b.is_ascii_hexdigit()));
}
use super::*;
fn backend() -> NextcloudBackend {
+4 -4
View File
@@ -10,7 +10,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
| Metric | Value |
|---|---|
| Source files scanned | 354 |
| TRACES tags found | 1471 |
| TRACES tags found | 1472 |
| Requirements defined | 191 |
| Requirements covered | 140 |
| **Coverage** | **73.3%** (140/191) |
@@ -38,7 +38,7 @@ _None._
| FR-CAT-11 | [`core/dr-catalog/src/dedup.rs:1`](../core/dr-catalog/src/dedup.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:392`](../core/dr-ingest/src/lib.rs#L392), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1508`](../ui/dr-ui/src/lib.rs#L1508), [`ui/dr-ui/src/library.rs:183`](../ui/dr-ui/src/library.rs#L183), [`ui/dr-ui/src/library.rs:3047`](../ui/dr-ui/src/library.rs#L3047), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
| FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:122`](../core/dr-pipeline/src/sidecar.rs#L122) |
| FR-CAT-13 | [`core/dr-catalog/src/rating.rs:278`](../core/dr-catalog/src/rating.rs#L278), [`core/dr-catalog/src/schema.rs:653`](../core/dr-catalog/src/schema.rs#L653), [`core/dr-sync/src/scan.rs:40`](../core/dr-sync/src/scan.rs#L40), [`core/dr-sync/src/scan.rs:944`](../core/dr-sync/src/scan.rs#L944), [`core/dr-types/src/settings.rs:92`](../core/dr-types/src/settings.rs#L92), [`core/dr-xmp/src/lib.rs:1`](../core/dr-xmp/src/lib.rs#L1), [`core/dr-xmp/src/lib.rs:331`](../core/dr-xmp/src/lib.rs#L331), [`core/dr-xmp/src/lib.rs:442`](../core/dr-xmp/src/lib.rs#L442), [`core/dr-xmp/src/lib.rs:535`](../core/dr-xmp/src/lib.rs#L535), [`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), [`core/dr-xmp/src/read.rs:1`](../core/dr-xmp/src/read.rs#L1), [`core/dr-xmp/src/write.rs:1`](../core/dr-xmp/src/write.rs#L1), [`core/dr-xmp/tests/round_trip.rs:1`](../core/dr-xmp/tests/round_trip.rs#L1), [`ui/dr-ui/src/library.rs:1500`](../ui/dr-ui/src/library.rs#L1500), [`ui/dr-ui/src/library.rs:1563`](../ui/dr-ui/src/library.rs#L1563), [`ui/dr-ui/src/library.rs:1579`](../ui/dr-ui/src/library.rs#L1579), [`ui/dr-ui/src/library.rs:1686`](../ui/dr-ui/src/library.rs#L1686), [`ui/dr-ui/src/library_ui.rs:1285`](../ui/dr-ui/src/library_ui.rs#L1285), [`ui/dr-ui/src/library_ui.rs:301`](../ui/dr-ui/src/library_ui.rs#L301), [`ui/dr-ui/src/library_ui.rs:3411`](../ui/dr-ui/src/library_ui.rs#L3411), [`ui/dr-ui/src/library_ui.rs:3467`](../ui/dr-ui/src/library_ui.rs#L3467), [`ui/dr-ui/src/library_ui.rs:3565`](../ui/dr-ui/src/library_ui.rs#L3565), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:519`](../ui/dr-ui/src/library_ui.rs#L519), [`ui/dr-ui/src/library_ui.rs:5713`](../ui/dr-ui/src/library_ui.rs#L5713), [`ui/dr-ui/src/settings_ui.rs:506`](../ui/dr-ui/src/settings_ui.rs#L506), [`ui/dr-ui/src/xmp_sync.rs:1`](../ui/dr-ui/src/xmp_sync.rs#L1), [`ui/dr-ui/src/xmp_sync.rs:250`](../ui/dr-ui/src/xmp_sync.rs#L250), [`ui/dr-ui/src/xmp_sync.rs:287`](../ui/dr-ui/src/xmp_sync.rs#L287), [`ui/dr-ui/src/xmp_sync.rs:324`](../ui/dr-ui/src/xmp_sync.rs#L324), [`ui/dr-ui/ui/app.slint:849`](../ui/dr-ui/ui/app.slint#L849), [`ui/dr-ui/ui/settings.slint:1001`](../ui/dr-ui/ui/settings.slint#L1001), [`ui/dr-ui/ui/settings.slint:184`](../ui/dr-ui/ui/settings.slint#L184) |
| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:923`](../core/dr-catalog/src/schema.rs#L923), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:457`](../core/dr-sync-nextcloud/src/lib.rs#L457), [`core/dr-sync/src/lib.rs:135`](../core/dr-sync/src/lib.rs#L135), [`core/dr-sync/src/scan.rs:618`](../core/dr-sync/src/scan.rs#L618), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`core/dr-types/src/place.rs:91`](../core/dr-types/src/place.rs#L91), [`ui/dr-ui/src/collections_ui.rs:1435`](../ui/dr-ui/src/collections_ui.rs#L1435), [`ui/dr-ui/src/collections_ui.rs:2341`](../ui/dr-ui/src/collections_ui.rs#L2341), [`ui/dr-ui/src/library.rs:183`](../ui/dr-ui/src/library.rs#L183), [`ui/dr-ui/src/library.rs:200`](../ui/dr-ui/src/library.rs#L200), [`ui/dr-ui/src/library.rs:249`](../ui/dr-ui/src/library.rs#L249), [`ui/dr-ui/src/library.rs:4829`](../ui/dr-ui/src/library.rs#L4829), [`ui/dr-ui/src/library.rs:4863`](../ui/dr-ui/src/library.rs#L4863), [`ui/dr-ui/src/library_ui.rs:238`](../ui/dr-ui/src/library_ui.rs#L238), [`ui/dr-ui/src/library_ui.rs:870`](../ui/dr-ui/src/library_ui.rs#L870), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:745`](../ui/dr-ui/ui/collections.slint#L745) |
| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:923`](../core/dr-catalog/src/schema.rs#L923), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:507`](../core/dr-sync-nextcloud/src/lib.rs#L507), [`core/dr-sync/src/lib.rs:135`](../core/dr-sync/src/lib.rs#L135), [`core/dr-sync/src/scan.rs:618`](../core/dr-sync/src/scan.rs#L618), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`core/dr-types/src/place.rs:91`](../core/dr-types/src/place.rs#L91), [`ui/dr-ui/src/collections_ui.rs:1435`](../ui/dr-ui/src/collections_ui.rs#L1435), [`ui/dr-ui/src/collections_ui.rs:2341`](../ui/dr-ui/src/collections_ui.rs#L2341), [`ui/dr-ui/src/library.rs:183`](../ui/dr-ui/src/library.rs#L183), [`ui/dr-ui/src/library.rs:200`](../ui/dr-ui/src/library.rs#L200), [`ui/dr-ui/src/library.rs:249`](../ui/dr-ui/src/library.rs#L249), [`ui/dr-ui/src/library.rs:4829`](../ui/dr-ui/src/library.rs#L4829), [`ui/dr-ui/src/library.rs:4863`](../ui/dr-ui/src/library.rs#L4863), [`ui/dr-ui/src/library_ui.rs:238`](../ui/dr-ui/src/library_ui.rs#L238), [`ui/dr-ui/src/library_ui.rs:870`](../ui/dr-ui/src/library_ui.rs#L870), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:745`](../ui/dr-ui/ui/collections.slint#L745) |
| FR-CAT-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:56`](../core/dr-types/src/lib.rs#L56), [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.rs#L1), [`platform/dr-plat/src/storage.rs:216`](../platform/dr-plat/src/storage.rs#L216), [`platform/dr-plat/src/storage.rs:46`](../platform/dr-plat/src/storage.rs#L46) |
| FR-CAT-2 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1) |
| FR-CAT-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/walk.rs:66`](../core/dr-catalog/src/walk.rs#L66), [`core/dr-sync/src/scan.rs:105`](../core/dr-sync/src/scan.rs#L105), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/import.rs:463`](../ui/dr-ui/src/import.rs#L463), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488), [`ui/dr-ui/src/library.rs:3520`](../ui/dr-ui/src/library.rs#L3520), [`ui/dr-ui/src/library.rs:4278`](../ui/dr-ui/src/library.rs#L4278), [`ui/dr-ui/src/library_ui.rs:177`](../ui/dr-ui/src/library_ui.rs#L177), [`ui/dr-ui/src/library_ui.rs:4463`](../ui/dr-ui/src/library_ui.rs#L4463), [`ui/dr-ui/src/library_ui.rs:5974`](../ui/dr-ui/src/library_ui.rs#L5974), [`ui/dr-ui/ui/app.slint:383`](../ui/dr-ui/ui/app.slint#L383), [`ui/dr-ui/ui/settings.slint:420`](../ui/dr-ui/ui/settings.slint#L420), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) |
@@ -97,7 +97,7 @@ _None._
| FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1106`](../core/dr-gpu/src/adjust.rs#L1106), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/develop.rs:4672`](../ui/dr-ui/src/develop.rs#L4672), [`ui/dr-ui/src/develop.rs:4710`](../ui/dr-ui/src/develop.rs#L4710), [`ui/dr-ui/src/develop.rs:6878`](../ui/dr-ui/src/develop.rs#L6878), [`ui/dr-ui/src/lib.rs:601`](../ui/dr-ui/src/lib.rs#L601), [`ui/dr-ui/src/library.rs:3679`](../ui/dr-ui/src/library.rs#L3679), [`ui/dr-ui/src/library.rs:4092`](../ui/dr-ui/src/library.rs#L4092), [`ui/dr-ui/src/library.rs:4128`](../ui/dr-ui/src/library.rs#L4128), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) |
| FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:361`](../core/dr-sync/src/account.rs#L361), [`ui/dr-ui/src/launch.rs:277`](../ui/dr-ui/src/launch.rs#L277), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:417`](../ui/dr-ui/src/launch_ui.rs#L417) |
| FR-NC-10 | [`core/dr-sync/src/account.rs:226`](../core/dr-sync/src/account.rs#L226), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:674`](../ui/dr-ui/src/lib.rs#L674), [`ui/dr-ui/src/library.rs:1170`](../ui/dr-ui/src/library.rs#L1170), [`ui/dr-ui/src/library.rs:2483`](../ui/dr-ui/src/library.rs#L2483), [`ui/dr-ui/src/library.rs:589`](../ui/dr-ui/src/library.rs#L589), [`ui/dr-ui/src/library.rs:916`](../ui/dr-ui/src/library.rs#L916), [`ui/dr-ui/src/library_ui.rs:2020`](../ui/dr-ui/src/library_ui.rs#L2020), [`ui/dr-ui/src/library_ui.rs:4205`](../ui/dr-ui/src/library_ui.rs#L4205), [`ui/dr-ui/src/library_ui.rs:618`](../ui/dr-ui/src/library_ui.rs#L618), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
| FR-NC-12 | [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:40`](../core/dr-sync-nextcloud/src/lib.rs#L40), [`core/dr-sync-nextcloud/src/lib.rs:914`](../core/dr-sync-nextcloud/src/lib.rs#L914), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:87`](../core/dr-sync/src/account.rs#L87), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/lib.rs:51`](../core/dr-sync/src/lib.rs#L51), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`core/dr-sync/src/provider.rs:1`](../core/dr-sync/src/provider.rs#L1), [`core/dr-sync/src/provider.rs:53`](../core/dr-sync/src/provider.rs#L53), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) |
| FR-NC-12 | [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:1022`](../core/dr-sync-nextcloud/src/lib.rs#L1022), [`core/dr-sync-nextcloud/src/lib.rs:40`](../core/dr-sync-nextcloud/src/lib.rs#L40), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:87`](../core/dr-sync/src/account.rs#L87), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/lib.rs:51`](../core/dr-sync/src/lib.rs#L51), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`core/dr-sync/src/provider.rs:1`](../core/dr-sync/src/provider.rs#L1), [`core/dr-sync/src/provider.rs:53`](../core/dr-sync/src/provider.rs#L53), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) |
| FR-NC-13 | [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`ui/dr-ui/src/launch_ui.rs:316`](../ui/dr-ui/src/launch_ui.rs#L316), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) |
| FR-NC-2 | [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:304`](../core/dr-sync/src/account.rs#L304), [`core/dr-sync/src/account.rs:361`](../core/dr-sync/src/account.rs#L361), [`core/dr-sync/src/account.rs:59`](../core/dr-sync/src/account.rs#L59), [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) |
| FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`core/dr-sync/src/capability.rs:85`](../core/dr-sync/src/capability.rs#L85), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:3520`](../ui/dr-ui/src/library.rs#L3520), [`ui/dr-ui/src/library.rs:4278`](../ui/dr-ui/src/library.rs#L4278), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:4463`](../ui/dr-ui/src/library_ui.rs#L4463), [`ui/dr-ui/src/library_ui.rs:5974`](../ui/dr-ui/src/library_ui.rs#L5974), [`ui/dr-ui/ui/app.slint:383`](../ui/dr-ui/ui/app.slint#L383), [`ui/dr-ui/ui/settings.slint:420`](../ui/dr-ui/ui/settings.slint#L420), [`ui/dr-ui/ui/settings.slint:73`](../ui/dr-ui/ui/settings.slint#L73) |
@@ -112,7 +112,7 @@ _None._
| 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:309`](../core/dr-types/src/settings.rs#L309), [`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:1508`](../ui/dr-ui/src/lib.rs#L1508), [`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:1508`](../ui/dr-ui/src/lib.rs#L1508) |
| 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:179`](../core/dr-catalog/src/schema.rs#L179), [`core/dr-pipeline/src/sidecar.rs:122`](../core/dr-pipeline/src/sidecar.rs#L122), [`core/dr-pipeline/src/sidecar.rs:2944`](../core/dr-pipeline/src/sidecar.rs#L2944), [`core/dr-pipeline/src/sidecar.rs:645`](../core/dr-pipeline/src/sidecar.rs#L645), [`core/dr-pipeline/src/sidecar.rs:720`](../core/dr-pipeline/src/sidecar.rs#L720), [`core/dr-pipeline/src/sidecar.rs:96`](../core/dr-pipeline/src/sidecar.rs#L96), [`ui/dr-ui/src/lib.rs:2374`](../ui/dr-ui/src/lib.rs#L2374), [`ui/dr-ui/src/library.rs:1062`](../ui/dr-ui/src/library.rs#L1062), [`ui/dr-ui/src/library.rs:2538`](../ui/dr-ui/src/library.rs#L2538), [`ui/dr-ui/src/library.rs:500`](../ui/dr-ui/src/library.rs#L500), [`ui/dr-ui/src/library.rs:760`](../ui/dr-ui/src/library.rs#L760), [`ui/dr-ui/src/library.rs:7751`](../ui/dr-ui/src/library.rs#L7751), [`ui/dr-ui/src/library_ui.rs:586`](../ui/dr-ui/src/library_ui.rs#L586), [`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:179`](../core/dr-catalog/src/schema.rs#L179), [`core/dr-catalog/src/schema.rs:557`](../core/dr-catalog/src/schema.rs#L557), [`core/dr-catalog/src/schema.rs:838`](../core/dr-catalog/src/schema.rs#L838), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:177`](../core/dr-pipeline/src/sidecar.rs#L177), [`core/dr-pipeline/src/sidecar.rs:204`](../core/dr-pipeline/src/sidecar.rs#L204), [`core/dr-pipeline/src/sidecar.rs:2473`](../core/dr-pipeline/src/sidecar.rs#L2473), [`core/dr-pipeline/src/sidecar.rs:2710`](../core/dr-pipeline/src/sidecar.rs#L2710), [`core/dr-pipeline/src/sidecar.rs:2944`](../core/dr-pipeline/src/sidecar.rs#L2944), [`core/dr-pipeline/src/sidecar.rs:380`](../core/dr-pipeline/src/sidecar.rs#L380), [`core/dr-pipeline/src/sidecar.rs:478`](../core/dr-pipeline/src/sidecar.rs#L478), [`core/dr-pipeline/src/sidecar.rs:645`](../core/dr-pipeline/src/sidecar.rs#L645), [`core/dr-pipeline/src/sidecar.rs:695`](../core/dr-pipeline/src/sidecar.rs#L695), [`core/dr-pipeline/src/sidecar.rs:720`](../core/dr-pipeline/src/sidecar.rs#L720), [`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/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:614`](../ui/dr-ui/src/derived_sync.rs#L614), [`ui/dr-ui/src/derived_sync.rs:647`](../ui/dr-ui/src/derived_sync.rs#L647), [`ui/dr-ui/src/derived_sync.rs:878`](../ui/dr-ui/src/derived_sync.rs#L878), [`ui/dr-ui/src/library.rs:1062`](../ui/dr-ui/src/library.rs#L1062), [`ui/dr-ui/src/library.rs:1082`](../ui/dr-ui/src/library.rs#L1082), [`ui/dr-ui/src/library.rs:1391`](../ui/dr-ui/src/library.rs#L1391), [`ui/dr-ui/src/library.rs:1426`](../ui/dr-ui/src/library.rs#L1426), [`ui/dr-ui/src/library.rs:2538`](../ui/dr-ui/src/library.rs#L2538), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:7583`](../ui/dr-ui/src/library.rs#L7583), [`ui/dr-ui/src/library.rs:760`](../ui/dr-ui/src/library.rs#L760), [`ui/dr-ui/src/library.rs:7751`](../ui/dr-ui/src/library.rs#L7751), [`ui/dr-ui/src/library.rs:832`](../ui/dr-ui/src/library.rs#L832), [`ui/dr-ui/src/library.rs:916`](../ui/dr-ui/src/library.rs#L916), [`ui/dr-ui/src/library_ui.rs:1302`](../ui/dr-ui/src/library_ui.rs#L1302), [`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:179`](../core/dr-catalog/src/schema.rs#L179), [`core/dr-catalog/src/schema.rs:557`](../core/dr-catalog/src/schema.rs#L557), [`core/dr-catalog/src/schema.rs:838`](../core/dr-catalog/src/schema.rs#L838), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:177`](../core/dr-pipeline/src/sidecar.rs#L177), [`core/dr-pipeline/src/sidecar.rs:204`](../core/dr-pipeline/src/sidecar.rs#L204), [`core/dr-pipeline/src/sidecar.rs:2473`](../core/dr-pipeline/src/sidecar.rs#L2473), [`core/dr-pipeline/src/sidecar.rs:2710`](../core/dr-pipeline/src/sidecar.rs#L2710), [`core/dr-pipeline/src/sidecar.rs:2944`](../core/dr-pipeline/src/sidecar.rs#L2944), [`core/dr-pipeline/src/sidecar.rs:380`](../core/dr-pipeline/src/sidecar.rs#L380), [`core/dr-pipeline/src/sidecar.rs:478`](../core/dr-pipeline/src/sidecar.rs#L478), [`core/dr-pipeline/src/sidecar.rs:645`](../core/dr-pipeline/src/sidecar.rs#L645), [`core/dr-pipeline/src/sidecar.rs:695`](../core/dr-pipeline/src/sidecar.rs#L695), [`core/dr-pipeline/src/sidecar.rs:720`](../core/dr-pipeline/src/sidecar.rs#L720), [`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:614`](../ui/dr-ui/src/derived_sync.rs#L614), [`ui/dr-ui/src/derived_sync.rs:647`](../ui/dr-ui/src/derived_sync.rs#L647), [`ui/dr-ui/src/derived_sync.rs:878`](../ui/dr-ui/src/derived_sync.rs#L878), [`ui/dr-ui/src/library.rs:1062`](../ui/dr-ui/src/library.rs#L1062), [`ui/dr-ui/src/library.rs:1082`](../ui/dr-ui/src/library.rs#L1082), [`ui/dr-ui/src/library.rs:1391`](../ui/dr-ui/src/library.rs#L1391), [`ui/dr-ui/src/library.rs:1426`](../ui/dr-ui/src/library.rs#L1426), [`ui/dr-ui/src/library.rs:2538`](../ui/dr-ui/src/library.rs#L2538), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:7583`](../ui/dr-ui/src/library.rs#L7583), [`ui/dr-ui/src/library.rs:760`](../ui/dr-ui/src/library.rs#L760), [`ui/dr-ui/src/library.rs:7751`](../ui/dr-ui/src/library.rs#L7751), [`ui/dr-ui/src/library.rs:832`](../ui/dr-ui/src/library.rs#L832), [`ui/dr-ui/src/library.rs:916`](../ui/dr-ui/src/library.rs#L916), [`ui/dr-ui/src/library_ui.rs:1302`](../ui/dr-ui/src/library_ui.rs#L1302), [`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:1291`](../ui/dr-ui/src/library.rs#L1291), [`ui/dr-ui/src/library.rs:1344`](../ui/dr-ui/src/library.rs#L1344), [`ui/dr-ui/src/library.rs:1375`](../ui/dr-ui/src/library.rs#L1375), [`ui/dr-ui/src/library.rs:1955`](../ui/dr-ui/src/library.rs#L1955), [`ui/dr-ui/src/library_ui.rs:1273`](../ui/dr-ui/src/library_ui.rs#L1273), [`ui/dr-ui/src/library_ui.rs:1369`](../ui/dr-ui/src/library_ui.rs#L1369), [`ui/dr-ui/src/library_ui.rs:1968`](../ui/dr-ui/src/library_ui.rs#L1968), [`ui/dr-ui/src/library_ui.rs:328`](../ui/dr-ui/src/library_ui.rs#L328), [`ui/dr-ui/src/library_ui.rs:555`](../ui/dr-ui/src/library_ui.rs#L555) |
| 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) |