Treat a placeholder as the photograph, not as a one-byte file
The folder connector was pointed at a Nextcloud VFS tree and got three things wrong, the first of which loses work. **A dehydrated sidecar read as absent.** `a.drsc` does not exist when the client has dehydrated it — only `a.drsc.nextcloud` does — so `get` missed, `.ok()` swallowed the `NotFound`, and the sidecar writer took that for "there is no sidecar yet" and wrote a fresh document over the existing one. Every edit another device had put there went with it. That function's own doc comment calls this the exact loss the format's unknown-key preservation exists to prevent. **A stub was catalogued as a 1-byte image**, and ARCH §9.0 measured this machine at 121,785 placeholders against 10,267 real files — so a folder library on a synced tree was ~92% broken rows. **Identity changed on hydration**, so downloading a photograph looked like a delete and an add, orphaning its thumbnail and its face rows. Entries now carry the photograph's own name and a `materialised` flag; `get` on a stub returns the new `RemoteError::NotMaterialised`, which is distinct from `NotFound` precisely because the sidecar writer must treat them differently — it fetches the sidecar and merges, or leaves the entry queued. Hydration is a **borrow**. `BorrowPool` records what was on disk before it asked, so `release_all` dehydrates only what a pass brought and leaves what the user already had. Reference counted: the thumbnail pass and the face pass meet on the same RAW, and without counting the first to finish dehydrates the file the second is reading. A borrow against a plain folder or a server does nothing, so a pass written for VFS runs everywhere. Releasing means asking the client to dehydrate and never deleting: a deletion inside a synced tree propagates to the server and removes the photograph from every device. Not a second backend — the capability is per *connection*, not per type, since the same folder hydrates only while the client runs. The convention arrives through a detector the registry supplies, so `dr-sync-folder` still knows nothing about any client's protocol. ARCH §9.0a records this as an amendment: finding 3 rejected hydration because it costs 100× a range read, and that comparison assumed a connector was available. A folder library has none.
This commit is contained in:
Generated
+1
@@ -1580,6 +1580,7 @@ dependencies = [
|
|||||||
"dr-decode",
|
"dr-decode",
|
||||||
"dr-plat",
|
"dr-plat",
|
||||||
"dr-sync",
|
"dr-sync",
|
||||||
|
"dr-sync-folder",
|
||||||
"dr-types",
|
"dr-types",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
// TRACES: FR-NC-6c | FR-NC-6a
|
||||||
|
//! Hydrating a file for as long as it is needed, and no longer.
|
||||||
|
//!
|
||||||
|
//! A pass over a library — thumbnails, face indexing — needs each photograph's
|
||||||
|
//! bytes for a moment and never again. On a virtual-filesystem folder those
|
||||||
|
//! bytes may not be here, and fetching them is whole-file: hydrating a 17,000
|
||||||
|
//! image library to index it would land the entire library on a disk the user
|
||||||
|
//! deliberately keeps most of it off (ARCH §9.0).
|
||||||
|
//!
|
||||||
|
//! So hydration is a **borrow**. Ask for a file, use it, give it back. Peak
|
||||||
|
//! disk becomes the working set rather than the library, and the transfer is
|
||||||
|
//! paid once for a thumbnail that is then kept for ever — and pushed to the
|
||||||
|
//! server for other devices, which never pay it at all.
|
||||||
|
//!
|
||||||
|
//! # The rule that makes it safe
|
||||||
|
//!
|
||||||
|
//! **A file is returned to the state it was found in.** If it was already
|
||||||
|
//! downloaded — the user pinned it, opened it yesterday, or never uses VFS —
|
||||||
|
//! the borrow leaves it downloaded. Only what this pass hydrated is released.
|
||||||
|
//! Anything else silently undoes a choice the user made, and "my pinned trip
|
||||||
|
//! evaporated after an indexing run" is the kind of failure that makes people
|
||||||
|
//! stop trusting the feature.
|
||||||
|
//!
|
||||||
|
//! # Why it is reference counted
|
||||||
|
//!
|
||||||
|
//! Lanes run concurrently and two of them meet on the same file: the
|
||||||
|
//! thumbnail pass and the face pass want the same RAW. Without counting, the
|
||||||
|
//! first to finish dehydrates the file the second is reading. With it, the
|
||||||
|
//! transfer is paid once and the release happens when the last borrower is
|
||||||
|
//! done.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use dr_sync::{RemoteBackend, RemoteError, RemoteId, RemotePath};
|
||||||
|
|
||||||
|
/// What a borrow is holding, per path.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct Held {
|
||||||
|
/// How many borrowers are using it now.
|
||||||
|
borrowers: usize,
|
||||||
|
/// Whether *we* brought it here. False means it was already downloaded
|
||||||
|
/// and must be left that way.
|
||||||
|
ours: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tracks what has been hydrated and by whom.
|
||||||
|
///
|
||||||
|
/// Cheap to clone — every worker holds one and they share the same state.
|
||||||
|
#[derive(Clone, Default, Debug)]
|
||||||
|
pub struct BorrowPool {
|
||||||
|
held: Arc<Mutex<HashMap<RemotePath, Held>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What a completed borrow did, for reporting a pass's real cost.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
pub struct BorrowStats {
|
||||||
|
/// Files that were already here. These cost nothing.
|
||||||
|
pub already_local: usize,
|
||||||
|
/// Files this pass downloaded.
|
||||||
|
pub hydrated: usize,
|
||||||
|
/// Files released again afterwards.
|
||||||
|
pub released: usize,
|
||||||
|
/// Files left downloaded because they were already so.
|
||||||
|
pub kept: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorrowPool {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Borrow a file's content for the life of the returned guard.
|
||||||
|
///
|
||||||
|
/// Downloads it if it is a placeholder; does nothing if it is already
|
||||||
|
/// here. The guard releases it on drop, but only if this pool hydrated it
|
||||||
|
/// and nothing else still holds it.
|
||||||
|
///
|
||||||
|
/// A backend without [`Materialisation::OnDemand`] short-circuits: the
|
||||||
|
/// borrow succeeds and does nothing, so a caller written for a VFS library
|
||||||
|
/// runs unchanged against a server or a plain folder.
|
||||||
|
///
|
||||||
|
/// [`Materialisation::OnDemand`]: dr_sync::Materialisation::OnDemand
|
||||||
|
pub async fn borrow<'p>(
|
||||||
|
&'p self,
|
||||||
|
backend: &dyn RemoteBackend,
|
||||||
|
path: &RemotePath,
|
||||||
|
) -> Result<Borrowed<'p>, RemoteError> {
|
||||||
|
self.borrow_known(backend, path, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`borrow`](Self::borrow) where the caller already knows whether the
|
||||||
|
/// content is here.
|
||||||
|
///
|
||||||
|
/// A sweep reads availability out of the catalog on its way to building a
|
||||||
|
/// work list, so making the pool re-derive it costs a directory listing
|
||||||
|
/// per file for an answer already in hand.
|
||||||
|
///
|
||||||
|
/// **`None` means "find out", and being wrong is not symmetric.** Claiming
|
||||||
|
/// a file was already local leaves it downloaded — disk, and nothing else.
|
||||||
|
/// Claiming it was not releases a file the user may have pinned. So an
|
||||||
|
/// uncertain caller passes `Some(true)` or `None`, never a guess at
|
||||||
|
/// `false`.
|
||||||
|
pub async fn borrow_known<'p>(
|
||||||
|
&'p self,
|
||||||
|
backend: &dyn RemoteBackend,
|
||||||
|
path: &RemotePath,
|
||||||
|
already_local: Option<bool>,
|
||||||
|
) -> Result<Borrowed<'p>, RemoteError> {
|
||||||
|
if !backend.capabilities().materialisation.can_materialise() {
|
||||||
|
return Ok(Borrowed {
|
||||||
|
pool: None,
|
||||||
|
path: path.clone(),
|
||||||
|
hydrated: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Another borrower already has it: join them rather than asking the
|
||||||
|
// client a second time.
|
||||||
|
{
|
||||||
|
let mut held = self.lock();
|
||||||
|
if let Some(entry) = held.get_mut(path) {
|
||||||
|
entry.borrowers += 1;
|
||||||
|
return Ok(Borrowed {
|
||||||
|
pool: Some(self),
|
||||||
|
path: path.clone(),
|
||||||
|
hydrated: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = RemoteId::Path(path.clone());
|
||||||
|
|
||||||
|
// What was here *before* we asked. The whole contract rests on this
|
||||||
|
// being read first: after `materialise` there is no way to tell what
|
||||||
|
// we brought from what was already there.
|
||||||
|
let was_local = match already_local {
|
||||||
|
Some(known) => known,
|
||||||
|
None => is_materialised(backend, path).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !was_local {
|
||||||
|
backend.materialise(&id).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.lock().insert(
|
||||||
|
path.clone(),
|
||||||
|
Held {
|
||||||
|
borrowers: 1,
|
||||||
|
ours: !was_local,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Borrowed {
|
||||||
|
pool: Some(self),
|
||||||
|
path: path.clone(),
|
||||||
|
hydrated: !was_local,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release everything this pool still holds that it hydrated.
|
||||||
|
///
|
||||||
|
/// The end-of-pass sweep. A guard dropped on a panicking worker cannot run
|
||||||
|
/// its async release, so the pool is drained deliberately at the end
|
||||||
|
/// rather than trusted to unwind cleanly.
|
||||||
|
pub async fn release_all(&self, backend: &dyn RemoteBackend) -> BorrowStats {
|
||||||
|
let ours: Vec<RemotePath> = {
|
||||||
|
let held = self.lock();
|
||||||
|
held.iter()
|
||||||
|
.filter(|(_, h)| h.ours)
|
||||||
|
.map(|(p, _)| p.clone())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut stats = BorrowStats::default();
|
||||||
|
for path in ours {
|
||||||
|
match backend.dematerialise(&RemoteId::Path(path.clone())).await {
|
||||||
|
Ok(()) => stats.released += 1,
|
||||||
|
// Not fatal, and not worth failing a completed pass over: the
|
||||||
|
// content stays, which costs disk and loses nothing.
|
||||||
|
Err(e) => log::debug!("releasing {path}: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.lock().clear();
|
||||||
|
stats
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How many paths are currently held.
|
||||||
|
pub fn held(&self) -> usize {
|
||||||
|
self.lock().len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<RemotePath, Held>> {
|
||||||
|
// A poisoned lock means a worker panicked while holding it. The map is
|
||||||
|
// bookkeeping, not a resource — carrying on with it is better than
|
||||||
|
// taking the whole pass down.
|
||||||
|
self.held.lock().unwrap_or_else(|e| e.into_inner())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A file held local for as long as this lives.
|
||||||
|
///
|
||||||
|
/// Dropping it marks the borrow finished. The actual release happens in
|
||||||
|
/// [`BorrowPool::release_all`], because dropping cannot await.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Borrowed<'p> {
|
||||||
|
pool: Option<&'p BorrowPool>,
|
||||||
|
path: RemotePath,
|
||||||
|
/// Whether this borrow was the one that downloaded it.
|
||||||
|
hydrated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Borrowed<'_> {
|
||||||
|
/// Whether this borrow paid for a download.
|
||||||
|
pub fn hydrated(&self) -> bool {
|
||||||
|
self.hydrated
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &RemotePath {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Borrowed<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let Some(pool) = self.pool else { return };
|
||||||
|
let mut held = pool.lock();
|
||||||
|
if let Some(entry) = held.get_mut(&self.path) {
|
||||||
|
entry.borrowers = entry.borrowers.saturating_sub(1);
|
||||||
|
// Left in the map even at zero borrowers: `release_all` needs to
|
||||||
|
// know it was ours, and a file wanted again a moment later should
|
||||||
|
// not be downloaded twice.
|
||||||
|
if entry.borrowers == 0 && !entry.ours {
|
||||||
|
held.remove(&self.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the backend currently holds this file's content.
|
||||||
|
///
|
||||||
|
/// Asked by listing its parent, because the trait has no "stat one object" —
|
||||||
|
/// and adding one for this would put a method on every backend to serve a case
|
||||||
|
/// only one of them has.
|
||||||
|
async fn is_materialised(backend: &dyn RemoteBackend, path: &RemotePath) -> bool {
|
||||||
|
// `parent()` is `None` for a file directly under the library root, whose
|
||||||
|
// parent *is* the root — not "no parent". Treating the two alike reported
|
||||||
|
// every top-level photograph as already downloaded, so nothing was ever
|
||||||
|
// hydrated and nothing was ever released.
|
||||||
|
let parent = path.parent().unwrap_or_else(RemotePath::root);
|
||||||
|
match backend.list(&parent, None).await {
|
||||||
|
Ok(entries) => entries
|
||||||
|
.iter()
|
||||||
|
.find(|e| &e.path == path)
|
||||||
|
.map(|e| e.materialised)
|
||||||
|
// Not listed at all: nothing to hydrate, and the caller's own read
|
||||||
|
// will report the miss properly.
|
||||||
|
.unwrap_or(true),
|
||||||
|
Err(_) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
//! The borrow contract, against a filesystem and a fake client.
|
||||||
|
//!
|
||||||
|
//! The fake stands in for the sync client's socket, not for the filesystem:
|
||||||
|
//! it renames stubs exactly as suffix-mode VFS does, so everything under test
|
||||||
|
//! is the real path resolution and the real state tracking.
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::{FolderBackend, Vfs};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
/// A stand-in for a sync client, counting what it was asked to do.
|
||||||
|
struct FakeClient {
|
||||||
|
suffix: &'static str,
|
||||||
|
hydrations: AtomicUsize,
|
||||||
|
dehydrations: AtomicUsize,
|
||||||
|
/// When true, refuse to hydrate — the client is running but the server is
|
||||||
|
/// not reachable.
|
||||||
|
broken: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FakeClient {
|
||||||
|
fn new() -> Arc<Self> {
|
||||||
|
Arc::new(Self {
|
||||||
|
suffix: ".nextcloud",
|
||||||
|
hydrations: AtomicUsize::new(0),
|
||||||
|
dehydrations: AtomicUsize::new(0),
|
||||||
|
broken: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn broken() -> Arc<Self> {
|
||||||
|
Arc::new(Self {
|
||||||
|
suffix: ".nextcloud",
|
||||||
|
hydrations: AtomicUsize::new(0),
|
||||||
|
dehydrations: AtomicUsize::new(0),
|
||||||
|
broken: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vfs for FakeClient {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"fake"
|
||||||
|
}
|
||||||
|
fn is_placeholder(&self, on_disk: &str) -> bool {
|
||||||
|
on_disk.ends_with(self.suffix)
|
||||||
|
}
|
||||||
|
fn real_name<'a>(&self, on_disk: &'a str) -> &'a str {
|
||||||
|
on_disk.strip_suffix(self.suffix).unwrap_or(on_disk)
|
||||||
|
}
|
||||||
|
fn placeholder_name(&self, name: &str) -> std::borrow::Cow<'_, str> {
|
||||||
|
std::borrow::Cow::Owned(format!("{name}{}", self.suffix))
|
||||||
|
}
|
||||||
|
fn can_materialise(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
fn materialise(&self, local: &Path) -> Result<(), RemoteError> {
|
||||||
|
self.hydrations.fetch_add(1, Ordering::SeqCst);
|
||||||
|
if self.broken {
|
||||||
|
return Err(RemoteError::Network("no server".into()));
|
||||||
|
}
|
||||||
|
// Suffix mode renames rather than filling in place, and writes the
|
||||||
|
// real content.
|
||||||
|
let real = PathBuf::from(local.to_string_lossy().strip_suffix(self.suffix).unwrap());
|
||||||
|
std::fs::write(&real, vec![9u8; 4096]).unwrap();
|
||||||
|
std::fs::remove_file(local).unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn dematerialise(&self, local: &Path) -> Result<(), RemoteError> {
|
||||||
|
self.dehydrations.fetch_add(1, Ordering::SeqCst);
|
||||||
|
let stub = format!("{}{}", local.display(), self.suffix);
|
||||||
|
std::fs::write(&stub, [0u8]).unwrap();
|
||||||
|
std::fs::remove_file(local).unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Tmp(PathBuf);
|
||||||
|
|
||||||
|
impl Tmp {
|
||||||
|
fn new(name: &str) -> Self {
|
||||||
|
let d = std::env::temp_dir().join(format!("dr-borrow-{name}"));
|
||||||
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
|
std::fs::create_dir_all(&d).unwrap();
|
||||||
|
Tmp(d)
|
||||||
|
}
|
||||||
|
/// A dehydrated photograph.
|
||||||
|
fn stub(&self, rel: &str) -> &Self {
|
||||||
|
std::fs::write(self.0.join(format!("{rel}.nextcloud")), [0u8]).unwrap();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
/// One the user already has.
|
||||||
|
fn real(&self, rel: &str) -> &Self {
|
||||||
|
std::fs::write(self.0.join(rel), vec![1u8; 2048]).unwrap();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
fn has(&self, rel: &str) -> bool {
|
||||||
|
self.0.join(rel).is_file()
|
||||||
|
}
|
||||||
|
fn backend(&self, vfs: Arc<dyn Vfs>) -> FolderBackend {
|
||||||
|
FolderBackend::with_vfs(&self.0, vfs).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Tmp {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = std::fs::remove_dir_all(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_borrowed_placeholder_is_downloaded_and_given_back() {
|
||||||
|
let t = Tmp::new("cycle");
|
||||||
|
t.stub("a.CR2");
|
||||||
|
let client = FakeClient::new();
|
||||||
|
let b = t.backend(client.clone());
|
||||||
|
let pool = BorrowPool::new();
|
||||||
|
let path = RemotePath::new("a.CR2");
|
||||||
|
|
||||||
|
{
|
||||||
|
let held = pool.borrow(&b, &path).await.unwrap();
|
||||||
|
assert!(held.hydrated(), "this borrow paid for it");
|
||||||
|
assert!(t.has("a.CR2"), "content is here while borrowed");
|
||||||
|
assert_eq!(
|
||||||
|
b.get(&RemoteId::Path(path.clone()), None)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.len(),
|
||||||
|
4096
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stats = pool.release_all(&b).await;
|
||||||
|
assert_eq!(stats.released, 1);
|
||||||
|
assert!(!t.has("a.CR2"), "given back");
|
||||||
|
assert!(t.has("a.CR2.nextcloud"), "a placeholder is left behind");
|
||||||
|
assert_eq!(client.dehydrations.load(Ordering::SeqCst), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_file_the_user_already_had_is_never_taken_away() {
|
||||||
|
// The rule the whole design rests on. Silently undoing a pin — or just a
|
||||||
|
// file someone opened yesterday — after an indexing run is the failure
|
||||||
|
// that would make people stop trusting this.
|
||||||
|
let t = Tmp::new("keep");
|
||||||
|
t.real("pinned.CR2");
|
||||||
|
let client = FakeClient::new();
|
||||||
|
let b = t.backend(client.clone());
|
||||||
|
let pool = BorrowPool::new();
|
||||||
|
|
||||||
|
{
|
||||||
|
let held = pool
|
||||||
|
.borrow(&b, &RemotePath::new("pinned.CR2"))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(!held.hydrated(), "nothing was downloaded");
|
||||||
|
}
|
||||||
|
let stats = pool.release_all(&b).await;
|
||||||
|
|
||||||
|
assert_eq!(stats.released, 0);
|
||||||
|
assert!(t.has("pinned.CR2"), "still here");
|
||||||
|
assert_eq!(client.hydrations.load(Ordering::SeqCst), 0);
|
||||||
|
assert_eq!(client.dehydrations.load(Ordering::SeqCst), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn two_lanes_wanting_one_file_download_it_once() {
|
||||||
|
// The thumbnail pass and the face pass meet on the same RAW. Without
|
||||||
|
// counting, the first to finish dehydrates the file the second is reading.
|
||||||
|
let t = Tmp::new("shared");
|
||||||
|
t.stub("a.CR2");
|
||||||
|
let client = FakeClient::new();
|
||||||
|
let b = t.backend(client.clone());
|
||||||
|
let pool = BorrowPool::new();
|
||||||
|
let path = RemotePath::new("a.CR2");
|
||||||
|
|
||||||
|
let first = pool.borrow(&b, &path).await.unwrap();
|
||||||
|
let second = pool.borrow(&b, &path).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(client.hydrations.load(Ordering::SeqCst), 1, "paid once");
|
||||||
|
drop(first);
|
||||||
|
assert!(t.has("a.CR2"), "still held by the second borrower");
|
||||||
|
drop(second);
|
||||||
|
|
||||||
|
pool.release_all(&b).await;
|
||||||
|
assert!(!t.has("a.CR2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_failed_download_does_not_leave_a_phantom_borrow() {
|
||||||
|
// The client is up but the server is not. The pass must see the failure
|
||||||
|
// and the pool must not believe it holds anything.
|
||||||
|
let t = Tmp::new("failed");
|
||||||
|
t.stub("a.CR2");
|
||||||
|
let b = t.backend(FakeClient::broken());
|
||||||
|
let pool = BorrowPool::new();
|
||||||
|
|
||||||
|
let e = pool
|
||||||
|
.borrow(&b, &RemotePath::new("a.CR2"))
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(e, RemoteError::Network(_)), "{e:?}");
|
||||||
|
assert_eq!(pool.held(), 0);
|
||||||
|
assert!(t.has("a.CR2.nextcloud"), "left as it was found");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn borrowing_against_a_plain_folder_does_nothing_at_all() {
|
||||||
|
// A caller written for a VFS library must run unchanged elsewhere, or
|
||||||
|
// every sweep grows two code paths.
|
||||||
|
let t = Tmp::new("plain");
|
||||||
|
t.real("a.CR2");
|
||||||
|
let b = FolderBackend::new(&t.0).unwrap();
|
||||||
|
let pool = BorrowPool::new();
|
||||||
|
|
||||||
|
let held = pool.borrow(&b, &RemotePath::new("a.CR2")).await.unwrap();
|
||||||
|
assert!(!held.hydrated());
|
||||||
|
drop(held);
|
||||||
|
assert_eq!(pool.release_all(&b).await.released, 0);
|
||||||
|
assert!(t.has("a.CR2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn an_uncertain_caller_keeps_the_file_rather_than_releasing_it() {
|
||||||
|
// The asymmetry stated on `borrow_known`: claiming "already local" costs
|
||||||
|
// disk, claiming "not local" can release a pin.
|
||||||
|
let t = Tmp::new("uncertain");
|
||||||
|
t.real("a.CR2");
|
||||||
|
let client = FakeClient::new();
|
||||||
|
let b = t.backend(client.clone());
|
||||||
|
let pool = BorrowPool::new();
|
||||||
|
|
||||||
|
{
|
||||||
|
let _held = pool
|
||||||
|
.borrow_known(&b, &RemotePath::new("a.CR2"), Some(true))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
pool.release_all(&b).await;
|
||||||
|
assert!(t.has("a.CR2"), "kept");
|
||||||
|
assert_eq!(client.dehydrations.load(Ordering::SeqCst), 0);
|
||||||
|
}
|
||||||
+238
-14
@@ -50,23 +50,76 @@ use std::io::{Read, Seek, SeekFrom, Write};
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use dr_sync::{
|
use dr_sync::{
|
||||||
Account, BackendProvider, Capabilities, ChangeDetection, Connection, Cursor, EntryKind,
|
Account, BackendProvider, Capabilities, ChangeDetection, Connection, Cursor, EntryKind,
|
||||||
Precondition, RemoteBackend, RemoteChange, RemoteEntry, RemoteError, RemoteId, RemotePath,
|
Materialisation, Precondition, RemoteBackend, RemoteChange, RemoteEntry, RemoteError, RemoteId,
|
||||||
ServerPreviews, SignIn, Validator,
|
RemotePath, ServerPreviews, SignIn, Validator,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub mod borrow;
|
||||||
|
pub mod vfs;
|
||||||
|
|
||||||
|
pub use borrow::{BorrowPool, BorrowStats, Borrowed};
|
||||||
|
pub use vfs::{NoVfs, Vfs};
|
||||||
|
|
||||||
/// The id written to [`Account::backend`] for a folder library.
|
/// The id written to [`Account::backend`] for a folder library.
|
||||||
///
|
///
|
||||||
/// On-disk configuration: changing it orphans every folder account.
|
/// On-disk configuration: changing it orphans every folder account.
|
||||||
pub const BACKEND_ID: &str = "folder";
|
pub const BACKEND_ID: &str = "folder";
|
||||||
|
|
||||||
/// TRACES: FR-NC-13
|
/// TRACES: FR-NC-13 | FR-NC-6c
|
||||||
/// Registers the folder connector.
|
/// Registers the folder connector.
|
||||||
///
|
///
|
||||||
/// See [`dr_sync::provider`] for what each method is for.
|
/// See [`dr_sync::provider`] for what each method is for.
|
||||||
pub struct FolderProvider;
|
///
|
||||||
|
/// # The detector
|
||||||
|
///
|
||||||
|
/// This crate knows how to read a directory and nothing about sync clients,
|
||||||
|
/// so the placeholder convention arrives from outside: whoever registers the
|
||||||
|
/// provider supplies a function that recognises a synced folder and returns
|
||||||
|
/// the [`Vfs`] for it. That keeps `dr-sync-folder` free of any client's
|
||||||
|
/// protocol, and it is what lets one connector serve a plain disk, a Nextcloud
|
||||||
|
/// tree, and whatever comes next.
|
||||||
|
///
|
||||||
|
/// Detection runs per connection because the answer changes: the same
|
||||||
|
/// directory offers hydration while the client is up and not while it is down.
|
||||||
|
/// Recognises a placeholder convention in a directory, if any applies.
|
||||||
|
///
|
||||||
|
/// Runs per connection rather than once, because the answer changes: the same
|
||||||
|
/// folder offers hydration while the sync client is up and not while it is
|
||||||
|
/// down.
|
||||||
|
pub type VfsDetector = dyn Fn(&Path) -> Option<Arc<dyn Vfs>> + Send + Sync;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct FolderProvider {
|
||||||
|
detect_vfs: Option<Box<VfsDetector>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FolderProvider {
|
||||||
|
/// A folder connector that treats every directory as ordinary.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A folder connector that recognises placeholder conventions.
|
||||||
|
pub fn with_vfs_detector(
|
||||||
|
detect: impl Fn(&Path) -> Option<Arc<dyn Vfs>> + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
detect_vfs: Some(Box::new(detect)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vfs_for(&self, root: &Path) -> Arc<dyn Vfs> {
|
||||||
|
self.detect_vfs
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|d| d(root))
|
||||||
|
.unwrap_or_else(|| Arc::new(NoVfs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BackendProvider for FolderProvider {
|
impl BackendProvider for FolderProvider {
|
||||||
fn id(&self) -> &'static str {
|
fn id(&self) -> &'static str {
|
||||||
@@ -136,18 +189,30 @@ impl BackendProvider for FolderProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn connect(&self, conn: &Connection) -> Result<Box<dyn RemoteBackend>, RemoteError> {
|
fn connect(&self, conn: &Connection) -> Result<Box<dyn RemoteBackend>, RemoteError> {
|
||||||
Ok(Box::new(FolderBackend::new(&conn.account.endpoint)?))
|
let root = Path::new(&conn.account.endpoint);
|
||||||
|
Ok(Box::new(FolderBackend::with_vfs(root, self.vfs_for(root))?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TRACES: FR-NC-13 | FR-NC-4
|
/// TRACES: FR-NC-13 | FR-NC-4 | FR-NC-6c
|
||||||
/// A library rooted at a directory.
|
/// A library rooted at a directory.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FolderBackend {
|
pub struct FolderBackend {
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
|
/// The placeholder convention in force, [`NoVfs`] for an ordinary folder.
|
||||||
|
vfs: Arc<dyn Vfs>,
|
||||||
caps: Capabilities,
|
caps: Capabilities,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for FolderBackend {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("FolderBackend")
|
||||||
|
.field("root", &self.root)
|
||||||
|
.field("vfs", &self.vfs.name())
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FolderBackend {
|
impl FolderBackend {
|
||||||
/// Open the folder at `root`.
|
/// Open the folder at `root`.
|
||||||
///
|
///
|
||||||
@@ -156,6 +221,15 @@ impl FolderBackend {
|
|||||||
/// [`RemoteError::Network`], which is what puts the app into offline mode
|
/// [`RemoteError::Network`], which is what puts the app into offline mode
|
||||||
/// and leaves the catalog readable, exactly as a dead server does.
|
/// and leaves the catalog readable, exactly as a dead server does.
|
||||||
pub fn new(root: impl Into<PathBuf>) -> Result<Self, RemoteError> {
|
pub fn new(root: impl Into<PathBuf>) -> Result<Self, RemoteError> {
|
||||||
|
Self::with_vfs(root, Arc::new(NoVfs))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open the folder at `root` under a placeholder convention.
|
||||||
|
///
|
||||||
|
/// The convention is chosen by the caller rather than sniffed here: the
|
||||||
|
/// connector that knows how to talk to a given sync client is the one that
|
||||||
|
/// knows whether it is running (see `dr_sync_nextcloud`).
|
||||||
|
pub fn with_vfs(root: impl Into<PathBuf>, vfs: Arc<dyn Vfs>) -> Result<Self, RemoteError> {
|
||||||
let root = root.into();
|
let root = root.into();
|
||||||
if !root.is_dir() {
|
if !root.is_dir() {
|
||||||
return Err(RemoteError::Configuration(format!(
|
return Err(RemoteError::Configuration(format!(
|
||||||
@@ -163,8 +237,19 @@ impl FolderBackend {
|
|||||||
root.display()
|
root.display()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
// Reported per connection, not per backend: the same folder offers
|
||||||
|
// hydration while the client is up and not while it is down, so this
|
||||||
|
// cannot be a constant of the type (see `vfs`).
|
||||||
|
let materialisation = if vfs.can_materialise() {
|
||||||
|
Materialisation::OnDemand
|
||||||
|
} else if vfs.name() == NoVfs.name() {
|
||||||
|
Materialisation::Always
|
||||||
|
} else {
|
||||||
|
Materialisation::Placeholders
|
||||||
|
};
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
root,
|
root,
|
||||||
|
vfs,
|
||||||
caps: Capabilities {
|
caps: Capabilities {
|
||||||
// A directory's mtime describes its own entry list and nothing
|
// A directory's mtime describes its own entry list and nothing
|
||||||
// below it, so there is no propagation to exploit; the engine
|
// below it, so there is no propagation to exploit; the engine
|
||||||
@@ -179,6 +264,7 @@ impl FolderBackend {
|
|||||||
bulk_upload: false,
|
bulk_upload: false,
|
||||||
conditional_write: true,
|
conditional_write: true,
|
||||||
server_previews: ServerPreviews::None,
|
server_previews: ServerPreviews::None,
|
||||||
|
materialisation,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -210,6 +296,30 @@ impl FolderBackend {
|
|||||||
Ok(self.root.join(rel))
|
Ok(self.root.join(rel))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where a photograph's bytes are on disk, and whether they are really
|
||||||
|
/// there.
|
||||||
|
///
|
||||||
|
/// A placeholder lives under a *different* name — suffix-mode VFS renames
|
||||||
|
/// on hydration rather than filling in place — so every read and write has
|
||||||
|
/// to look for both. The materialised name is tried first: it is the
|
||||||
|
/// common case, and the second `stat` is paid only when it misses.
|
||||||
|
///
|
||||||
|
/// Returns the path to use and whether it holds real content.
|
||||||
|
fn locate(&self, path: &RemotePath) -> Result<(PathBuf, bool), RemoteError> {
|
||||||
|
let direct = self.resolve(path)?;
|
||||||
|
if self.vfs.name() == NoVfs.name() || direct.exists() {
|
||||||
|
return Ok((direct, true));
|
||||||
|
}
|
||||||
|
let stub = self.resolve(&RemotePath::new(
|
||||||
|
self.vfs.placeholder_name(path.as_str()).into_owned(),
|
||||||
|
))?;
|
||||||
|
if stub.exists() {
|
||||||
|
return Ok((stub, false));
|
||||||
|
}
|
||||||
|
// Neither: genuinely missing. Report the name the caller asked for.
|
||||||
|
Ok((direct, true))
|
||||||
|
}
|
||||||
|
|
||||||
/// The local path a [`RemoteId`] names.
|
/// The local path a [`RemoteId`] names.
|
||||||
///
|
///
|
||||||
/// A stable id here is a hash and nothing can be resolved from it, exactly
|
/// A stable id here is a hash and nothing can be resolved from it, exactly
|
||||||
@@ -223,6 +333,16 @@ impl FolderBackend {
|
|||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`locate`](Self::locate) for an id.
|
||||||
|
fn locate_id(&self, id: &RemoteId) -> Result<(PathBuf, bool), RemoteError> {
|
||||||
|
match id {
|
||||||
|
RemoteId::Path(p) => self.locate(p),
|
||||||
|
RemoteId::Stable(_) => Err(RemoteError::Unsupported(
|
||||||
|
"a folder cannot be addressed by id; use RemoteId::Path",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run a filesystem operation off the async worker that asked for it.
|
/// Run a filesystem operation off the async worker that asked for it.
|
||||||
@@ -274,6 +394,16 @@ fn map_io(e: std::io::Error, what: &str) -> RemoteError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How long to wait for a requested download to land.
|
||||||
|
///
|
||||||
|
/// Generous, because the file may be tens of megabytes over a domestic
|
||||||
|
/// connection, and bounded, because a client that has stopped transferring
|
||||||
|
/// must not wedge a whole pass.
|
||||||
|
const MATERIALISE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
|
||||||
|
|
||||||
|
/// How often to look for the materialised file while waiting.
|
||||||
|
const POLL: std::time::Duration = std::time::Duration::from_millis(200);
|
||||||
|
|
||||||
/// The identity of a file, from its path relative to the library root.
|
/// The identity of a file, from its path relative to the library root.
|
||||||
///
|
///
|
||||||
/// FNV-1a rather than `DefaultHasher`, whose output is explicitly unstable
|
/// FNV-1a rather than `DefaultHasher`, whose output is explicitly unstable
|
||||||
@@ -330,6 +460,7 @@ impl RemoteBackend for FolderBackend {
|
|||||||
) -> Result<Vec<RemoteEntry>, RemoteError> {
|
) -> Result<Vec<RemoteEntry>, RemoteError> {
|
||||||
let local = self.resolve(dir)?;
|
let local = self.resolve(dir)?;
|
||||||
let dir = dir.clone();
|
let dir = dir.clone();
|
||||||
|
let vfs = self.vfs.clone();
|
||||||
blocking(move || {
|
blocking(move || {
|
||||||
let read =
|
let read =
|
||||||
std::fs::read_dir(&local).map_err(|e| map_io(e, &local.display().to_string()))?;
|
std::fs::read_dir(&local).map_err(|e| map_io(e, &local.display().to_string()))?;
|
||||||
@@ -368,7 +499,13 @@ impl RemoteBackend for FolderBackend {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let path = dir.join(name);
|
// The photograph's own name, never the stub's. Identity is
|
||||||
|
// derived from it, so downloading a file must not look like a
|
||||||
|
// delete and an add — and `source_ref` must match what every
|
||||||
|
// other device calls the same photograph.
|
||||||
|
let stub = vfs.is_placeholder(name);
|
||||||
|
let path = dir.join(vfs.real_name(name));
|
||||||
|
|
||||||
out.push(RemoteEntry {
|
out.push(RemoteEntry {
|
||||||
id: RemoteId::Stable(identity(&path)),
|
id: RemoteId::Stable(identity(&path)),
|
||||||
kind: if meta.is_dir() {
|
kind: if meta.is_dir() {
|
||||||
@@ -377,11 +514,15 @@ impl RemoteBackend for FolderBackend {
|
|||||||
EntryKind::File
|
EntryKind::File
|
||||||
},
|
},
|
||||||
validator: validator_of(&meta),
|
validator: validator_of(&meta),
|
||||||
size: meta.len(),
|
// A stub is one byte and says nothing about what it stands
|
||||||
|
// for. Reporting that byte count would put a 1-byte
|
||||||
|
// `file_size` in the catalog for most of the library.
|
||||||
|
size: if stub { 0 } else { meta.len() },
|
||||||
modified: modified_secs(&meta),
|
modified: modified_secs(&meta),
|
||||||
// No renderer behind a folder; previews are extracted
|
// No renderer behind a folder; previews are extracted
|
||||||
// locally from the file itself.
|
// locally from the file itself.
|
||||||
has_preview: false,
|
has_preview: false,
|
||||||
|
materialised: !stub,
|
||||||
path,
|
path,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -408,7 +549,14 @@ impl RemoteBackend for FolderBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get(&self, id: &RemoteId, range: Option<Range<u64>>) -> Result<Vec<u8>, RemoteError> {
|
async fn get(&self, id: &RemoteId, range: Option<Range<u64>>) -> Result<Vec<u8>, RemoteError> {
|
||||||
let local = self.resolve_id(id)?;
|
let (local, materialised) = self.locate_id(id)?;
|
||||||
|
if !materialised {
|
||||||
|
// The one byte in the stub is not the file. Returning it produced
|
||||||
|
// a sidecar that parsed as empty and a thumbnail that never
|
||||||
|
// decoded; reporting `NotFound` made the sidecar writer treat an
|
||||||
|
// existing document as absent and overwrite it.
|
||||||
|
return Err(RemoteError::NotMaterialised(local.display().to_string()));
|
||||||
|
}
|
||||||
blocking(move || {
|
blocking(move || {
|
||||||
let what = local.display().to_string();
|
let what = local.display().to_string();
|
||||||
let mut file = std::fs::File::open(&local).map_err(|e| map_io(e, &what))?;
|
let mut file = std::fs::File::open(&local).map_err(|e| map_io(e, &what))?;
|
||||||
@@ -440,7 +588,15 @@ impl RemoteBackend for FolderBackend {
|
|||||||
body: Vec<u8>,
|
body: Vec<u8>,
|
||||||
precond: Option<Precondition>,
|
precond: Option<Precondition>,
|
||||||
) -> Result<Validator, RemoteError> {
|
) -> Result<Validator, RemoteError> {
|
||||||
let local = self.resolve(path)?;
|
let (local, materialised) = self.locate(path)?;
|
||||||
|
if !materialised {
|
||||||
|
// Writing `a.drsc` while `a.drsc.nextcloud` sits beside it creates
|
||||||
|
// two files for one document and hands the sync client a conflict
|
||||||
|
// to resolve — in favour of whichever it sees last. The caller
|
||||||
|
// must materialise it and merge, which is what the typed error is
|
||||||
|
// for.
|
||||||
|
return Err(RemoteError::NotMaterialised(local.display().to_string()));
|
||||||
|
}
|
||||||
blocking(move || {
|
blocking(move || {
|
||||||
let what = local.display().to_string();
|
let what = local.display().to_string();
|
||||||
if let Some(parent) = local.parent() {
|
if let Some(parent) = local.parent() {
|
||||||
@@ -522,7 +678,10 @@ impl RemoteBackend for FolderBackend {
|
|||||||
id: &RemoteId,
|
id: &RemoteId,
|
||||||
precond: Option<Precondition>,
|
precond: Option<Precondition>,
|
||||||
) -> Result<(), RemoteError> {
|
) -> Result<(), RemoteError> {
|
||||||
let local = self.resolve_id(id)?;
|
// Deliberately by whichever name is on disk: deleting a photograph
|
||||||
|
// means deleting it whether or not its content happens to be here, and
|
||||||
|
// a stub left behind would be re-listed by the next scan.
|
||||||
|
let (local, _) = self.locate_id(id)?;
|
||||||
blocking(move || {
|
blocking(move || {
|
||||||
let what = local.display().to_string();
|
let what = local.display().to_string();
|
||||||
let meta = std::fs::symlink_metadata(&local).map_err(|e| map_io(e, &what))?;
|
let meta = std::fs::symlink_metadata(&local).map_err(|e| map_io(e, &what))?;
|
||||||
@@ -562,8 +721,19 @@ impl RemoteBackend for FolderBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn move_to(&self, from: &RemoteId, to: &RemotePath) -> Result<(), RemoteError> {
|
async fn move_to(&self, from: &RemoteId, to: &RemotePath) -> Result<(), RemoteError> {
|
||||||
let src = self.resolve_id(from)?;
|
// Move whichever name exists. Trashing a photograph that is not
|
||||||
let dst = self.resolve(to)?;
|
// downloaded is a perfectly ordinary thing to do, and it must move the
|
||||||
|
// stub — renaming a placeholder keeps it a placeholder.
|
||||||
|
let (src, materialised) = self.locate_id(from)?;
|
||||||
|
let dst = if materialised {
|
||||||
|
self.resolve(to)?
|
||||||
|
} else {
|
||||||
|
// The destination keeps the placeholder suffix, or the client
|
||||||
|
// would see a one-byte file appear where a photograph should be.
|
||||||
|
self.resolve(&RemotePath::new(
|
||||||
|
self.vfs.placeholder_name(to.as_str()).into_owned(),
|
||||||
|
))?
|
||||||
|
};
|
||||||
blocking(move || {
|
blocking(move || {
|
||||||
let what = dst.display().to_string();
|
let what = dst.display().to_string();
|
||||||
// Parents first: the trash folder does not exist until the first
|
// Parents first: the trash folder does not exist until the first
|
||||||
@@ -597,6 +767,60 @@ impl RemoteBackend for FolderBackend {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-NC-6c
|
||||||
|
/// Ask the sync client to download a placeholder, and wait for it.
|
||||||
|
///
|
||||||
|
/// Suffix-mode VFS *renames* on hydration, so completion is the
|
||||||
|
/// materialised path appearing — not the stub changing size. Polling the
|
||||||
|
/// original would wait forever.
|
||||||
|
async fn materialise(&self, id: &RemoteId) -> Result<(), RemoteError> {
|
||||||
|
let (local, materialised) = self.locate_id(id)?;
|
||||||
|
if materialised {
|
||||||
|
// Already here. Not an error, and not a reason to ask again: the
|
||||||
|
// borrow pool relies on this being idempotent.
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let vfs = self.vfs.clone();
|
||||||
|
let target = self.resolve_id(id)?;
|
||||||
|
blocking(move || {
|
||||||
|
vfs.materialise(&local)?;
|
||||||
|
|
||||||
|
// The client acknowledges the command, not the transfer, so this
|
||||||
|
// waits for the file to appear. A bounded wait: a hydration that
|
||||||
|
// has not landed in this long is one the caller should be told
|
||||||
|
// about rather than blocked on for ever — the pass can come back
|
||||||
|
// to it.
|
||||||
|
let deadline = std::time::Instant::now() + MATERIALISE_TIMEOUT;
|
||||||
|
while std::time::Instant::now() < deadline {
|
||||||
|
if target.is_file() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
std::thread::sleep(POLL);
|
||||||
|
}
|
||||||
|
Err(RemoteError::Network(format!(
|
||||||
|
"{} did not download within {}s",
|
||||||
|
target.display(),
|
||||||
|
MATERIALISE_TIMEOUT.as_secs()
|
||||||
|
)))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-NC-6c
|
||||||
|
/// Hand the content back, leaving a placeholder.
|
||||||
|
///
|
||||||
|
/// **Never a delete.** In a synced tree removing the file propagates the
|
||||||
|
/// removal to the server; the client is asked to dehydrate, and if it
|
||||||
|
/// cannot the content simply stays.
|
||||||
|
async fn dematerialise(&self, id: &RemoteId) -> Result<(), RemoteError> {
|
||||||
|
let (local, materialised) = self.locate_id(id)?;
|
||||||
|
if !materialised {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let vfs = self.vfs.clone();
|
||||||
|
blocking(move || vfs.dematerialise(&local)).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_dir(&self, path: &RemotePath) -> Result<(), RemoteError> {
|
async fn create_dir(&self, path: &RemotePath) -> Result<(), RemoteError> {
|
||||||
let local = self.resolve(path)?;
|
let local = self.resolve(path)?;
|
||||||
blocking(move || {
|
blocking(move || {
|
||||||
|
|||||||
@@ -482,7 +482,7 @@ async fn an_upload_lands_where_the_engine_places_it() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn an_endpoint_is_checked_before_an_account_is_written_for_it() {
|
fn an_endpoint_is_checked_before_an_account_is_written_for_it() {
|
||||||
let t = Tmp::new("provider");
|
let t = Tmp::new("provider");
|
||||||
let p = FolderProvider;
|
let p = FolderProvider::new();
|
||||||
|
|
||||||
assert!(p.normalise_endpoint(" ").is_err(), "empty");
|
assert!(p.normalise_endpoint(" ").is_err(), "empty");
|
||||||
assert!(p.normalise_endpoint("Pictures").is_err(), "relative");
|
assert!(p.normalise_endpoint("Pictures").is_err(), "relative");
|
||||||
@@ -504,7 +504,7 @@ fn two_spellings_of_one_folder_become_one_account() {
|
|||||||
// Otherwise the same photographs are indexed twice, into two catalogs.
|
// Otherwise the same photographs are indexed twice, into two catalogs.
|
||||||
let t = Tmp::new("canonical");
|
let t = Tmp::new("canonical");
|
||||||
t.file("sub/a.CR2", b"x");
|
t.file("sub/a.CR2", b"x");
|
||||||
let p = FolderProvider;
|
let p = FolderProvider::new();
|
||||||
let direct = p
|
let direct = p
|
||||||
.normalise_endpoint(&t.0.join("sub").to_string_lossy())
|
.normalise_endpoint(&t.0.join("sub").to_string_lossy())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -516,7 +516,7 @@ fn two_spellings_of_one_folder_become_one_account() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_folder_account_needs_no_credential() {
|
fn a_folder_account_needs_no_credential() {
|
||||||
let p = FolderProvider;
|
let p = FolderProvider::new();
|
||||||
assert_eq!(p.sign_in(), SignIn::EndpointOnly);
|
assert_eq!(p.sign_in(), SignIn::EndpointOnly);
|
||||||
assert!(!p.sign_in().needs_secret());
|
assert!(!p.sign_in().needs_secret());
|
||||||
|
|
||||||
@@ -532,9 +532,169 @@ fn the_registry_opens_a_folder_account() {
|
|||||||
// with nothing in between naming this crate.
|
// with nothing in between naming this crate.
|
||||||
let t = Tmp::new("registry");
|
let t = Tmp::new("registry");
|
||||||
let mut registry = dr_sync::BackendRegistry::new();
|
let mut registry = dr_sync::BackendRegistry::new();
|
||||||
registry.register(std::sync::Arc::new(FolderProvider));
|
registry.register(std::sync::Arc::new(FolderProvider::new()));
|
||||||
|
|
||||||
let account = Account::new(BACKEND_ID, t.0.to_string_lossy());
|
let account = Account::new(BACKEND_ID, t.0.to_string_lossy());
|
||||||
let backend = registry.connect(&Connection::new(account, None)).unwrap();
|
let backend = registry.connect(&Connection::new(account, None)).unwrap();
|
||||||
assert_eq!(backend.name(), "Folder");
|
assert_eq!(backend.name(), "Folder");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- virtual filesystems --------------------------------------------------
|
||||||
|
//
|
||||||
|
// A suffix-mode convention, matching the only one Linux supports. The
|
||||||
|
// behaviour under test is what the *backend* does with it; the borrow cycle
|
||||||
|
// has its own tests beside the pool.
|
||||||
|
|
||||||
|
struct SuffixVfs;
|
||||||
|
|
||||||
|
impl Vfs for SuffixVfs {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"suffix"
|
||||||
|
}
|
||||||
|
fn is_placeholder(&self, on_disk: &str) -> bool {
|
||||||
|
on_disk.ends_with(".stub")
|
||||||
|
}
|
||||||
|
fn real_name<'a>(&self, on_disk: &'a str) -> &'a str {
|
||||||
|
on_disk.strip_suffix(".stub").unwrap_or(on_disk)
|
||||||
|
}
|
||||||
|
fn placeholder_name(&self, name: &str) -> std::borrow::Cow<'_, str> {
|
||||||
|
std::borrow::Cow::Owned(format!("{name}.stub"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_stubs(t: &Tmp) -> FolderBackend {
|
||||||
|
FolderBackend::with_vfs(&t.0, std::sync::Arc::new(SuffixVfs)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_placeholder_is_listed_under_the_photographs_own_name() {
|
||||||
|
// The catalog records this as `source_ref`, and identity is derived from
|
||||||
|
// it. Reporting the stub's name gives the same photograph two identities
|
||||||
|
// and a name no other device recognises.
|
||||||
|
let t = Tmp::new("vfs-name");
|
||||||
|
t.file("shoot/IMG_0001.CR2.stub", &[0u8]);
|
||||||
|
let b = with_stubs(&t);
|
||||||
|
|
||||||
|
let entries = b.list(&RemotePath::new("shoot"), None).await.unwrap();
|
||||||
|
assert_eq!(entries[0].path.as_str(), "shoot/IMG_0001.CR2");
|
||||||
|
assert!(!entries[0].materialised, "the content is not here");
|
||||||
|
// One byte is not the photograph's size, and putting it in the catalog
|
||||||
|
// would claim a 30 MB RAW is a single byte.
|
||||||
|
assert_eq!(entries[0].size, 0, "unknown, not one");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn identity_survives_a_download() {
|
||||||
|
// The failure this prevents: downloading a photograph looked like a
|
||||||
|
// delete and an add, which orphaned its thumbnail and its face rows.
|
||||||
|
let t = Tmp::new("vfs-identity");
|
||||||
|
t.file("a.CR2.stub", &[0u8]);
|
||||||
|
let b = with_stubs(&t);
|
||||||
|
|
||||||
|
let before = b.list(&RemotePath::root(), None).await.unwrap()[0]
|
||||||
|
.id
|
||||||
|
.clone();
|
||||||
|
std::fs::remove_file(t.0.join("a.CR2.stub")).unwrap();
|
||||||
|
std::fs::write(t.0.join("a.CR2"), vec![3u8; 4096]).unwrap();
|
||||||
|
let after = b.list(&RemotePath::root(), None).await.unwrap()[0]
|
||||||
|
.id
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
assert_eq!(before, after, "the same photograph throughout");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn reading_a_placeholder_is_distinguishable_from_a_missing_file() {
|
||||||
|
// The distinction the sidecar writer depends on: "not here" is fetchable
|
||||||
|
// and "not found" means create a new one. Conflating them overwrites an
|
||||||
|
// existing sidecar with a fresh document.
|
||||||
|
let t = Tmp::new("vfs-read");
|
||||||
|
t.file("a.drsc.stub", &[0u8]);
|
||||||
|
let b = with_stubs(&t);
|
||||||
|
|
||||||
|
let stub = b
|
||||||
|
.get(&RemoteId::Path(RemotePath::new("a.drsc")), None)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(stub, RemoteError::NotMaterialised(_)), "{stub:?}");
|
||||||
|
|
||||||
|
let absent = b
|
||||||
|
.get(&RemoteId::Path(RemotePath::new("nothing.drsc")), None)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(absent, RemoteError::NotFound(_)), "{absent:?}");
|
||||||
|
|
||||||
|
// And emphatically not the stub's one byte, which is what made a
|
||||||
|
// dehydrated sidecar parse as an empty document.
|
||||||
|
assert!(!matches!(stub, RemoteError::NotFound(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn writing_over_a_placeholder_is_refused() {
|
||||||
|
// Writing `a.drsc` beside `a.drsc.stub` makes two files for one document
|
||||||
|
// and hands the sync client a conflict it resolves arbitrarily.
|
||||||
|
let t = Tmp::new("vfs-write");
|
||||||
|
t.file("a.drsc.stub", &[0u8]);
|
||||||
|
let b = with_stubs(&t);
|
||||||
|
|
||||||
|
let e = b
|
||||||
|
.put(&RemotePath::new("a.drsc"), b"<new/>".to_vec(), None)
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(matches!(e, RemoteError::NotMaterialised(_)), "{e:?}");
|
||||||
|
assert!(!t.0.join("a.drsc").exists(), "no rival file created");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn trashing_a_photograph_that_is_not_downloaded_moves_the_placeholder() {
|
||||||
|
// Culling without downloading is the ordinary way to use a VFS library.
|
||||||
|
// The stub has to move, and has to stay a stub — leaving it behind means
|
||||||
|
// the next scan re-lists the image and undoes the delete.
|
||||||
|
let t = Tmp::new("vfs-trash");
|
||||||
|
t.file("a.CR2.stub", &[0u8]);
|
||||||
|
let b = with_stubs(&t);
|
||||||
|
|
||||||
|
b.move_to(
|
||||||
|
&RemoteId::Path(RemotePath::new("a.CR2")),
|
||||||
|
&RemotePath::new(".darkroom-trash/a.CR2"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!t.0.join("a.CR2.stub").exists());
|
||||||
|
assert!(
|
||||||
|
t.0.join(".darkroom-trash/a.CR2.stub").is_file(),
|
||||||
|
"still a stub"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_folder_without_a_client_still_lists_and_reads_what_is_there() {
|
||||||
|
// No hydration available is a degraded mode, not a broken one: the
|
||||||
|
// materialised half of the library works completely.
|
||||||
|
let t = Tmp::new("vfs-degraded");
|
||||||
|
t.file("here.CR2", b"real").file("gone.CR2.stub", &[0u8]);
|
||||||
|
let b = with_stubs(&t);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
b.capabilities().materialisation,
|
||||||
|
dr_sync::Materialisation::Placeholders,
|
||||||
|
"stubs exist and nothing can fetch them"
|
||||||
|
);
|
||||||
|
assert!(!b.capabilities().materialisation.can_materialise());
|
||||||
|
|
||||||
|
let got = b
|
||||||
|
.get(&RemoteId::Path(RemotePath::new("here.CR2")), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(got, b"real");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_plain_folder_reports_that_everything_it_lists_is_readable() {
|
||||||
|
let t = Tmp::new("vfs-plain");
|
||||||
|
assert_eq!(
|
||||||
|
t.backend().capabilities().materialisation,
|
||||||
|
dr_sync::Materialisation::Always
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
// TRACES: FR-NC-6c
|
||||||
|
//! Virtual-filesystem conventions layered over a directory.
|
||||||
|
//!
|
||||||
|
//! A sync client in virtual-files mode leaves a *placeholder* where a file is
|
||||||
|
//! catalogued but not downloaded. The folder is otherwise ordinary, so all of
|
||||||
|
//! [`FolderBackend`](crate::FolderBackend) applies — only three questions
|
||||||
|
//! differ, and they are the whole of this trait: what is a placeholder, what
|
||||||
|
//! is the photograph really called, and can the content be summoned.
|
||||||
|
//!
|
||||||
|
//! # Why this is not a separate backend
|
||||||
|
//!
|
||||||
|
//! It varies nothing about listing, reading, writing, moving or deleting — a
|
||||||
|
//! second connector would duplicate every one of those to change a name test.
|
||||||
|
//! More decisively, **the interesting capability is not a property of the
|
||||||
|
//! backend at all**: the same folder can materialise on demand while the sync
|
||||||
|
//! client is running and cannot when it is not, so it has to be computed per
|
||||||
|
//! connection either way. Registering a `folder-vfs` provider beside `folder`
|
||||||
|
//! would ask the user to choose between two things that differ by whether a
|
||||||
|
//! background process happens to be up.
|
||||||
|
//!
|
||||||
|
//! # Why the plain case is a `Vfs` too
|
||||||
|
//!
|
||||||
|
//! [`NoVfs`] answers "nothing is a placeholder" and refuses to materialise.
|
||||||
|
//! That keeps one code path through the backend rather than an `Option` tested
|
||||||
|
//! at every call site, and it is the shape a third convention — Dropbox,
|
||||||
|
//! OneDrive, macOS FileProvider — slots into.
|
||||||
|
//!
|
||||||
|
//! # What is *not* abstracted here
|
||||||
|
//!
|
||||||
|
//! Windows and macOS express placeholders in filesystem metadata rather than
|
||||||
|
//! in the name: a reparse point, or `st_blocks == 0` against a non-zero
|
||||||
|
//! `st_size`. That form needs a `Metadata` to answer, not a name, and the one
|
||||||
|
//! convention this project has met needs only a name. Widening the trait for a
|
||||||
|
//! platform nobody has run this on would be guessing at the shape.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use dr_sync::RemoteError;
|
||||||
|
|
||||||
|
/// A placeholder convention, and what can be done about it.
|
||||||
|
///
|
||||||
|
/// Implementations are held behind an `Arc` and used from every worker
|
||||||
|
/// thread.
|
||||||
|
pub trait Vfs: Send + Sync {
|
||||||
|
/// A name for logs and the interface. "none", "Nextcloud".
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
|
||||||
|
/// Whether a name **on disk** stands for content that is not here.
|
||||||
|
fn is_placeholder(&self, on_disk: &str) -> bool;
|
||||||
|
|
||||||
|
/// The photograph's own name, given whatever is on disk.
|
||||||
|
///
|
||||||
|
/// This is what the catalog records and what identity is derived from, so
|
||||||
|
/// a file keeps one name and one id across being downloaded and released.
|
||||||
|
/// Reporting the on-disk name instead makes hydration look like a delete
|
||||||
|
/// and an add.
|
||||||
|
fn real_name<'a>(&self, on_disk: &'a str) -> &'a str;
|
||||||
|
|
||||||
|
/// What a placeholder for `name` would be called on disk.
|
||||||
|
fn placeholder_name(&self, name: &str) -> Cow<'_, str>;
|
||||||
|
|
||||||
|
/// Whether content can actually be summoned right now.
|
||||||
|
///
|
||||||
|
/// False where the mechanism is absent — the client is not running, the
|
||||||
|
/// platform has no socket — which is an ordinary state and not an error.
|
||||||
|
/// The backend reports [`Materialisation::Placeholders`] rather than
|
||||||
|
/// [`OnDemand`] when this is false.
|
||||||
|
///
|
||||||
|
/// [`Materialisation::Placeholders`]: dr_sync::Materialisation::Placeholders
|
||||||
|
/// [`OnDemand`]: dr_sync::Materialisation::OnDemand
|
||||||
|
fn can_materialise(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ask for a placeholder's content. Whole-file and slow.
|
||||||
|
fn materialise(&self, _local: &Path) -> Result<(), RemoteError> {
|
||||||
|
Err(RemoteError::Unsupported("this folder has no VFS client"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Give the content back, leaving a placeholder.
|
||||||
|
///
|
||||||
|
/// **Must not delete.** In a synced tree a deletion propagates to the
|
||||||
|
/// server and removes the photograph from every device. An implementation
|
||||||
|
/// that cannot dehydrate returns `Unsupported`.
|
||||||
|
fn dematerialise(&self, _local: &Path) -> Result<(), RemoteError> {
|
||||||
|
Err(RemoteError::Unsupported("this folder has no VFS client"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An ordinary directory: every file is what it appears to be.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct NoVfs;
|
||||||
|
|
||||||
|
impl Vfs for NoVfs {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"none"
|
||||||
|
}
|
||||||
|
fn is_placeholder(&self, _on_disk: &str) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn real_name<'a>(&self, on_disk: &'a str) -> &'a str {
|
||||||
|
on_disk
|
||||||
|
}
|
||||||
|
fn placeholder_name(&self, name: &str) -> Cow<'_, str> {
|
||||||
|
// Nothing is ever a placeholder here, so the only honest answer is
|
||||||
|
// the name itself — the backend will look for it, not find a second
|
||||||
|
// candidate, and report the file missing.
|
||||||
|
Cow::Owned(name.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_plain_folder_has_no_placeholders_and_cannot_summon_anything() {
|
||||||
|
let v = NoVfs;
|
||||||
|
assert!(
|
||||||
|
!v.is_placeholder("IMG.CR2.nextcloud"),
|
||||||
|
"not this folder's convention"
|
||||||
|
);
|
||||||
|
assert_eq!(v.real_name("IMG.CR2"), "IMG.CR2");
|
||||||
|
assert!(!v.can_materialise());
|
||||||
|
assert!(v.materialise(Path::new("/x")).is_err());
|
||||||
|
// And it must refuse rather than approximate: deleting a file to
|
||||||
|
// "dehydrate" it would remove the photograph.
|
||||||
|
assert!(v.dematerialise(Path::new("/x")).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,9 @@ license.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
dr-types.workspace = true
|
dr-types.workspace = true
|
||||||
dr-sync.workspace = true
|
dr-sync.workspace = true
|
||||||
|
# For the VFS convention: the folder connector does the filesystem work, and
|
||||||
|
# this crate supplies the placeholder rules and the client socket.
|
||||||
|
dr-sync-folder.workspace = true
|
||||||
dr-plat.workspace = true
|
dr-plat.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
rustls.workspace = true
|
rustls.workspace = true
|
||||||
|
|||||||
@@ -163,3 +163,138 @@ mod tests {
|
|||||||
let _ = DesktopClient::detect();
|
let _ = DesktopClient::detect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-NC-6c
|
||||||
|
/// The desktop client's placeholder convention, as a
|
||||||
|
/// [`Vfs`](dr_sync_folder::Vfs).
|
||||||
|
///
|
||||||
|
/// This is what turns a folder the client syncs into a library DarkRoom can
|
||||||
|
/// open: the folder connector handles every filesystem operation, and this
|
||||||
|
/// answers the three questions it cannot — what is a stub, what is the
|
||||||
|
/// photograph called, and can the content be fetched and given back.
|
||||||
|
///
|
||||||
|
/// **Linux suffix mode only**, which is the only mode Linux supports
|
||||||
|
/// (ARCH §9.0). A dehydrated `IMG.CR2` exists solely as `IMG.CR2.nextcloud`
|
||||||
|
/// holding one byte.
|
||||||
|
pub struct NextcloudVfs {
|
||||||
|
/// `None` where no client is running. The folder still lists and reads
|
||||||
|
/// correctly; it simply cannot fetch what is not there, which the backend
|
||||||
|
/// reports as `Materialisation::Placeholders`.
|
||||||
|
client: Option<DesktopClient>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NextcloudVfs {
|
||||||
|
/// Attach to a running client, if there is one.
|
||||||
|
///
|
||||||
|
/// Absence is the ordinary state — Android always, desktop whenever the
|
||||||
|
/// client is not running — and never an error.
|
||||||
|
pub fn detect() -> Self {
|
||||||
|
Self {
|
||||||
|
client: DesktopClient::detect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a directory looks like one this client syncs.
|
||||||
|
///
|
||||||
|
/// Used to decide whether to apply this convention at all. Deliberately
|
||||||
|
/// cheap and deliberately not authoritative: the client's own database
|
||||||
|
/// would answer properly, but it is a private schema, and being wrong here
|
||||||
|
/// costs one extra `stat` per read rather than anything correctness
|
||||||
|
/// depends on.
|
||||||
|
pub fn looks_synced(root: &Path) -> bool {
|
||||||
|
std::fs::read_dir(root)
|
||||||
|
.map(|entries| {
|
||||||
|
entries.flatten().any(|e| {
|
||||||
|
let name = e.file_name();
|
||||||
|
let name = name.to_string_lossy();
|
||||||
|
// The client's per-folder journal sits at the sync root,
|
||||||
|
// and a stub anywhere beneath it is equally conclusive.
|
||||||
|
name.starts_with("._sync_") && name.ends_with(".db")
|
||||||
|
|| name.ends_with(dr_types::PLACEHOLDER_SUFFIX)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl dr_sync_folder::Vfs for NextcloudVfs {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Nextcloud"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_placeholder(&self, on_disk: &str) -> bool {
|
||||||
|
on_disk.ends_with(dr_types::PLACEHOLDER_SUFFIX)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn real_name<'a>(&self, on_disk: &'a str) -> &'a str {
|
||||||
|
on_disk
|
||||||
|
.strip_suffix(dr_types::PLACEHOLDER_SUFFIX)
|
||||||
|
.unwrap_or(on_disk)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder_name(&self, name: &str) -> std::borrow::Cow<'_, str> {
|
||||||
|
std::borrow::Cow::Owned(format!("{name}{}", dr_types::PLACEHOLDER_SUFFIX))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_materialise(&self) -> bool {
|
||||||
|
self.client.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn materialise(&self, local: &Path) -> Result<(), RemoteError> {
|
||||||
|
self.client
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(RemoteError::Unsupported(
|
||||||
|
"no Nextcloud desktop client is running to fetch this",
|
||||||
|
))?
|
||||||
|
.make_available_locally(local)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dematerialise(&self, local: &Path) -> Result<(), RemoteError> {
|
||||||
|
self.client
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(RemoteError::Unsupported(
|
||||||
|
"no Nextcloud desktop client is running to release this",
|
||||||
|
))?
|
||||||
|
.make_online_only(local)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod vfs_tests {
|
||||||
|
use super::*;
|
||||||
|
use dr_sync_folder::Vfs as _;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_stub_is_recognised_and_reports_the_photographs_name() {
|
||||||
|
let v = NextcloudVfs { client: None };
|
||||||
|
assert!(v.is_placeholder("IMG_4130.CR2.nextcloud"));
|
||||||
|
assert!(!v.is_placeholder("IMG_4130.CR2"));
|
||||||
|
// The name the catalog records, so identity survives a download.
|
||||||
|
assert_eq!(v.real_name("IMG_4130.CR2.nextcloud"), "IMG_4130.CR2");
|
||||||
|
assert_eq!(v.real_name("IMG_4130.CR2"), "IMG_4130.CR2");
|
||||||
|
assert_eq!(v.placeholder_name("IMG_4130.CR2"), "IMG_4130.CR2.nextcloud");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn without_a_client_it_refuses_rather_than_pretending() {
|
||||||
|
// The folder still works; it just cannot fetch. Silently doing nothing
|
||||||
|
// would make a borrow think it had the content.
|
||||||
|
let v = NextcloudVfs { client: None };
|
||||||
|
assert!(!v.can_materialise());
|
||||||
|
assert!(v.materialise(Path::new("/x/a.CR2.nextcloud")).is_err());
|
||||||
|
assert!(v.dematerialise(Path::new("/x/a.CR2")).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_ordinary_folder_is_not_mistaken_for_a_synced_one() {
|
||||||
|
let d = std::env::temp_dir().join("dr-vfs-detect");
|
||||||
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
|
std::fs::create_dir_all(&d).unwrap();
|
||||||
|
std::fs::write(d.join("a.CR2"), b"raw").unwrap();
|
||||||
|
assert!(!NextcloudVfs::looks_synced(&d));
|
||||||
|
|
||||||
|
std::fs::write(d.join("b.CR2.nextcloud"), [0u8]).unwrap();
|
||||||
|
assert!(NextcloudVfs::looks_synced(&d));
|
||||||
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ mod propfind;
|
|||||||
pub mod provider;
|
pub mod provider;
|
||||||
|
|
||||||
pub use auth::{AppCredentials, LoginFlow};
|
pub use auth::{AppCredentials, LoginFlow};
|
||||||
pub use desktop_client::DesktopClient;
|
pub use desktop_client::{DesktopClient, NextcloudVfs};
|
||||||
pub use provider::NextcloudProvider;
|
pub use provider::NextcloudProvider;
|
||||||
|
|
||||||
/// Chunk sizes Nextcloud's chunked upload v2 accepts.
|
/// Chunk sizes Nextcloud's chunked upload v2 accepts.
|
||||||
@@ -73,6 +73,10 @@ impl NextcloudBackend {
|
|||||||
// Stock Nextcloud ships no RAW preview provider (ARCH §6.7).
|
// Stock Nextcloud ships no RAW preview provider (ARCH §6.7).
|
||||||
// Probed per-account at setup and upgraded where present.
|
// Probed per-account at setup and upgraded where present.
|
||||||
server_previews: ServerPreviews::CommonFormatsOnly,
|
server_previews: ServerPreviews::CommonFormatsOnly,
|
||||||
|
// The server answers for everything it lists. Placeholders
|
||||||
|
// belong to a locally *synced folder*, which is the folder
|
||||||
|
// connector's business (`desktop_client::NextcloudVfs`).
|
||||||
|
materialisation: dr_sync::Materialisation::Always,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ pub fn parse_multistatus(xml: &str, base: &str) -> Result<Vec<RemoteEntry>, Remo
|
|||||||
size: r.content_length.unwrap_or(0),
|
size: r.content_length.unwrap_or(0),
|
||||||
modified: r.last_modified.as_deref().and_then(parse_http_date),
|
modified: r.last_modified.as_deref().and_then(parse_http_date),
|
||||||
has_preview: r.has_preview,
|
has_preview: r.has_preview,
|
||||||
|
// Everything WebDAV lists can be fetched; placeholders are a
|
||||||
|
// property of a locally synced folder, not of the server.
|
||||||
|
materialised: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(out)
|
Ok(out)
|
||||||
|
|||||||
@@ -38,6 +38,50 @@ pub struct ChunkConstraints {
|
|||||||
pub max_chunks: u32,
|
pub max_chunks: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// TRACES: FR-NC-6c
|
||||||
|
/// Whether every listed object's content is actually reachable.
|
||||||
|
///
|
||||||
|
/// Every backend but a virtual-filesystem folder answers [`Always`](Self::Always).
|
||||||
|
/// A VFS folder is the case this exists for: the sync client leaves a
|
||||||
|
/// placeholder where a file is catalogued but not downloaded, so the name is
|
||||||
|
/// listable and the bytes are not (ARCH §9.0).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Materialisation {
|
||||||
|
/// Listing an object means its content can be read. Every server backend,
|
||||||
|
/// and a plain directory.
|
||||||
|
Always,
|
||||||
|
|
||||||
|
/// Some objects are placeholders, and nothing this process can do will
|
||||||
|
/// change that — the sync client is not running, or the platform offers no
|
||||||
|
/// way to ask. Such an object reads as
|
||||||
|
/// [`RemoteError::NotMaterialised`](crate::RemoteError::NotMaterialised)
|
||||||
|
/// and is shown as offline rather than broken.
|
||||||
|
Placeholders,
|
||||||
|
|
||||||
|
/// Some objects are placeholders, and this backend can ask for their
|
||||||
|
/// content — and give it back.
|
||||||
|
///
|
||||||
|
/// **Whole-file, and that is the whole difficulty.** Hydration has two
|
||||||
|
/// states, one byte or all bytes, so using it to fill a grid transfers the
|
||||||
|
/// entire library to produce thumbnails (ARCH §9.0). It belongs to the
|
||||||
|
/// originals tier — an image opened in develop, exported, or deliberately
|
||||||
|
/// pinned — and to passes the user has asked for and been quoted a price
|
||||||
|
/// on. Never to browsing.
|
||||||
|
OnDemand,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Materialisation {
|
||||||
|
/// Whether content can be fetched on request.
|
||||||
|
pub fn can_materialise(self) -> bool {
|
||||||
|
matches!(self, Materialisation::OnDemand)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether some objects may have no content locally.
|
||||||
|
pub fn has_placeholders(self) -> bool {
|
||||||
|
!matches!(self, Materialisation::Always)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// TRACES: FR-NC-3
|
/// TRACES: FR-NC-3
|
||||||
/// Whether the server can render thumbnails, and for what.
|
/// Whether the server can render thumbnails, and for what.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -69,6 +113,8 @@ pub struct Capabilities {
|
|||||||
/// Conditional write (If-Match), for conflict-safe sidecar updates.
|
/// Conditional write (If-Match), for conflict-safe sidecar updates.
|
||||||
pub conditional_write: bool,
|
pub conditional_write: bool,
|
||||||
pub server_previews: ServerPreviews,
|
pub server_previews: ServerPreviews,
|
||||||
|
/// Whether a listed object's content is necessarily present.
|
||||||
|
pub materialisation: Materialisation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Capabilities {
|
impl Capabilities {
|
||||||
@@ -83,6 +129,9 @@ impl Capabilities {
|
|||||||
bulk_upload: false,
|
bulk_upload: false,
|
||||||
conditional_write: false,
|
conditional_write: false,
|
||||||
server_previews: ServerPreviews::None,
|
server_previews: ServerPreviews::None,
|
||||||
|
// The weakest backend still answers for everything it lists;
|
||||||
|
// placeholders are a property a backend opts into.
|
||||||
|
materialisation: Materialisation::Always,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,22 @@ pub enum RemoteError {
|
|||||||
#[error("not found: {0}")]
|
#[error("not found: {0}")]
|
||||||
NotFound(String),
|
NotFound(String),
|
||||||
|
|
||||||
|
/// TRACES: FR-NC-6c
|
||||||
|
/// The object exists, but its content is not on this device.
|
||||||
|
///
|
||||||
|
/// A virtual-filesystem placeholder: the sync client holds the name and a
|
||||||
|
/// stub, and the bytes are still on the server (ARCH §9.0).
|
||||||
|
///
|
||||||
|
/// **Emphatically not [`NotFound`](Self::NotFound), and the distinction is
|
||||||
|
/// what stops a silent data loss.** The sidecar writer reads before it
|
||||||
|
/// writes, and treats a miss as "there is no sidecar yet, create one" — so
|
||||||
|
/// a dehydrated sidecar reported as absent makes it write a fresh document
|
||||||
|
/// over an existing one, discarding every edit another device had put
|
||||||
|
/// there. It is also the difference between an error a user can act on
|
||||||
|
/// (fetch it) and one they cannot (it is gone).
|
||||||
|
#[error("not on this device: {0}")]
|
||||||
|
NotMaterialised(String),
|
||||||
|
|
||||||
/// The backend does not support this operation. Expected, not a bug —
|
/// The backend does not support this operation. Expected, not a bug —
|
||||||
/// callers check capabilities and adapt.
|
/// callers check capabilities and adapt.
|
||||||
#[error("operation unsupported by this backend: {0}")]
|
#[error("operation unsupported by this backend: {0}")]
|
||||||
|
|||||||
+46
-1
@@ -35,7 +35,9 @@ pub mod types;
|
|||||||
pub mod upload;
|
pub mod upload;
|
||||||
|
|
||||||
pub use account::{Account, AccountError, AccountStore, Connection, Secret, LEGACY_BACKEND};
|
pub use account::{Account, AccountError, AccountStore, Connection, Secret, LEGACY_BACKEND};
|
||||||
pub use capability::{Capabilities, ChangeDetection, ChunkConstraints, ServerPreviews};
|
pub use capability::{
|
||||||
|
Capabilities, ChangeDetection, ChunkConstraints, Materialisation, ServerPreviews,
|
||||||
|
};
|
||||||
pub use error::RemoteError;
|
pub use error::RemoteError;
|
||||||
pub use provider::{BackendProvider, BackendRegistry, SignIn};
|
pub use provider::{BackendProvider, BackendRegistry, SignIn};
|
||||||
pub use reachability::{Connectivity, Reachability};
|
pub use reachability::{Connectivity, Reachability};
|
||||||
@@ -151,6 +153,49 @@ pub trait RemoteBackend: Send + Sync {
|
|||||||
/// destination, not to claim they created it.
|
/// destination, not to claim they created it.
|
||||||
async fn create_dir(&self, path: &RemotePath) -> Result<(), RemoteError>;
|
async fn create_dir(&self, path: &RemotePath) -> Result<(), RemoteError>;
|
||||||
|
|
||||||
|
// ---- materialisation --------------------------------------------------
|
||||||
|
|
||||||
|
/// TRACES: FR-NC-6c
|
||||||
|
/// Ask for a placeholder's content to be brought to this device.
|
||||||
|
///
|
||||||
|
/// Only meaningful where [`Capabilities::materialisation`] is
|
||||||
|
/// [`Materialisation::OnDemand`]; others return
|
||||||
|
/// [`RemoteError::Unsupported`].
|
||||||
|
///
|
||||||
|
/// **Whole-file, and slow.** There is no partial hydration: a placeholder
|
||||||
|
/// becomes one byte or all of them, so this transfers a 27 MB RAW to
|
||||||
|
/// answer a question a 256 KB range read would have answered (ARCH §9.0
|
||||||
|
/// finding 3). It is for the originals tier — develop, export, a pin the
|
||||||
|
/// user asked for — and for passes the user has been quoted a price on and
|
||||||
|
/// agreed to. **Never for filling a grid**: doing so downloads the entire
|
||||||
|
/// library to produce thumbnails.
|
||||||
|
///
|
||||||
|
/// Returns once the content is readable. Callers that borrowed it should
|
||||||
|
/// hand it back with [`dematerialise`](Self::dematerialise).
|
||||||
|
async fn materialise(&self, _id: &RemoteId) -> Result<(), RemoteError> {
|
||||||
|
Err(RemoteError::Unsupported(
|
||||||
|
"this backend has no placeholders to materialise",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Give a placeholder's content back, freeing the disk it held.
|
||||||
|
///
|
||||||
|
/// The counterpart that makes hydration a *borrow* rather than an
|
||||||
|
/// acquisition: a pass that hydrates a library to index it can return each
|
||||||
|
/// file as it finishes, so peak disk is the working set rather than the
|
||||||
|
/// library.
|
||||||
|
///
|
||||||
|
/// **Never destructive.** On a synced folder this asks the client to
|
||||||
|
/// dehydrate; it must not delete, because a deletion in a synced tree
|
||||||
|
/// propagates to the server and removes the photograph everywhere. An
|
||||||
|
/// implementation that cannot dehydrate must return
|
||||||
|
/// [`RemoteError::Unsupported`] rather than approximating it.
|
||||||
|
async fn dematerialise(&self, _id: &RemoteId) -> Result<(), RemoteError> {
|
||||||
|
Err(RemoteError::Unsupported(
|
||||||
|
"this backend has no placeholders to release",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// ---- optional ---------------------------------------------------------
|
// ---- optional ---------------------------------------------------------
|
||||||
|
|
||||||
/// Server-rendered thumbnail, where available.
|
/// Server-rendered thumbnail, where available.
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ mod tests {
|
|||||||
size: 0,
|
size: 0,
|
||||||
modified: None,
|
modified: None,
|
||||||
has_preview: false,
|
has_preview: false,
|
||||||
|
materialised: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +266,7 @@ mod tests {
|
|||||||
size: 1000,
|
size: 1000,
|
||||||
modified: None,
|
modified: None,
|
||||||
has_preview: false,
|
has_preview: false,
|
||||||
|
materialised: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,6 +303,7 @@ mod tests {
|
|||||||
bulk_upload: false,
|
bulk_upload: false,
|
||||||
conditional_write: true,
|
conditional_write: true,
|
||||||
server_previews: ServerPreviews::None,
|
server_previews: ServerPreviews::None,
|
||||||
|
materialisation: crate::Materialisation::Always,
|
||||||
},
|
},
|
||||||
lists: RefCell::new(0),
|
lists: RefCell::new(0),
|
||||||
probes: RefCell::new(0),
|
probes: RefCell::new(0),
|
||||||
|
|||||||
@@ -97,6 +97,23 @@ pub struct RemoteEntry {
|
|||||||
/// Whether the server claims a renderable preview exists. Advisory: stock
|
/// Whether the server claims a renderable preview exists. Advisory: stock
|
||||||
/// Nextcloud reports none for RAW (ARCH §6.7).
|
/// Nextcloud reports none for RAW (ARCH §6.7).
|
||||||
pub has_preview: bool,
|
pub has_preview: bool,
|
||||||
|
|
||||||
|
/// TRACES: FR-NC-6c
|
||||||
|
/// Whether [`get`](crate::RemoteBackend::get) can produce this object's
|
||||||
|
/// content right now.
|
||||||
|
///
|
||||||
|
/// True for everything a server backend lists — the bytes are remote, but
|
||||||
|
/// they are reachable. False only for a virtual-filesystem placeholder,
|
||||||
|
/// where the name is on this device and the content is not (ARCH §9.0).
|
||||||
|
///
|
||||||
|
/// The catalog maps this to [`Availability::Offline`](dr_types::Availability),
|
||||||
|
/// which is the difference between a photograph shown as *not downloaded*
|
||||||
|
/// and one shown as broken.
|
||||||
|
///
|
||||||
|
/// **`size` is not meaningful when this is false.** A Linux suffix-mode
|
||||||
|
/// stub is one byte and carries no record of what it stands for, so there
|
||||||
|
/// is nothing to report but zero.
|
||||||
|
pub materialised: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ mod tests {
|
|||||||
size: *size,
|
size: *size,
|
||||||
modified: None,
|
modified: None,
|
||||||
has_preview: false,
|
has_preview: false,
|
||||||
|
materialised: true,
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -876,6 +876,39 @@ remains the only mechanism that satisfies FR-NC-3.
|
|||||||
- Never depend on it: the socket is Linux-only, absent on Android, and absent when the client is
|
- Never depend on it: the socket is Linux-only, absent on Android, and absent when the client is
|
||||||
not running. The direct connector remains the primary path.
|
not running. The direct connector remains the primary path.
|
||||||
|
|
||||||
|
#### 9.0a Amendment, 2026-08-29 — VFS as a managed tier for *folder* libraries
|
||||||
|
|
||||||
|
The rejection above was written when the only backend was the direct Nextcloud connector, and it
|
||||||
|
assumed the alternative to hydration was a range read. Since `dr-sync-folder` exists (§8.4a) a
|
||||||
|
library can be opened as a directory with **no server connection at all**, and that changes what
|
||||||
|
finding 3 is comparing against.
|
||||||
|
|
||||||
|
**What finding 3 actually says.** Hydration transfers ~100× what a preview needs *when a range read
|
||||||
|
is available*. On a folder library there is no connector, so there are no range reads: the choice is
|
||||||
|
not hydrate-versus-range-read, it is hydrate-once or never have a thumbnail. The finding stands
|
||||||
|
unamended for the direct connector, which must still never hydrate to browse.
|
||||||
|
|
||||||
|
**What makes the cost acceptable on a folder library**, and all three are required:
|
||||||
|
|
||||||
|
1. **Hydration is a borrow, not an acquisition.** A file is returned to the state it was found in —
|
||||||
|
what the pass downloaded is released, what the user already had is left alone. Peak disk is the
|
||||||
|
working set, not the library.
|
||||||
|
2. **It is paid once.** Thumbnails are kept, and `derived_sync` pushes the shards to the server, so
|
||||||
|
a second device downloads 200 MB of shards instead of hydrating 340 GB of RAWs.
|
||||||
|
3. **It is quoted and consented to.** Never automatic, never on the browsing path, always
|
||||||
|
resumable and cancellable (FR-NC-6c).
|
||||||
|
|
||||||
|
**What this does not change.** Findings 1 and 2 stand and are now implemented rather than merely
|
||||||
|
noted: a stub is not transparent, so `RemoteEntry` carries `materialised` and the backend reports
|
||||||
|
`RemoteError::NotMaterialised` rather than a miss; and the socket remains the only way to hydrate,
|
||||||
|
so it stays Linux-only, optional, and absent on Android.
|
||||||
|
|
||||||
|
**The one thing that must never be got wrong.** Releasing content means asking the client to
|
||||||
|
dehydrate — never deleting the file. A deletion inside a synced tree propagates to the server and
|
||||||
|
removes the photograph from every device the user owns. `RemoteBackend::dematerialise` says so, the
|
||||||
|
`Vfs` trait says so, and an implementation that cannot dehydrate returns `Unsupported` rather than
|
||||||
|
approximating it with `remove_file`.
|
||||||
|
|
||||||
### 9.1 The three tiers, restated as policy
|
### 9.1 The three tiers, restated as policy
|
||||||
|
|
||||||
| Tier | Content | Default |
|
| Tier | Content | Default |
|
||||||
|
|||||||
@@ -647,6 +647,25 @@ proxies, then thumbnails. **Metadata and sidecars are never evicted** — they a
|
|||||||
extension, and size, so users do not know what they actually have. Sync failures of legibility are
|
extension, and size, so users do not know what they actually have. Sync failures of legibility are
|
||||||
more damaging than failures of transport.
|
more damaging than failures of transport.
|
||||||
|
|
||||||
|
**FR-NC-6d — Placeholder libraries.** Where a library is a folder kept by a sync client in
|
||||||
|
virtual-files mode, the app shall treat a placeholder as *the photograph, not downloaded* — never as
|
||||||
|
a one-byte file and never as a missing one.
|
||||||
|
|
||||||
|
- A placeholder is catalogued under the photograph's own name, with an identity that does not change
|
||||||
|
when it is downloaded
|
||||||
|
- Reading one yields a distinct, actionable error; it shall **not** be reported as absent, because
|
||||||
|
the sidecar writer creates a new document when a sidecar is absent and would discard the existing
|
||||||
|
one (FR-CAT-8)
|
||||||
|
- Its size is reported as unknown rather than as the stub's byte count
|
||||||
|
|
||||||
|
Where the client offers hydration, content may be fetched **as a borrow**: a file is returned to the
|
||||||
|
state it was found in, so a pass releases what it downloaded and leaves alone what the user already
|
||||||
|
had. Releasing means asking the client to dehydrate — **never deleting**, which inside a synced tree
|
||||||
|
would propagate to the server and remove the photograph everywhere.
|
||||||
|
|
||||||
|
Hydration is whole-file and shall never serve browsing (ARCH §9.0 finding 3, §9.0a). It is for the
|
||||||
|
originals tier and for passes the user has been quoted a cost on and has agreed to.
|
||||||
|
|
||||||
**FR-NC-7 — Upload.** Files above 5MB use **chunked upload v2** against
|
**FR-NC-7 — Upload.** Files above 5MB use **chunked upload v2** against
|
||||||
`/remote.php/dav/uploads/<userid>/`: `MKCOL` to create the upload folder, `PUT` each chunk, then
|
`/remote.php/dav/uploads/<userid>/`: `MKCOL` to create the upload folder, `PUT` each chunk, then
|
||||||
`MOVE` the `.file` pseudo-entry to the destination. Chunks are 5MB–5GB and named 1–10000.
|
`MOVE` the `.file` pseudo-entry to the destination. Chunks are 5MB–5GB and named 1–10000.
|
||||||
|
|||||||
+114
-1
@@ -366,7 +366,120 @@ wrong, so neither of the other two would send the user anywhere useful.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. What the abstraction does not yet cover
|
## 6. Virtual filesystems
|
||||||
|
|
||||||
|
A sync client in virtual-files mode leaves a **placeholder** where a file is
|
||||||
|
catalogued but not downloaded. On Linux — the only mode it supports — that
|
||||||
|
means `IMG.CR2` does not exist at all and `IMG.CR2.nextcloud` does, holding one
|
||||||
|
byte. ARCH §9.0 measured a real machine: 121,785 placeholders against 10,267
|
||||||
|
materialised files.
|
||||||
|
|
||||||
|
A folder library that ignores this is not merely degraded, it is dangerous.
|
||||||
|
Before the handling below existed, the folder connector catalogued every stub
|
||||||
|
as a 1-byte image, gave it an identity that changed the moment it was
|
||||||
|
downloaded, and — worst — reported a dehydrated *sidecar* as absent, which made
|
||||||
|
the sidecar writer create a fresh document over an existing one and discard
|
||||||
|
every edit another device had put there.
|
||||||
|
|
||||||
|
### 6.1 Three questions, one trait
|
||||||
|
|
||||||
|
Everything else about a synced folder is an ordinary directory, so this is not
|
||||||
|
a second connector. `dr_sync_folder::Vfs` asks only what differs:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub trait Vfs: Send + Sync {
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
fn is_placeholder(&self, on_disk: &str) -> bool;
|
||||||
|
fn real_name<'a>(&self, on_disk: &'a str) -> &'a str;
|
||||||
|
fn placeholder_name(&self, name: &str) -> Cow<'_, str>;
|
||||||
|
fn can_materialise(&self) -> bool; // defaulted false
|
||||||
|
fn materialise(&self, local: &Path) -> Result<(), RemoteError>; // defaulted
|
||||||
|
fn dematerialise(&self, local: &Path) -> Result<(), RemoteError>; // defaulted
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`NoVfs` for a plain directory; `dr_sync_nextcloud::NextcloudVfs` for a synced
|
||||||
|
one, wrapping the `DesktopClient` socket. A third convention is a third impl.
|
||||||
|
|
||||||
|
**Why not a `folder-vfs` provider.** The interesting capability is not a
|
||||||
|
property of the backend: the same directory can materialise on demand while the
|
||||||
|
client is running and cannot when it is down, so it must be computed per
|
||||||
|
connection either way. Registering two providers would ask the user to choose
|
||||||
|
between two things that differ by whether a background process is up. The
|
||||||
|
convention is detected instead, per connection, by a hook the registry supplies
|
||||||
|
(`FolderProvider::with_vfs_detector`) — which is what keeps `dr-sync-folder`
|
||||||
|
free of any client's protocol.
|
||||||
|
|
||||||
|
### 6.2 What the backend reports
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| `RemoteEntry::path` | the photograph's name, never the stub's — so identity survives a download |
|
||||||
|
| `RemoteEntry::materialised` | `false` on a stub; the catalog maps it to `Availability::Offline` |
|
||||||
|
| `RemoteEntry::size` | `0` on a stub, meaning *unknown* — see below |
|
||||||
|
| `get` on a stub | `RemoteError::NotMaterialised`, **never** `NotFound` and never the stub's one byte |
|
||||||
|
| `put` over a stub | refused; writing a rival file hands the client a conflict to resolve arbitrarily |
|
||||||
|
| `move_to` a stub | moves the stub and keeps it a stub — culling without downloading is ordinary |
|
||||||
|
| `delete` a stub | deletes it; a photograph is deleted whether or not its bytes are here |
|
||||||
|
| `capabilities().materialisation` | `OnDemand` with a client, `Placeholders` without, `Always` on a plain folder |
|
||||||
|
|
||||||
|
**Size is genuinely unknown.** A Linux suffix-mode stub is one byte and carries
|
||||||
|
no record of what it stands for. The client's `._sync_*.db` has the real size,
|
||||||
|
but that is a private schema and reading it would couple us to their migrations.
|
||||||
|
FR-NC-6c wants a transfer size quoted before an operation starts; for a stub the
|
||||||
|
honest answer is that it cannot be, and the interface should say so rather than
|
||||||
|
report one byte or invent an estimate silently.
|
||||||
|
|
||||||
|
### 6.3 Hydration is a borrow
|
||||||
|
|
||||||
|
The rule: **a file is returned to the state it was found in.** What a pass
|
||||||
|
downloaded is released; what the user already had is left alone. `BorrowPool`
|
||||||
|
enforces it.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let pool = BorrowPool::new();
|
||||||
|
{
|
||||||
|
let held = pool.borrow(&backend, &path).await?; // downloads only if absent
|
||||||
|
// ... read it, thumbnail it, index its faces ...
|
||||||
|
} // borrow ends
|
||||||
|
let stats = pool.release_all(&backend).await; // dehydrates only what it hydrated
|
||||||
|
```
|
||||||
|
|
||||||
|
Three properties that are not obvious:
|
||||||
|
|
||||||
|
- **Reference counted.** The thumbnail pass and the face pass meet on the same
|
||||||
|
RAW. Without counting, the first to finish dehydrates the file the second is
|
||||||
|
reading; with it, the transfer is paid once and released when the last
|
||||||
|
borrower is done.
|
||||||
|
- **Prior state is read before asking.** After `materialise` there is no way to
|
||||||
|
tell what the pass brought from what was already there, so it is recorded
|
||||||
|
first. Getting this wrong silently undoes a pin, and "my pinned trip
|
||||||
|
evaporated after an indexing run" is the failure that would make people stop
|
||||||
|
trusting the feature.
|
||||||
|
- **Being unsure is not symmetric.** `borrow_known(.., Some(true))` keeps a file
|
||||||
|
that might have been ours — costing disk. `Some(false)` releases one that
|
||||||
|
might have been the user's. An uncertain caller passes `true` or `None`,
|
||||||
|
never a guess at `false`.
|
||||||
|
|
||||||
|
A borrow against a plain folder or a server backend short-circuits and does
|
||||||
|
nothing, so a pass written for a VFS library runs unchanged everywhere rather
|
||||||
|
than growing two code paths.
|
||||||
|
|
||||||
|
### 6.4 Release means dehydrate, never delete
|
||||||
|
|
||||||
|
The single most dangerous thing in this feature. A synced folder is not a
|
||||||
|
cache: deleting a materialised file inside it propagates the deletion to the
|
||||||
|
server and removes the photograph from every device the user owns. `Vfs` and
|
||||||
|
`RemoteBackend::dematerialise` both say so, and an implementation that cannot
|
||||||
|
dehydrate returns `Unsupported` rather than approximating it.
|
||||||
|
|
||||||
|
This is also why the originals cache (`dr_catalog::cache`) cannot simply be
|
||||||
|
pointed at a VFS library: `Cache::release` deletes bytes, which is right for a
|
||||||
|
copy under `originals/` and catastrophic in place.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. What the abstraction does not yet cover
|
||||||
|
|
||||||
Stated so the next person does not have to rediscover it.
|
Stated so the next person does not have to rediscover it.
|
||||||
|
|
||||||
|
|||||||
+29
-28
@@ -9,17 +9,17 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
|
|||||||
|
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Source files scanned | 283 |
|
| Source files scanned | 286 |
|
||||||
| TRACES tags found | 823 |
|
| TRACES tags found | 834 |
|
||||||
| Requirements defined | 178 |
|
| Requirements defined | 179 |
|
||||||
| Requirements covered | 107 |
|
| Requirements covered | 107 |
|
||||||
| **Coverage** | **60.1%** (107/178) |
|
| **Coverage** | **59.8%** (107/179) |
|
||||||
|
|
||||||
### By type
|
### By type
|
||||||
|
|
||||||
| Type | Covered | Defined |
|
| Type | Covered | Defined |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| FR | 85 | 123 |
|
| FR | 85 | 124 |
|
||||||
| NFR | 20 | 49 |
|
| NFR | 20 | 49 |
|
||||||
| R | 2 | 6 |
|
| R | 2 | 6 |
|
||||||
|
|
||||||
@@ -35,26 +35,26 @@ _None._
|
|||||||
|---|---|
|
|---|---|
|
||||||
| FR-CAT-1 | [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:109`](../core/dr-catalog/src/walk.rs#L109), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-types/src/lib.rs:200`](../core/dr-types/src/lib.rs#L200), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302), [`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), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479), [`tools/traceability/src/lib.rs:511`](../tools/traceability/src/lib.rs#L511), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1) |
|
| FR-CAT-1 | [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:109`](../core/dr-catalog/src/walk.rs#L109), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-types/src/lib.rs:200`](../core/dr-types/src/lib.rs#L200), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302), [`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), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479), [`tools/traceability/src/lib.rs:511`](../tools/traceability/src/lib.rs#L511), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1) |
|
||||||
| FR-CAT-10 | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:586`](../platform/dr-plat/src/storage.rs#L586), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1123`](../ui/dr-ui/src/lib.rs#L1123), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1014`](../ui/dr-ui/ui/library.slint#L1014), [`ui/dr-ui/ui/library.slint:1176`](../ui/dr-ui/ui/library.slint#L1176), [`ui/dr-ui/ui/library.slint:863`](../ui/dr-ui/ui/library.slint#L863) |
|
| FR-CAT-10 | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`platform/dr-plat/src/storage.rs:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:586`](../platform/dr-plat/src/storage.rs#L586), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1123`](../ui/dr-ui/src/lib.rs#L1123), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1014`](../ui/dr-ui/ui/library.slint#L1014), [`ui/dr-ui/ui/library.slint:1176`](../ui/dr-ui/ui/library.slint#L1176), [`ui/dr-ui/ui/library.slint:863`](../ui/dr-ui/ui/library.slint#L863) |
|
||||||
| 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:1123`](../ui/dr-ui/src/lib.rs#L1123), [`ui/dr-ui/src/library.rs:162`](../ui/dr-ui/src/library.rs#L162), [`ui/dr-ui/src/library.rs:2229`](../ui/dr-ui/src/library.rs#L2229), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
| 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:1123`](../ui/dr-ui/src/lib.rs#L1123), [`ui/dr-ui/src/library.rs:162`](../ui/dr-ui/src/library.rs#L162), [`ui/dr-ui/src/library.rs:2257`](../ui/dr-ui/src/library.rs#L2257), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
||||||
| FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118) |
|
| FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118) |
|
||||||
| FR-CAT-13 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1) |
|
| FR-CAT-13 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1) |
|
||||||
| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:641`](../core/dr-catalog/src/schema.rs#L641), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:452`](../core/dr-sync-nextcloud/src/lib.rs#L452), [`core/dr-sync/src/lib.rs:133`](../core/dr-sync/src/lib.rs#L133), [`core/dr-sync/src/scan.rs:426`](../core/dr-sync/src/scan.rs#L426), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/collections_ui.rs:1219`](../ui/dr-ui/src/collections_ui.rs#L1219), [`ui/dr-ui/src/collections_ui.rs:1975`](../ui/dr-ui/src/collections_ui.rs#L1975), [`ui/dr-ui/src/library.rs:162`](../ui/dr-ui/src/library.rs#L162), [`ui/dr-ui/src/library.rs:179`](../ui/dr-ui/src/library.rs#L179), [`ui/dr-ui/src/library.rs:208`](../ui/dr-ui/src/library.rs#L208), [`ui/dr-ui/src/library.rs:3637`](../ui/dr-ui/src/library.rs#L3637), [`ui/dr-ui/src/library.rs:3671`](../ui/dr-ui/src/library.rs#L3671), [`ui/dr-ui/src/library_ui.rs:190`](../ui/dr-ui/src/library_ui.rs#L190), [`ui/dr-ui/src/library_ui.rs:756`](../ui/dr-ui/src/library_ui.rs#L756), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:572`](../ui/dr-ui/ui/collections.slint#L572) |
|
| FR-CAT-15 | [`core/dr-catalog/src/schema.rs:641`](../core/dr-catalog/src/schema.rs#L641), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:456`](../core/dr-sync-nextcloud/src/lib.rs#L456), [`core/dr-sync/src/lib.rs:135`](../core/dr-sync/src/lib.rs#L135), [`core/dr-sync/src/scan.rs:429`](../core/dr-sync/src/scan.rs#L429), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/collections_ui.rs:1219`](../ui/dr-ui/src/collections_ui.rs#L1219), [`ui/dr-ui/src/collections_ui.rs:1975`](../ui/dr-ui/src/collections_ui.rs#L1975), [`ui/dr-ui/src/library.rs:162`](../ui/dr-ui/src/library.rs#L162), [`ui/dr-ui/src/library.rs:179`](../ui/dr-ui/src/library.rs#L179), [`ui/dr-ui/src/library.rs:208`](../ui/dr-ui/src/library.rs#L208), [`ui/dr-ui/src/library.rs:3665`](../ui/dr-ui/src/library.rs#L3665), [`ui/dr-ui/src/library.rs:3699`](../ui/dr-ui/src/library.rs#L3699), [`ui/dr-ui/src/library_ui.rs:190`](../ui/dr-ui/src/library_ui.rs#L190), [`ui/dr-ui/src/library_ui.rs:756`](../ui/dr-ui/src/library_ui.rs#L756), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:572`](../ui/dr-ui/ui/collections.slint#L572) |
|
||||||
| FR-CAT-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53), [`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-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53), [`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), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) |
|
| 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), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) |
|
||||||
| 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:69`](../core/dr-sync/src/scan.rs#L69), [`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:2702`](../ui/dr-ui/src/library.rs#L2702), [`ui/dr-ui/src/library.rs:3172`](../ui/dr-ui/src/library.rs#L3172), [`ui/dr-ui/src/library_ui.rs:172`](../ui/dr-ui/src/library_ui.rs#L172), [`ui/dr-ui/src/library_ui.rs:3610`](../ui/dr-ui/src/library_ui.rs#L3610), [`ui/dr-ui/src/library_ui.rs:4575`](../ui/dr-ui/src/library_ui.rs#L4575), [`ui/dr-ui/ui/app.slint:319`](../ui/dr-ui/ui/app.slint#L319), [`ui/dr-ui/ui/settings.slint:372`](../ui/dr-ui/ui/settings.slint#L372), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) |
|
| 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:69`](../core/dr-sync/src/scan.rs#L69), [`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:2730`](../ui/dr-ui/src/library.rs#L2730), [`ui/dr-ui/src/library.rs:3200`](../ui/dr-ui/src/library.rs#L3200), [`ui/dr-ui/src/library_ui.rs:172`](../ui/dr-ui/src/library_ui.rs#L172), [`ui/dr-ui/src/library_ui.rs:3610`](../ui/dr-ui/src/library_ui.rs#L3610), [`ui/dr-ui/src/library_ui.rs:4575`](../ui/dr-ui/src/library_ui.rs#L4575), [`ui/dr-ui/ui/app.slint:319`](../ui/dr-ui/ui/app.slint#L319), [`ui/dr-ui/ui/settings.slint:372`](../ui/dr-ui/ui/settings.slint#L372), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) |
|
||||||
| FR-CAT-4 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/schema.rs:318`](../core/dr-catalog/src/schema.rs#L318), [`ui/dr-ui/src/library.rs:189`](../ui/dr-ui/src/library.rs#L189), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1) |
|
| FR-CAT-4 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/schema.rs:318`](../core/dr-catalog/src/schema.rs#L318), [`ui/dr-ui/src/library.rs:189`](../ui/dr-ui/src/library.rs#L189), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1) |
|
||||||
| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:1059`](../core/dr-catalog/src/schema.rs#L1059), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/collections_ui.rs:1558`](../ui/dr-ui/src/collections_ui.rs#L1558), [`ui/dr-ui/src/collections_ui.rs:1577`](../ui/dr-ui/src/collections_ui.rs#L1577), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3685`](../ui/dr-ui/src/library.rs#L3685), [`ui/dr-ui/src/library_ui.rs:631`](../ui/dr-ui/src/library_ui.rs#L631), [`ui/dr-ui/src/library_ui.rs:6372`](../ui/dr-ui/src/library_ui.rs#L6372), [`ui/dr-ui/src/library_ui.rs:6383`](../ui/dr-ui/src/library_ui.rs#L6383), [`ui/dr-ui/src/library_ui.rs:6396`](../ui/dr-ui/src/library_ui.rs#L6396), [`ui/dr-ui/src/library_ui.rs:6411`](../ui/dr-ui/src/library_ui.rs#L6411), [`ui/dr-ui/src/library_ui.rs:6420`](../ui/dr-ui/src/library_ui.rs#L6420), [`ui/dr-ui/ui/app.slint:264`](../ui/dr-ui/ui/app.slint#L264), [`ui/dr-ui/ui/app.slint:445`](../ui/dr-ui/ui/app.slint#L445), [`ui/dr-ui/ui/library.slint:1225`](../ui/dr-ui/ui/library.slint#L1225), [`ui/dr-ui/ui/library.slint:1228`](../ui/dr-ui/ui/library.slint#L1228), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:856`](../ui/dr-ui/ui/library.slint#L856), [`ui/dr-ui/ui/library.slint:908`](../ui/dr-ui/ui/library.slint#L908) |
|
| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:1059`](../core/dr-catalog/src/schema.rs#L1059), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/collections_ui.rs:1558`](../ui/dr-ui/src/collections_ui.rs#L1558), [`ui/dr-ui/src/collections_ui.rs:1577`](../ui/dr-ui/src/collections_ui.rs#L1577), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3713`](../ui/dr-ui/src/library.rs#L3713), [`ui/dr-ui/src/library_ui.rs:631`](../ui/dr-ui/src/library_ui.rs#L631), [`ui/dr-ui/src/library_ui.rs:6372`](../ui/dr-ui/src/library_ui.rs#L6372), [`ui/dr-ui/src/library_ui.rs:6383`](../ui/dr-ui/src/library_ui.rs#L6383), [`ui/dr-ui/src/library_ui.rs:6396`](../ui/dr-ui/src/library_ui.rs#L6396), [`ui/dr-ui/src/library_ui.rs:6411`](../ui/dr-ui/src/library_ui.rs#L6411), [`ui/dr-ui/src/library_ui.rs:6420`](../ui/dr-ui/src/library_ui.rs#L6420), [`ui/dr-ui/ui/app.slint:264`](../ui/dr-ui/ui/app.slint#L264), [`ui/dr-ui/ui/app.slint:445`](../ui/dr-ui/ui/app.slint#L445), [`ui/dr-ui/ui/library.slint:1225`](../ui/dr-ui/ui/library.slint#L1225), [`ui/dr-ui/ui/library.slint:1228`](../ui/dr-ui/ui/library.slint#L1228), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:856`](../ui/dr-ui/ui/library.slint#L856), [`ui/dr-ui/ui/library.slint:908`](../ui/dr-ui/ui/library.slint#L908) |
|
||||||
| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:213`](../ui/dr-ui/src/library.rs#L213), [`ui/dr-ui/src/library.rs:3931`](../ui/dr-ui/src/library.rs#L3931), [`ui/dr-ui/src/library_ui.rs:321`](../ui/dr-ui/src/library_ui.rs#L321), [`ui/dr-ui/src/library_ui.rs:393`](../ui/dr-ui/src/library_ui.rs#L393), [`ui/dr-ui/src/library_ui.rs:5186`](../ui/dr-ui/src/library_ui.rs#L5186), [`ui/dr-ui/src/library_ui.rs:5239`](../ui/dr-ui/src/library_ui.rs#L5239), [`ui/dr-ui/src/library_ui.rs:6512`](../ui/dr-ui/src/library_ui.rs#L6512), [`ui/dr-ui/ui/app.slint:267`](../ui/dr-ui/ui/app.slint#L267), [`ui/dr-ui/ui/app.slint:445`](../ui/dr-ui/ui/app.slint#L445), [`ui/dr-ui/ui/app.slint:690`](../ui/dr-ui/ui/app.slint#L690), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1301`](../ui/dr-ui/ui/library.slint#L1301), [`ui/dr-ui/ui/library.slint:908`](../ui/dr-ui/ui/library.slint#L908), [`ui/dr-ui/ui/settings.slint:147`](../ui/dr-ui/ui/settings.slint#L147) |
|
| FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:213`](../ui/dr-ui/src/library.rs#L213), [`ui/dr-ui/src/library.rs:3959`](../ui/dr-ui/src/library.rs#L3959), [`ui/dr-ui/src/library_ui.rs:321`](../ui/dr-ui/src/library_ui.rs#L321), [`ui/dr-ui/src/library_ui.rs:393`](../ui/dr-ui/src/library_ui.rs#L393), [`ui/dr-ui/src/library_ui.rs:5186`](../ui/dr-ui/src/library_ui.rs#L5186), [`ui/dr-ui/src/library_ui.rs:5239`](../ui/dr-ui/src/library_ui.rs#L5239), [`ui/dr-ui/src/library_ui.rs:6512`](../ui/dr-ui/src/library_ui.rs#L6512), [`ui/dr-ui/ui/app.slint:267`](../ui/dr-ui/ui/app.slint#L267), [`ui/dr-ui/ui/app.slint:445`](../ui/dr-ui/ui/app.slint#L445), [`ui/dr-ui/ui/app.slint:690`](../ui/dr-ui/ui/app.slint#L690), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1301`](../ui/dr-ui/ui/library.slint#L1301), [`ui/dr-ui/ui/library.slint:908`](../ui/dr-ui/ui/library.slint#L908), [`ui/dr-ui/ui/settings.slint:147`](../ui/dr-ui/ui/settings.slint#L147) |
|
||||||
| FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1673`](../ui/dr-ui/src/collections_ui.rs#L1673), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3472`](../ui/dr-ui/src/library_ui.rs#L3472), [`ui/dr-ui/src/library_ui.rs:4165`](../ui/dr-ui/src/library_ui.rs#L4165), [`ui/dr-ui/ui/app.slint:440`](../ui/dr-ui/ui/app.slint#L440), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2564`](../ui/dr-ui/ui/library.slint#L2564), [`ui/dr-ui/ui/library.slint:879`](../ui/dr-ui/ui/library.slint#L879), [`ui/dr-ui/ui/library.slint:898`](../ui/dr-ui/ui/library.slint#L898) |
|
| FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1673`](../ui/dr-ui/src/collections_ui.rs#L1673), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3472`](../ui/dr-ui/src/library_ui.rs#L3472), [`ui/dr-ui/src/library_ui.rs:4165`](../ui/dr-ui/src/library_ui.rs#L4165), [`ui/dr-ui/ui/app.slint:440`](../ui/dr-ui/ui/app.slint#L440), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2564`](../ui/dr-ui/ui/library.slint#L2564), [`ui/dr-ui/ui/library.slint:879`](../ui/dr-ui/ui/library.slint#L879), [`ui/dr-ui/ui/library.slint:898`](../ui/dr-ui/ui/library.slint#L898) |
|
||||||
| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:3374`](../ui/dr-ui/src/develop.rs#L3374), [`ui/dr-ui/src/export.rs:750`](../ui/dr-ui/src/export.rs#L750), [`ui/dr-ui/src/lib.rs:1376`](../ui/dr-ui/src/lib.rs#L1376), [`ui/dr-ui/src/lib.rs:1777`](../ui/dr-ui/src/lib.rs#L1777), [`ui/dr-ui/src/lib.rs:1912`](../ui/dr-ui/src/lib.rs#L1912), [`ui/dr-ui/src/lib.rs:501`](../ui/dr-ui/src/lib.rs#L501), [`ui/dr-ui/src/lib.rs:926`](../ui/dr-ui/src/lib.rs#L926), [`ui/dr-ui/src/library.rs:1641`](../ui/dr-ui/src/library.rs#L1641), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459), [`ui/dr-ui/src/library.rs:506`](../ui/dr-ui/src/library.rs#L506), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:790`](../ui/dr-ui/src/library.rs#L790), [`ui/dr-ui/src/library_ui.rs:4867`](../ui/dr-ui/src/library_ui.rs#L4867), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:3374`](../ui/dr-ui/src/develop.rs#L3374), [`ui/dr-ui/src/export.rs:750`](../ui/dr-ui/src/export.rs#L750), [`ui/dr-ui/src/lib.rs:1376`](../ui/dr-ui/src/lib.rs#L1376), [`ui/dr-ui/src/lib.rs:1777`](../ui/dr-ui/src/lib.rs#L1777), [`ui/dr-ui/src/lib.rs:1912`](../ui/dr-ui/src/lib.rs#L1912), [`ui/dr-ui/src/lib.rs:501`](../ui/dr-ui/src/lib.rs#L501), [`ui/dr-ui/src/lib.rs:926`](../ui/dr-ui/src/lib.rs#L926), [`ui/dr-ui/src/library.rs:1669`](../ui/dr-ui/src/library.rs#L1669), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459), [`ui/dr-ui/src/library.rs:506`](../ui/dr-ui/src/library.rs#L506), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:790`](../ui/dr-ui/src/library.rs#L790), [`ui/dr-ui/src/library_ui.rs:4867`](../ui/dr-ui/src/library_ui.rs#L4867), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||||
| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`ui/dr-ui/src/develop.rs:2870`](../ui/dr-ui/src/develop.rs#L2870), [`ui/dr-ui/src/library.rs:148`](../ui/dr-ui/src/library.rs#L148), [`ui/dr-ui/src/library.rs:1601`](../ui/dr-ui/src/library.rs#L1601), [`ui/dr-ui/src/library.rs:1677`](../ui/dr-ui/src/library.rs#L1677), [`ui/dr-ui/src/library.rs:233`](../ui/dr-ui/src/library.rs#L233), [`ui/dr-ui/src/library.rs:4038`](../ui/dr-ui/src/library.rs#L4038), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:774`](../ui/dr-ui/src/library.rs#L774), [`ui/dr-ui/src/library.rs:790`](../ui/dr-ui/src/library.rs#L790), [`ui/dr-ui/src/library.rs:844`](../ui/dr-ui/src/library.rs#L844), [`ui/dr-ui/src/library_ui.rs:1563`](../ui/dr-ui/src/library_ui.rs#L1563), [`ui/dr-ui/src/library_ui.rs:1589`](../ui/dr-ui/src/library_ui.rs#L1589), [`ui/dr-ui/src/library_ui.rs:1605`](../ui/dr-ui/src/library_ui.rs#L1605), [`ui/dr-ui/src/library_ui.rs:1699`](../ui/dr-ui/src/library_ui.rs#L1699), [`ui/dr-ui/src/library_ui.rs:2315`](../ui/dr-ui/src/library_ui.rs#L2315), [`ui/dr-ui/src/library_ui.rs:232`](../ui/dr-ui/src/library_ui.rs#L232), [`ui/dr-ui/src/library_ui.rs:265`](../ui/dr-ui/src/library_ui.rs#L265), [`ui/dr-ui/src/library_ui.rs:2753`](../ui/dr-ui/src/library_ui.rs#L2753), [`ui/dr-ui/src/library_ui.rs:2975`](../ui/dr-ui/src/library_ui.rs#L2975), [`ui/dr-ui/src/library_ui.rs:3253`](../ui/dr-ui/src/library_ui.rs#L3253), [`ui/dr-ui/src/library_ui.rs:3341`](../ui/dr-ui/src/library_ui.rs#L3341), [`ui/dr-ui/src/library_ui.rs:3523`](../ui/dr-ui/src/library_ui.rs#L3523), [`ui/dr-ui/src/library_ui.rs:3637`](../ui/dr-ui/src/library_ui.rs#L3637), [`ui/dr-ui/src/library_ui.rs:446`](../ui/dr-ui/src/library_ui.rs#L446), [`ui/dr-ui/src/library_ui.rs:504`](../ui/dr-ui/src/library_ui.rs#L504), [`ui/dr-ui/src/library_ui.rs:5284`](../ui/dr-ui/src/library_ui.rs#L5284), [`ui/dr-ui/src/library_ui.rs:5399`](../ui/dr-ui/src/library_ui.rs#L5399), [`ui/dr-ui/src/presets.rs:326`](../ui/dr-ui/src/presets.rs#L326), [`ui/dr-ui/src/presets.rs:338`](../ui/dr-ui/src/presets.rs#L338), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`ui/dr-ui/src/develop.rs:2870`](../ui/dr-ui/src/develop.rs#L2870), [`ui/dr-ui/src/library.rs:148`](../ui/dr-ui/src/library.rs#L148), [`ui/dr-ui/src/library.rs:1629`](../ui/dr-ui/src/library.rs#L1629), [`ui/dr-ui/src/library.rs:1705`](../ui/dr-ui/src/library.rs#L1705), [`ui/dr-ui/src/library.rs:233`](../ui/dr-ui/src/library.rs#L233), [`ui/dr-ui/src/library.rs:4066`](../ui/dr-ui/src/library.rs#L4066), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:774`](../ui/dr-ui/src/library.rs#L774), [`ui/dr-ui/src/library.rs:790`](../ui/dr-ui/src/library.rs#L790), [`ui/dr-ui/src/library.rs:844`](../ui/dr-ui/src/library.rs#L844), [`ui/dr-ui/src/library_ui.rs:1563`](../ui/dr-ui/src/library_ui.rs#L1563), [`ui/dr-ui/src/library_ui.rs:1589`](../ui/dr-ui/src/library_ui.rs#L1589), [`ui/dr-ui/src/library_ui.rs:1605`](../ui/dr-ui/src/library_ui.rs#L1605), [`ui/dr-ui/src/library_ui.rs:1699`](../ui/dr-ui/src/library_ui.rs#L1699), [`ui/dr-ui/src/library_ui.rs:2315`](../ui/dr-ui/src/library_ui.rs#L2315), [`ui/dr-ui/src/library_ui.rs:232`](../ui/dr-ui/src/library_ui.rs#L232), [`ui/dr-ui/src/library_ui.rs:265`](../ui/dr-ui/src/library_ui.rs#L265), [`ui/dr-ui/src/library_ui.rs:2753`](../ui/dr-ui/src/library_ui.rs#L2753), [`ui/dr-ui/src/library_ui.rs:2975`](../ui/dr-ui/src/library_ui.rs#L2975), [`ui/dr-ui/src/library_ui.rs:3253`](../ui/dr-ui/src/library_ui.rs#L3253), [`ui/dr-ui/src/library_ui.rs:3341`](../ui/dr-ui/src/library_ui.rs#L3341), [`ui/dr-ui/src/library_ui.rs:3523`](../ui/dr-ui/src/library_ui.rs#L3523), [`ui/dr-ui/src/library_ui.rs:3637`](../ui/dr-ui/src/library_ui.rs#L3637), [`ui/dr-ui/src/library_ui.rs:446`](../ui/dr-ui/src/library_ui.rs#L446), [`ui/dr-ui/src/library_ui.rs:504`](../ui/dr-ui/src/library_ui.rs#L504), [`ui/dr-ui/src/library_ui.rs:5284`](../ui/dr-ui/src/library_ui.rs#L5284), [`ui/dr-ui/src/library_ui.rs:5399`](../ui/dr-ui/src/library_ui.rs#L5399), [`ui/dr-ui/src/presets.rs:326`](../ui/dr-ui/src/presets.rs#L326), [`ui/dr-ui/src/presets.rs:338`](../ui/dr-ui/src/presets.rs#L338), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
||||||
| FR-CULL-1 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) |
|
| FR-CULL-1 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) |
|
||||||
| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:357`](../core/dr-catalog/src/schema.rs#L357), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`ui/dr-ui/src/develop.rs:119`](../ui/dr-ui/src/develop.rs#L119), [`ui/dr-ui/src/develop.rs:128`](../ui/dr-ui/src/develop.rs#L128), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:194`](../ui/dr-ui/src/develop.rs#L194), [`ui/dr-ui/src/develop.rs:589`](../ui/dr-ui/src/develop.rs#L589), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1884`](../ui/dr-ui/src/lib.rs#L1884), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:357`](../core/dr-catalog/src/schema.rs#L357), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`ui/dr-ui/src/develop.rs:119`](../ui/dr-ui/src/develop.rs#L119), [`ui/dr-ui/src/develop.rs:128`](../ui/dr-ui/src/develop.rs#L128), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:194`](../ui/dr-ui/src/develop.rs#L194), [`ui/dr-ui/src/develop.rs:589`](../ui/dr-ui/src/develop.rs#L589), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1884`](../ui/dr-ui/src/lib.rs#L1884), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
||||||
| FR-CULL-11 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/library.rs:254`](../ui/dr-ui/src/library.rs#L254), [`ui/dr-ui/src/library.rs:284`](../ui/dr-ui/src/library.rs#L284), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
| FR-CULL-11 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/library.rs:254`](../ui/dr-ui/src/library.rs#L254), [`ui/dr-ui/src/library.rs:284`](../ui/dr-ui/src/library.rs#L284), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
||||||
| FR-CULL-12 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:357`](../core/dr-catalog/src/schema.rs#L357), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
| FR-CULL-12 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:357`](../core/dr-catalog/src/schema.rs#L357), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
||||||
| FR-CULL-2 | [`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), [`ui/dr-ui/src/import.rs:463`](../ui/dr-ui/src/import.rs#L463) |
|
| FR-CULL-2 | [`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), [`ui/dr-ui/src/import.rs:463`](../ui/dr-ui/src/import.rs#L463) |
|
||||||
| FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/library.rs:213`](../ui/dr-ui/src/library.rs#L213), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459) |
|
| FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`ui/dr-ui/src/library.rs:213`](../ui/dr-ui/src/library.rs#L213), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459) |
|
||||||
| FR-CULL-8 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:401`](../core/dr-catalog/src/schema.rs#L401), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/library.rs:2703`](../ui/dr-ui/src/library.rs#L2703), [`ui/dr-ui/src/library.rs:2807`](../ui/dr-ui/src/library.rs#L2807), [`ui/dr-ui/ui/settings.slint:404`](../ui/dr-ui/ui/settings.slint#L404), [`ui/dr-ui/ui/settings.slint:81`](../ui/dr-ui/ui/settings.slint#L81) |
|
| FR-CULL-8 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:401`](../core/dr-catalog/src/schema.rs#L401), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/library.rs:2731`](../ui/dr-ui/src/library.rs#L2731), [`ui/dr-ui/src/library.rs:2835`](../ui/dr-ui/src/library.rs#L2835), [`ui/dr-ui/ui/settings.slint:404`](../ui/dr-ui/ui/settings.slint#L404), [`ui/dr-ui/ui/settings.slint:81`](../ui/dr-ui/ui/settings.slint#L81) |
|
||||||
| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) |
|
| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`core/dr-face/src/neighbours.rs:1`](../core/dr-face/src/neighbours.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) |
|
||||||
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389) |
|
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389) |
|
||||||
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/framing.rs:191`](../core/dr-pipeline/src/framing.rs#L191), [`core/dr-pipeline/src/framing.rs:365`](../core/dr-pipeline/src/framing.rs#L365), [`core/dr-pipeline/src/framing.rs:620`](../core/dr-pipeline/src/framing.rs#L620), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/mask.rs:121`](../core/dr-pipeline/src/mask.rs#L121), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:516`](../core/dr-pipeline/src/operation.rs#L516), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:210`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L210), [`core/dr-pipeline/src/ops/curve.rs:100`](../core/dr-pipeline/src/ops/curve.rs#L100), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:219`](../core/dr-pipeline/src/ops/curve.rs#L219), [`core/dr-pipeline/src/ops/curve.rs:635`](../core/dr-pipeline/src/ops/curve.rs#L635), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:273`](../core/dr-pipeline/src/ops/noise_reduction.rs#L273), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:1696`](../core/dr-pipeline/src/sidecar.rs#L1696), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1297`](../ui/dr-ui/src/develop.rs#L1297), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1775`](../ui/dr-ui/src/develop.rs#L1775), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:1807`](../ui/dr-ui/src/develop.rs#L1807), [`ui/dr-ui/src/develop.rs:1829`](../ui/dr-ui/src/develop.rs#L1829), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/develop.rs:2073`](../ui/dr-ui/src/develop.rs#L2073), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3909`](../ui/dr-ui/src/develop.rs#L3909), [`ui/dr-ui/src/develop.rs:3963`](../ui/dr-ui/src/develop.rs#L3963), [`ui/dr-ui/src/develop.rs:4007`](../ui/dr-ui/src/develop.rs#L4007), [`ui/dr-ui/src/develop.rs:4057`](../ui/dr-ui/src/develop.rs#L4057), [`ui/dr-ui/src/develop.rs:628`](../ui/dr-ui/src/develop.rs#L628), [`ui/dr-ui/src/develop.rs:675`](../ui/dr-ui/src/develop.rs#L675), [`ui/dr-ui/src/lib.rs:1461`](../ui/dr-ui/src/lib.rs#L1461), [`ui/dr-ui/src/lib.rs:2162`](../ui/dr-ui/src/lib.rs#L2162), [`ui/dr-ui/src/lib.rs:319`](../ui/dr-ui/src/lib.rs#L319), [`ui/dr-ui/src/library.rs:506`](../ui/dr-ui/src/library.rs#L506), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/app.slint:1766`](../ui/dr-ui/ui/app.slint#L1766), [`ui/dr-ui/ui/app.slint:793`](../ui/dr-ui/ui/app.slint#L793), [`ui/dr-ui/ui/masks.slint:490`](../ui/dr-ui/ui/masks.slint#L490) |
|
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/framing.rs:191`](../core/dr-pipeline/src/framing.rs#L191), [`core/dr-pipeline/src/framing.rs:365`](../core/dr-pipeline/src/framing.rs#L365), [`core/dr-pipeline/src/framing.rs:620`](../core/dr-pipeline/src/framing.rs#L620), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/mask.rs:121`](../core/dr-pipeline/src/mask.rs#L121), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:516`](../core/dr-pipeline/src/operation.rs#L516), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:210`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L210), [`core/dr-pipeline/src/ops/curve.rs:100`](../core/dr-pipeline/src/ops/curve.rs#L100), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:219`](../core/dr-pipeline/src/ops/curve.rs#L219), [`core/dr-pipeline/src/ops/curve.rs:635`](../core/dr-pipeline/src/ops/curve.rs#L635), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:273`](../core/dr-pipeline/src/ops/noise_reduction.rs#L273), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:1696`](../core/dr-pipeline/src/sidecar.rs#L1696), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1297`](../ui/dr-ui/src/develop.rs#L1297), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1775`](../ui/dr-ui/src/develop.rs#L1775), [`ui/dr-ui/src/develop.rs:1793`](../ui/dr-ui/src/develop.rs#L1793), [`ui/dr-ui/src/develop.rs:1807`](../ui/dr-ui/src/develop.rs#L1807), [`ui/dr-ui/src/develop.rs:1829`](../ui/dr-ui/src/develop.rs#L1829), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/develop.rs:2073`](../ui/dr-ui/src/develop.rs#L2073), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3345`](../ui/dr-ui/src/develop.rs#L3345), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3909`](../ui/dr-ui/src/develop.rs#L3909), [`ui/dr-ui/src/develop.rs:3963`](../ui/dr-ui/src/develop.rs#L3963), [`ui/dr-ui/src/develop.rs:4007`](../ui/dr-ui/src/develop.rs#L4007), [`ui/dr-ui/src/develop.rs:4057`](../ui/dr-ui/src/develop.rs#L4057), [`ui/dr-ui/src/develop.rs:628`](../ui/dr-ui/src/develop.rs#L628), [`ui/dr-ui/src/develop.rs:675`](../ui/dr-ui/src/develop.rs#L675), [`ui/dr-ui/src/lib.rs:1461`](../ui/dr-ui/src/lib.rs#L1461), [`ui/dr-ui/src/lib.rs:2162`](../ui/dr-ui/src/lib.rs#L2162), [`ui/dr-ui/src/lib.rs:319`](../ui/dr-ui/src/lib.rs#L319), [`ui/dr-ui/src/library.rs:506`](../ui/dr-ui/src/library.rs#L506), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/app.slint:1766`](../ui/dr-ui/ui/app.slint#L1766), [`ui/dr-ui/ui/app.slint:793`](../ui/dr-ui/ui/app.slint#L793), [`ui/dr-ui/ui/masks.slint:490`](../ui/dr-ui/ui/masks.slint#L490) |
|
||||||
@@ -67,7 +67,7 @@ _None._
|
|||||||
| FR-DEV-3h | [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:205`](../core/dr-pipeline/src/framing.rs#L205), [`core/dr-pipeline/src/framing.rs:365`](../core/dr-pipeline/src/framing.rs#L365), [`core/dr-pipeline/src/framing.rs:927`](../core/dr-pipeline/src/framing.rs#L927), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336), [`core/dr-types/src/lib.rs:444`](../core/dr-types/src/lib.rs#L444), [`core/dr-types/src/lib.rs:456`](../core/dr-types/src/lib.rs#L456), [`core/dr-types/src/lib.rs:472`](../core/dr-types/src/lib.rs#L472), [`ui/dr-ui/src/develop.rs:138`](../ui/dr-ui/src/develop.rs#L138), [`ui/dr-ui/src/develop.rs:1992`](../ui/dr-ui/src/develop.rs#L1992), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) |
|
| FR-DEV-3h | [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:205`](../core/dr-pipeline/src/framing.rs#L205), [`core/dr-pipeline/src/framing.rs:365`](../core/dr-pipeline/src/framing.rs#L365), [`core/dr-pipeline/src/framing.rs:927`](../core/dr-pipeline/src/framing.rs#L927), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336), [`core/dr-types/src/lib.rs:444`](../core/dr-types/src/lib.rs#L444), [`core/dr-types/src/lib.rs:456`](../core/dr-types/src/lib.rs#L456), [`core/dr-types/src/lib.rs:472`](../core/dr-types/src/lib.rs#L472), [`ui/dr-ui/src/develop.rs:138`](../ui/dr-ui/src/develop.rs#L138), [`ui/dr-ui/src/develop.rs:1992`](../ui/dr-ui/src/develop.rs#L1992), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) |
|
||||||
| FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217), [`ui/dr-ui/ui/crop.slint:1`](../ui/dr-ui/ui/crop.slint#L1) |
|
| FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217), [`ui/dr-ui/ui/crop.slint:1`](../ui/dr-ui/ui/crop.slint#L1) |
|
||||||
| FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:2914`](../ui/dr-ui/src/develop.rs#L2914), [`ui/dr-ui/src/develop.rs:3397`](../ui/dr-ui/src/develop.rs#L3397), [`ui/dr-ui/src/develop.rs:3427`](../ui/dr-ui/src/develop.rs#L3427), [`ui/dr-ui/src/develop.rs:3440`](../ui/dr-ui/src/develop.rs#L3440), [`ui/dr-ui/src/develop.rs:3452`](../ui/dr-ui/src/develop.rs#L3452), [`ui/dr-ui/src/develop.rs:3468`](../ui/dr-ui/src/develop.rs#L3468), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/develop.rs:3504`](../ui/dr-ui/src/develop.rs#L3504), [`ui/dr-ui/src/develop.rs:3523`](../ui/dr-ui/src/develop.rs#L3523), [`ui/dr-ui/src/develop.rs:3539`](../ui/dr-ui/src/develop.rs#L3539), [`ui/dr-ui/src/develop.rs:610`](../ui/dr-ui/src/develop.rs#L610), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:215`](../ui/dr-ui/src/labels.rs#L215), [`ui/dr-ui/src/lib.rs:1409`](../ui/dr-ui/src/lib.rs#L1409), [`ui/dr-ui/src/lib.rs:1439`](../ui/dr-ui/src/lib.rs#L1439), [`ui/dr-ui/src/lib.rs:1447`](../ui/dr-ui/src/lib.rs#L1447), [`ui/dr-ui/src/lib.rs:2332`](../ui/dr-ui/src/lib.rs#L2332), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
|
| FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:2914`](../ui/dr-ui/src/develop.rs#L2914), [`ui/dr-ui/src/develop.rs:3397`](../ui/dr-ui/src/develop.rs#L3397), [`ui/dr-ui/src/develop.rs:3427`](../ui/dr-ui/src/develop.rs#L3427), [`ui/dr-ui/src/develop.rs:3440`](../ui/dr-ui/src/develop.rs#L3440), [`ui/dr-ui/src/develop.rs:3452`](../ui/dr-ui/src/develop.rs#L3452), [`ui/dr-ui/src/develop.rs:3468`](../ui/dr-ui/src/develop.rs#L3468), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/develop.rs:3504`](../ui/dr-ui/src/develop.rs#L3504), [`ui/dr-ui/src/develop.rs:3523`](../ui/dr-ui/src/develop.rs#L3523), [`ui/dr-ui/src/develop.rs:3539`](../ui/dr-ui/src/develop.rs#L3539), [`ui/dr-ui/src/develop.rs:610`](../ui/dr-ui/src/develop.rs#L610), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:215`](../ui/dr-ui/src/labels.rs#L215), [`ui/dr-ui/src/lib.rs:1409`](../ui/dr-ui/src/lib.rs#L1409), [`ui/dr-ui/src/lib.rs:1439`](../ui/dr-ui/src/lib.rs#L1439), [`ui/dr-ui/src/lib.rs:1447`](../ui/dr-ui/src/lib.rs#L1447), [`ui/dr-ui/src/lib.rs:2332`](../ui/dr-ui/src/lib.rs#L2332), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
|
||||||
| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/develop.rs:3360`](../ui/dr-ui/src/develop.rs#L3360), [`ui/dr-ui/src/lib.rs:1376`](../ui/dr-ui/src/lib.rs#L1376), [`ui/dr-ui/src/library.rs:1641`](../ui/dr-ui/src/library.rs#L1641), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459), [`ui/dr-ui/src/library.rs:487`](../ui/dr-ui/src/library.rs#L487), [`ui/dr-ui/src/library_ui.rs:2575`](../ui/dr-ui/src/library_ui.rs#L2575), [`ui/dr-ui/src/library_ui.rs:2975`](../ui/dr-ui/src/library_ui.rs#L2975), [`ui/dr-ui/src/library_ui.rs:472`](../ui/dr-ui/src/library_ui.rs#L472), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:549`](../ui/dr-ui/src/settings_ui.rs#L549), [`ui/dr-ui/ui/adjust.slint:613`](../ui/dr-ui/ui/adjust.slint#L613), [`ui/dr-ui/ui/library.slint:1328`](../ui/dr-ui/ui/library.slint#L1328), [`ui/dr-ui/ui/library.slint:837`](../ui/dr-ui/ui/library.slint#L837), [`ui/dr-ui/ui/library.slint:919`](../ui/dr-ui/ui/library.slint#L919), [`ui/dr-ui/ui/settings.slint:100`](../ui/dr-ui/ui/settings.slint#L100) |
|
| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/develop.rs:3360`](../ui/dr-ui/src/develop.rs#L3360), [`ui/dr-ui/src/lib.rs:1376`](../ui/dr-ui/src/lib.rs#L1376), [`ui/dr-ui/src/library.rs:1669`](../ui/dr-ui/src/library.rs#L1669), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459), [`ui/dr-ui/src/library.rs:487`](../ui/dr-ui/src/library.rs#L487), [`ui/dr-ui/src/library_ui.rs:2575`](../ui/dr-ui/src/library_ui.rs#L2575), [`ui/dr-ui/src/library_ui.rs:2975`](../ui/dr-ui/src/library_ui.rs#L2975), [`ui/dr-ui/src/library_ui.rs:472`](../ui/dr-ui/src/library_ui.rs#L472), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:549`](../ui/dr-ui/src/settings_ui.rs#L549), [`ui/dr-ui/ui/adjust.slint:613`](../ui/dr-ui/ui/adjust.slint#L613), [`ui/dr-ui/ui/library.slint:1328`](../ui/dr-ui/ui/library.slint#L1328), [`ui/dr-ui/ui/library.slint:837`](../ui/dr-ui/ui/library.slint#L837), [`ui/dr-ui/ui/library.slint:919`](../ui/dr-ui/ui/library.slint#L919), [`ui/dr-ui/ui/settings.slint:100`](../ui/dr-ui/ui/settings.slint#L100) |
|
||||||
| FR-DEV-7 | [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`ui/dr-ui/src/develop.rs:3468`](../ui/dr-ui/src/develop.rs#L3468), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/lib.rs:1447`](../ui/dr-ui/src/lib.rs#L1447), [`ui/dr-ui/src/lib.rs:2332`](../ui/dr-ui/src/lib.rs#L2332), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
|
| FR-DEV-7 | [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`ui/dr-ui/src/develop.rs:3468`](../ui/dr-ui/src/develop.rs#L3468), [`ui/dr-ui/src/develop.rs:3500`](../ui/dr-ui/src/develop.rs#L3500), [`ui/dr-ui/src/lib.rs:1447`](../ui/dr-ui/src/lib.rs#L1447), [`ui/dr-ui/src/lib.rs:2332`](../ui/dr-ui/src/lib.rs#L2332), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
|
||||||
| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:252`](../core/dr-gpu/src/detail.rs#L252), [`core/dr-gpu/src/detail.rs:434`](../core/dr-gpu/src/detail.rs#L434), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:363`](../core/dr-pipeline/src/detail.rs#L363), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:422`](../core/dr-pipeline/src/detail.rs#L422), [`core/dr-pipeline/src/detail.rs:496`](../core/dr-pipeline/src/detail.rs#L496), [`core/dr-pipeline/src/graph.rs:113`](../core/dr-pipeline/src/graph.rs#L113), [`core/dr-pipeline/src/graph.rs:191`](../core/dr-pipeline/src/graph.rs#L191), [`core/dr-pipeline/src/graph.rs:685`](../core/dr-pipeline/src/graph.rs#L685), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:554`](../core/dr-pipeline/src/operation.rs#L554), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:672`](../core/dr-pipeline/src/sidecar.rs#L672), [`core/dr-pipeline/src/sidecar.rs:808`](../core/dr-pipeline/src/sidecar.rs#L808), [`core/dr-pipeline/src/sidecar.rs:862`](../core/dr-pipeline/src/sidecar.rs#L862), [`core/dr-pipeline/src/sidecar.rs:892`](../core/dr-pipeline/src/sidecar.rs#L892), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2144`](../ui/dr-ui/src/develop.rs#L2144), [`ui/dr-ui/src/develop.rs:2182`](../ui/dr-ui/src/develop.rs#L2182), [`ui/dr-ui/src/develop.rs:2247`](../ui/dr-ui/src/develop.rs#L2247), [`ui/dr-ui/src/develop.rs:2330`](../ui/dr-ui/src/develop.rs#L2330), [`ui/dr-ui/src/develop.rs:2344`](../ui/dr-ui/src/develop.rs#L2344), [`ui/dr-ui/src/develop.rs:658`](../ui/dr-ui/src/develop.rs#L658), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1468`](../ui/dr-ui/src/lib.rs#L1468), [`ui/dr-ui/src/lib.rs:2429`](../ui/dr-ui/src/lib.rs#L2429), [`ui/dr-ui/src/lib.rs:324`](../ui/dr-ui/src/lib.rs#L324), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/src/spots_ui.rs:1`](../ui/dr-ui/src/spots_ui.rs#L1), [`ui/dr-ui/src/spots_ui.rs:265`](../ui/dr-ui/src/spots_ui.rs#L265), [`ui/dr-ui/ui/adjust.slint:700`](../ui/dr-ui/ui/adjust.slint#L700), [`ui/dr-ui/ui/app.slint:108`](../ui/dr-ui/ui/app.slint#L108), [`ui/dr-ui/ui/app.slint:1671`](../ui/dr-ui/ui/app.slint#L1671), [`ui/dr-ui/ui/app.slint:1844`](../ui/dr-ui/ui/app.slint#L1844), [`ui/dr-ui/ui/app.slint:2162`](../ui/dr-ui/ui/app.slint#L2162), [`ui/dr-ui/ui/spots.slint:180`](../ui/dr-ui/ui/spots.slint#L180), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) |
|
| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:252`](../core/dr-gpu/src/detail.rs#L252), [`core/dr-gpu/src/detail.rs:434`](../core/dr-gpu/src/detail.rs#L434), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:363`](../core/dr-pipeline/src/detail.rs#L363), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:422`](../core/dr-pipeline/src/detail.rs#L422), [`core/dr-pipeline/src/detail.rs:496`](../core/dr-pipeline/src/detail.rs#L496), [`core/dr-pipeline/src/graph.rs:113`](../core/dr-pipeline/src/graph.rs#L113), [`core/dr-pipeline/src/graph.rs:191`](../core/dr-pipeline/src/graph.rs#L191), [`core/dr-pipeline/src/graph.rs:685`](../core/dr-pipeline/src/graph.rs#L685), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:554`](../core/dr-pipeline/src/operation.rs#L554), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:672`](../core/dr-pipeline/src/sidecar.rs#L672), [`core/dr-pipeline/src/sidecar.rs:808`](../core/dr-pipeline/src/sidecar.rs#L808), [`core/dr-pipeline/src/sidecar.rs:862`](../core/dr-pipeline/src/sidecar.rs#L862), [`core/dr-pipeline/src/sidecar.rs:892`](../core/dr-pipeline/src/sidecar.rs#L892), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2144`](../ui/dr-ui/src/develop.rs#L2144), [`ui/dr-ui/src/develop.rs:2182`](../ui/dr-ui/src/develop.rs#L2182), [`ui/dr-ui/src/develop.rs:2247`](../ui/dr-ui/src/develop.rs#L2247), [`ui/dr-ui/src/develop.rs:2330`](../ui/dr-ui/src/develop.rs#L2330), [`ui/dr-ui/src/develop.rs:2344`](../ui/dr-ui/src/develop.rs#L2344), [`ui/dr-ui/src/develop.rs:658`](../ui/dr-ui/src/develop.rs#L658), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1468`](../ui/dr-ui/src/lib.rs#L1468), [`ui/dr-ui/src/lib.rs:2429`](../ui/dr-ui/src/lib.rs#L2429), [`ui/dr-ui/src/lib.rs:324`](../ui/dr-ui/src/lib.rs#L324), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/src/spots_ui.rs:1`](../ui/dr-ui/src/spots_ui.rs#L1), [`ui/dr-ui/src/spots_ui.rs:265`](../ui/dr-ui/src/spots_ui.rs#L265), [`ui/dr-ui/ui/adjust.slint:700`](../ui/dr-ui/ui/adjust.slint#L700), [`ui/dr-ui/ui/app.slint:108`](../ui/dr-ui/ui/app.slint#L108), [`ui/dr-ui/ui/app.slint:1671`](../ui/dr-ui/ui/app.slint#L1671), [`ui/dr-ui/ui/app.slint:1844`](../ui/dr-ui/ui/app.slint#L1844), [`ui/dr-ui/ui/app.slint:2162`](../ui/dr-ui/ui/app.slint#L2162), [`ui/dr-ui/ui/spots.slint:180`](../ui/dr-ui/ui/spots.slint#L180), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) |
|
||||||
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2088`](../core/dr-gpu/src/adjust.rs#L2088), [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:2250`](../core/dr-gpu/src/adjust.rs#L2250), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:263`](../core/dr-gpu/tests/local_contrast.rs#L263), [`core/dr-gpu/tests/noise_reduction.rs:378`](../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/graph.rs:547`](../core/dr-pipeline/src/graph.rs#L547), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:657`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L657), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:672`](../core/dr-pipeline/src/ops/local_contrast.rs#L672), [`core/dr-pipeline/src/ops/noise_reduction.rs:698`](../core/dr-pipeline/src/ops/noise_reduction.rs#L698), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:2668`](../ui/dr-ui/src/develop.rs#L2668), [`ui/dr-ui/src/develop.rs:3698`](../ui/dr-ui/src/develop.rs#L3698), [`ui/dr-ui/src/develop.rs:4181`](../ui/dr-ui/src/develop.rs#L4181), [`ui/dr-ui/src/develop.rs:4215`](../ui/dr-ui/src/develop.rs#L4215), [`ui/dr-ui/src/lib.rs:72`](../ui/dr-ui/src/lib.rs#L72), [`ui/dr-ui/src/lib.rs:757`](../ui/dr-ui/src/lib.rs#L757), [`ui/dr-ui/src/lib.rs:816`](../ui/dr-ui/src/lib.rs#L816) |
|
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2088`](../core/dr-gpu/src/adjust.rs#L2088), [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:2250`](../core/dr-gpu/src/adjust.rs#L2250), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:263`](../core/dr-gpu/tests/local_contrast.rs#L263), [`core/dr-gpu/tests/noise_reduction.rs:378`](../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/graph.rs:547`](../core/dr-pipeline/src/graph.rs#L547), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:657`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L657), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:672`](../core/dr-pipeline/src/ops/local_contrast.rs#L672), [`core/dr-pipeline/src/ops/noise_reduction.rs:698`](../core/dr-pipeline/src/ops/noise_reduction.rs#L698), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:2668`](../ui/dr-ui/src/develop.rs#L2668), [`ui/dr-ui/src/develop.rs:3698`](../ui/dr-ui/src/develop.rs#L3698), [`ui/dr-ui/src/develop.rs:4181`](../ui/dr-ui/src/develop.rs#L4181), [`ui/dr-ui/src/develop.rs:4215`](../ui/dr-ui/src/develop.rs#L4215), [`ui/dr-ui/src/lib.rs:72`](../ui/dr-ui/src/lib.rs#L72), [`ui/dr-ui/src/lib.rs:757`](../ui/dr-ui/src/lib.rs#L757), [`ui/dr-ui/src/lib.rs:816`](../ui/dr-ui/src/lib.rs#L816) |
|
||||||
@@ -86,22 +86,22 @@ _None._
|
|||||||
| FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:652`](../core/dr-types/src/lib.rs#L652), [`core/dr-types/src/settings.rs:313`](../core/dr-types/src/settings.rs#L313), [`ui/dr-ui/src/export.rs:619`](../ui/dr-ui/src/export.rs#L619), [`ui/dr-ui/src/export.rs:647`](../ui/dr-ui/src/export.rs#L647), [`ui/dr-ui/src/export.rs:777`](../ui/dr-ui/src/export.rs#L777), [`ui/dr-ui/src/export.rs:794`](../ui/dr-ui/src/export.rs#L794), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
| FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:652`](../core/dr-types/src/lib.rs#L652), [`core/dr-types/src/settings.rs:313`](../core/dr-types/src/settings.rs#L313), [`ui/dr-ui/src/export.rs:619`](../ui/dr-ui/src/export.rs#L619), [`ui/dr-ui/src/export.rs:647`](../ui/dr-ui/src/export.rs#L647), [`ui/dr-ui/src/export.rs:777`](../ui/dr-ui/src/export.rs#L777), [`ui/dr-ui/src/export.rs:794`](../ui/dr-ui/src/export.rs#L794), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||||
| 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:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:2829`](../ui/dr-ui/src/develop.rs#L2829), [`ui/dr-ui/src/lib.rs:362`](../ui/dr-ui/src/lib.rs#L362) |
|
| 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:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:2829`](../ui/dr-ui/src/develop.rs#L2829), [`ui/dr-ui/src/lib.rs:362`](../ui/dr-ui/src/lib.rs#L362) |
|
||||||
| 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:355`](../core/dr-sync/src/account.rs#L355), [`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-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:355`](../core/dr-sync/src/account.rs#L355), [`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:220`](../core/dr-sync/src/account.rs#L220), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:424`](../ui/dr-ui/src/lib.rs#L424), [`ui/dr-ui/src/library.rs:1040`](../ui/dr-ui/src/library.rs#L1040), [`ui/dr-ui/src/library.rs:1677`](../ui/dr-ui/src/library.rs#L1677), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:844`](../ui/dr-ui/src/library.rs#L844), [`ui/dr-ui/src/library_ui.rs:1605`](../ui/dr-ui/src/library_ui.rs#L1605), [`ui/dr-ui/src/library_ui.rs:3358`](../ui/dr-ui/src/library_ui.rs#L3358), [`ui/dr-ui/src/library_ui.rs:504`](../ui/dr-ui/src/library_ui.rs#L504), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
| FR-NC-10 | [`core/dr-sync/src/account.rs:220`](../core/dr-sync/src/account.rs#L220), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:424`](../ui/dr-ui/src/lib.rs#L424), [`ui/dr-ui/src/library.rs:1068`](../ui/dr-ui/src/library.rs#L1068), [`ui/dr-ui/src/library.rs:1705`](../ui/dr-ui/src/library.rs#L1705), [`ui/dr-ui/src/library.rs:543`](../ui/dr-ui/src/library.rs#L543), [`ui/dr-ui/src/library.rs:844`](../ui/dr-ui/src/library.rs#L844), [`ui/dr-ui/src/library_ui.rs:1605`](../ui/dr-ui/src/library_ui.rs#L1605), [`ui/dr-ui/src/library_ui.rs:3358`](../ui/dr-ui/src/library_ui.rs#L3358), [`ui/dr-ui/src/library_ui.rs:504`](../ui/dr-ui/src/library_ui.rs#L504), [`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:39`](../core/dr-sync-nextcloud/src/lib.rs#L39), [`core/dr-sync-nextcloud/src/lib.rs:909`](../core/dr-sync-nextcloud/src/lib.rs#L909), [`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:81`](../core/dr-sync/src/account.rs#L81), [`core/dr-sync/src/lib.rs:166`](../core/dr-sync/src/lib.rs#L166), [`core/dr-sync/src/lib.rs:49`](../core/dr-sync/src/lib.rs#L49), [`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:39`](../core/dr-sync-nextcloud/src/lib.rs#L39), [`core/dr-sync-nextcloud/src/lib.rs:913`](../core/dr-sync-nextcloud/src/lib.rs#L913), [`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:81`](../core/dr-sync/src/account.rs#L81), [`core/dr-sync/src/lib.rs:211`](../core/dr-sync/src/lib.rs#L211), [`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:143`](../core/dr-sync-folder/src/lib.rs#L143), [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/lib.rs:65`](../core/dr-sync-folder/src/lib.rs#L65), [`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-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:298`](../core/dr-sync/src/account.rs#L298), [`core/dr-sync/src/account.rs:355`](../core/dr-sync/src/account.rs#L355), [`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-2 | [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:298`](../core/dr-sync/src/account.rs#L298), [`core/dr-sync/src/account.rs:355`](../core/dr-sync/src/account.rs#L355), [`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:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:2702`](../ui/dr-ui/src/library.rs#L2702), [`ui/dr-ui/src/library.rs:3172`](../ui/dr-ui/src/library.rs#L3172), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3610`](../ui/dr-ui/src/library_ui.rs#L3610), [`ui/dr-ui/src/library_ui.rs:4575`](../ui/dr-ui/src/library_ui.rs#L4575), [`ui/dr-ui/ui/app.slint:319`](../ui/dr-ui/ui/app.slint#L319), [`ui/dr-ui/ui/settings.slint:372`](../ui/dr-ui/ui/settings.slint#L372), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) |
|
| 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), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:2730`](../ui/dr-ui/src/library.rs#L2730), [`ui/dr-ui/src/library.rs:3200`](../ui/dr-ui/src/library.rs#L3200), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3610`](../ui/dr-ui/src/library_ui.rs#L3610), [`ui/dr-ui/src/library_ui.rs:4575`](../ui/dr-ui/src/library_ui.rs#L4575), [`ui/dr-ui/ui/app.slint:319`](../ui/dr-ui/ui/app.slint#L319), [`ui/dr-ui/ui/settings.slint:372`](../ui/dr-ui/ui/settings.slint#L372), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) |
|
||||||
| FR-NC-4 | [`core/dr-sync-folder/src/lib.rs:143`](../core/dr-sync-folder/src/lib.rs#L143), [`core/dr-sync-nextcloud/src/propfind.rs:100`](../core/dr-sync-nextcloud/src/propfind.rs#L100), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:166`](../core/dr-sync/src/lib.rs#L166), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61) |
|
| FR-NC-4 | [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-nextcloud/src/propfind.rs:103`](../core/dr-sync-nextcloud/src/propfind.rs#L103), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:211`](../core/dr-sync/src/lib.rs#L211), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61) |
|
||||||
| FR-NC-5 | [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488) |
|
| FR-NC-5 | [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488) |
|
||||||
| FR-NC-6 | [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1) |
|
| 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/schema.rs:1014`](../core/dr-catalog/src/schema.rs#L1014), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`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:3174`](../ui/dr-ui/src/collections_ui.rs#L3174), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1807`](../ui/dr-ui/src/lib.rs#L1807), [`ui/dr-ui/src/lib.rs:2698`](../ui/dr-ui/src/lib.rs#L2698), [`ui/dr-ui/src/library.rs:1421`](../ui/dr-ui/src/library.rs#L1421), [`ui/dr-ui/src/library.rs:1444`](../ui/dr-ui/src/library.rs#L1444), [`ui/dr-ui/src/library.rs:1601`](../ui/dr-ui/src/library.rs#L1601), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1412`](../ui/dr-ui/src/library_ui.rs#L1412), [`ui/dr-ui/src/library_ui.rs:1530`](../ui/dr-ui/src/library_ui.rs#L1530), [`ui/dr-ui/src/library_ui.rs:2057`](../ui/dr-ui/src/library_ui.rs#L2057), [`ui/dr-ui/src/library_ui.rs:273`](../ui/dr-ui/src/library_ui.rs#L273), [`ui/dr-ui/src/library_ui.rs:284`](../ui/dr-ui/src/library_ui.rs#L284), [`ui/dr-ui/src/library_ui.rs:292`](../ui/dr-ui/src/library_ui.rs#L292), [`ui/dr-ui/src/library_ui.rs:304`](../ui/dr-ui/src/library_ui.rs#L304), [`ui/dr-ui/src/library_ui.rs:313`](../ui/dr-ui/src/library_ui.rs#L313), [`ui/dr-ui/src/library_ui.rs:405`](../ui/dr-ui/src/library_ui.rs#L405), [`ui/dr-ui/src/library_ui.rs:415`](../ui/dr-ui/src/library_ui.rs#L415), [`ui/dr-ui/src/library_ui.rs:457`](../ui/dr-ui/src/library_ui.rs#L457), [`ui/dr-ui/src/library_ui.rs:520`](../ui/dr-ui/src/library_ui.rs#L520), [`ui/dr-ui/src/library_ui.rs:5301`](../ui/dr-ui/src/library_ui.rs#L5301), [`ui/dr-ui/src/library_ui.rs:5319`](../ui/dr-ui/src/library_ui.rs#L5319), [`ui/dr-ui/src/library_ui.rs:5331`](../ui/dr-ui/src/library_ui.rs#L5331), [`ui/dr-ui/src/library_ui.rs:551`](../ui/dr-ui/src/library_ui.rs#L551), [`ui/dr-ui/src/library_ui.rs:563`](../ui/dr-ui/src/library_ui.rs#L563), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:2335`](../ui/dr-ui/ui/app.slint#L2335), [`ui/dr-ui/ui/app.slint:434`](../ui/dr-ui/ui/app.slint#L434), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:980`](../ui/dr-ui/ui/library.slint#L980) |
|
| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:1014`](../core/dr-catalog/src/schema.rs#L1014), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`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:3174`](../ui/dr-ui/src/collections_ui.rs#L3174), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1807`](../ui/dr-ui/src/lib.rs#L1807), [`ui/dr-ui/src/lib.rs:2698`](../ui/dr-ui/src/lib.rs#L2698), [`ui/dr-ui/src/library.rs:1449`](../ui/dr-ui/src/library.rs#L1449), [`ui/dr-ui/src/library.rs:1472`](../ui/dr-ui/src/library.rs#L1472), [`ui/dr-ui/src/library.rs:1629`](../ui/dr-ui/src/library.rs#L1629), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1412`](../ui/dr-ui/src/library_ui.rs#L1412), [`ui/dr-ui/src/library_ui.rs:1530`](../ui/dr-ui/src/library_ui.rs#L1530), [`ui/dr-ui/src/library_ui.rs:2057`](../ui/dr-ui/src/library_ui.rs#L2057), [`ui/dr-ui/src/library_ui.rs:273`](../ui/dr-ui/src/library_ui.rs#L273), [`ui/dr-ui/src/library_ui.rs:284`](../ui/dr-ui/src/library_ui.rs#L284), [`ui/dr-ui/src/library_ui.rs:292`](../ui/dr-ui/src/library_ui.rs#L292), [`ui/dr-ui/src/library_ui.rs:304`](../ui/dr-ui/src/library_ui.rs#L304), [`ui/dr-ui/src/library_ui.rs:313`](../ui/dr-ui/src/library_ui.rs#L313), [`ui/dr-ui/src/library_ui.rs:405`](../ui/dr-ui/src/library_ui.rs#L405), [`ui/dr-ui/src/library_ui.rs:415`](../ui/dr-ui/src/library_ui.rs#L415), [`ui/dr-ui/src/library_ui.rs:457`](../ui/dr-ui/src/library_ui.rs#L457), [`ui/dr-ui/src/library_ui.rs:520`](../ui/dr-ui/src/library_ui.rs#L520), [`ui/dr-ui/src/library_ui.rs:5301`](../ui/dr-ui/src/library_ui.rs#L5301), [`ui/dr-ui/src/library_ui.rs:5319`](../ui/dr-ui/src/library_ui.rs#L5319), [`ui/dr-ui/src/library_ui.rs:5331`](../ui/dr-ui/src/library_ui.rs#L5331), [`ui/dr-ui/src/library_ui.rs:551`](../ui/dr-ui/src/library_ui.rs#L551), [`ui/dr-ui/src/library_ui.rs:563`](../ui/dr-ui/src/library_ui.rs#L563), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:2335`](../ui/dr-ui/ui/app.slint#L2335), [`ui/dr-ui/ui/app.slint:434`](../ui/dr-ui/ui/app.slint#L434), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:980`](../ui/dr-ui/ui/library.slint#L980) |
|
||||||
| FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294) |
|
| FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294) |
|
||||||
| FR-NC-6c | [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`core/dr-types/src/lib.rs:201`](../core/dr-types/src/lib.rs#L201), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3174`](../ui/dr-ui/src/collections_ui.rs#L3174), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260) |
|
| FR-NC-6c | [`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:770`](../core/dr-sync-folder/src/lib.rs#L770), [`core/dr-sync-folder/src/lib.rs:809`](../core/dr-sync-folder/src/lib.rs#L809), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1), [`core/dr-sync-nextcloud/src/desktop_client.rs:167`](../core/dr-sync-nextcloud/src/desktop_client.rs#L167), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`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:119`](../core/dr-types/src/lib.rs#L119), [`core/dr-types/src/lib.rs:201`](../core/dr-types/src/lib.rs#L201), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3174`](../ui/dr-ui/src/collections_ui.rs#L3174), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/library.rs:948`](../ui/dr-ui/src/library.rs#L948), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/remote.rs:40`](../ui/dr-ui/src/remote.rs#L40), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260) |
|
||||||
| 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:100`](../core/dr-sync-nextcloud/src/lib.rs#L100), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:3172`](../ui/dr-ui/src/library.rs#L3172), [`ui/dr-ui/src/library_ui.rs:3610`](../ui/dr-ui/src/library_ui.rs#L3610), [`ui/dr-ui/ui/settings.slint:372`](../ui/dr-ui/ui/settings.slint#L372) |
|
| 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:104`](../core/dr-sync-nextcloud/src/lib.rs#L104), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:3200`](../ui/dr-ui/src/library.rs#L3200), [`ui/dr-ui/src/library_ui.rs:3610`](../ui/dr-ui/src/library_ui.rs#L3610), [`ui/dr-ui/ui/settings.slint:372`](../ui/dr-ui/ui/settings.slint#L372) |
|
||||||
| 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:116`](../core/dr-types/src/settings.rs#L116), [`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:1123`](../ui/dr-ui/src/lib.rs#L1123), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
| 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:116`](../core/dr-types/src/settings.rs#L116), [`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:1123`](../ui/dr-ui/src/lib.rs#L1123), [`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:1123`](../ui/dr-ui/src/lib.rs#L1123) |
|
| 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:1123`](../ui/dr-ui/src/lib.rs#L1123) |
|
||||||
| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`ui/dr-ui/src/lib.rs:1777`](../ui/dr-ui/src/lib.rs#L1777), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459), [`ui/dr-ui/src/library_ui.rs:472`](../ui/dr-ui/src/library_ui.rs#L472) |
|
| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`ui/dr-ui/src/lib.rs:1777`](../ui/dr-ui/src/lib.rs#L1777), [`ui/dr-ui/src/library.rs:459`](../ui/dr-ui/src/library.rs#L459), [`ui/dr-ui/src/library_ui.rs:472`](../ui/dr-ui/src/library_ui.rs#L472) |
|
||||||
| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:2033`](../core/dr-pipeline/src/sidecar.rs#L2033), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:450`](../core/dr-pipeline/src/sidecar.rs#L450), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/library.rs:844`](../ui/dr-ui/src/library.rs#L844), [`ui/dr-ui/src/library.rs:970`](../ui/dr-ui/src/library.rs#L970) |
|
| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:556`](../core/dr-catalog/src/schema.rs#L556), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:2033`](../core/dr-pipeline/src/sidecar.rs#L2033), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:450`](../core/dr-pipeline/src/sidecar.rs#L450), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/library.rs:844`](../ui/dr-ui/src/library.rs#L844), [`ui/dr-ui/src/library.rs:998`](../ui/dr-ui/src/library.rs#L998) |
|
||||||
| FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) |
|
| FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) |
|
||||||
| FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) |
|
| FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) |
|
||||||
| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:866`](../ui/dr-ui/src/lib.rs#L866), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
|
| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:866`](../ui/dr-ui/src/lib.rs#L866), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
|
||||||
@@ -118,23 +118,23 @@ _None._
|
|||||||
| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1488`](../ui/dr-ui/src/collections_ui.rs#L1488), [`ui/dr-ui/src/collections_ui.rs:1502`](../ui/dr-ui/src/collections_ui.rs#L1502), [`ui/dr-ui/src/collections_ui.rs:1548`](../ui/dr-ui/src/collections_ui.rs#L1548), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1646`](../ui/dr-ui/src/collections_ui.rs#L1646), [`ui/dr-ui/src/collections_ui.rs:1673`](../ui/dr-ui/src/collections_ui.rs#L1673), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4370`](../ui/dr-ui/src/library_ui.rs#L4370), [`ui/dr-ui/src/library_ui.rs:4415`](../ui/dr-ui/src/library_ui.rs#L4415), [`ui/dr-ui/src/library_ui.rs:4518`](../ui/dr-ui/src/library_ui.rs#L4518), [`ui/dr-ui/src/library_ui.rs:4546`](../ui/dr-ui/src/library_ui.rs#L4546), [`ui/dr-ui/src/library_ui.rs:5319`](../ui/dr-ui/src/library_ui.rs#L5319), [`ui/dr-ui/src/library_ui.rs:5331`](../ui/dr-ui/src/library_ui.rs#L5331), [`ui/dr-ui/ui/app.slint:1608`](../ui/dr-ui/ui/app.slint#L1608), [`ui/dr-ui/ui/app.slint:440`](../ui/dr-ui/ui/app.slint#L440), [`ui/dr-ui/ui/app.slint:471`](../ui/dr-ui/ui/app.slint#L471), [`ui/dr-ui/ui/library.slint:1202`](../ui/dr-ui/ui/library.slint#L1202), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:1215`](../ui/dr-ui/ui/library.slint#L1215), [`ui/dr-ui/ui/library.slint:2314`](../ui/dr-ui/ui/library.slint#L2314), [`ui/dr-ui/ui/library.slint:829`](../ui/dr-ui/ui/library.slint#L829), [`ui/dr-ui/ui/library.slint:879`](../ui/dr-ui/ui/library.slint#L879), [`ui/dr-ui/ui/library.slint:898`](../ui/dr-ui/ui/library.slint#L898) |
|
| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1488`](../ui/dr-ui/src/collections_ui.rs#L1488), [`ui/dr-ui/src/collections_ui.rs:1502`](../ui/dr-ui/src/collections_ui.rs#L1502), [`ui/dr-ui/src/collections_ui.rs:1548`](../ui/dr-ui/src/collections_ui.rs#L1548), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1646`](../ui/dr-ui/src/collections_ui.rs#L1646), [`ui/dr-ui/src/collections_ui.rs:1673`](../ui/dr-ui/src/collections_ui.rs#L1673), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4370`](../ui/dr-ui/src/library_ui.rs#L4370), [`ui/dr-ui/src/library_ui.rs:4415`](../ui/dr-ui/src/library_ui.rs#L4415), [`ui/dr-ui/src/library_ui.rs:4518`](../ui/dr-ui/src/library_ui.rs#L4518), [`ui/dr-ui/src/library_ui.rs:4546`](../ui/dr-ui/src/library_ui.rs#L4546), [`ui/dr-ui/src/library_ui.rs:5319`](../ui/dr-ui/src/library_ui.rs#L5319), [`ui/dr-ui/src/library_ui.rs:5331`](../ui/dr-ui/src/library_ui.rs#L5331), [`ui/dr-ui/ui/app.slint:1608`](../ui/dr-ui/ui/app.slint#L1608), [`ui/dr-ui/ui/app.slint:440`](../ui/dr-ui/ui/app.slint#L440), [`ui/dr-ui/ui/app.slint:471`](../ui/dr-ui/ui/app.slint#L471), [`ui/dr-ui/ui/library.slint:1202`](../ui/dr-ui/ui/library.slint#L1202), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:1215`](../ui/dr-ui/ui/library.slint#L1215), [`ui/dr-ui/ui/library.slint:2314`](../ui/dr-ui/ui/library.slint#L2314), [`ui/dr-ui/ui/library.slint:829`](../ui/dr-ui/ui/library.slint#L829), [`ui/dr-ui/ui/library.slint:879`](../ui/dr-ui/ui/library.slint#L879), [`ui/dr-ui/ui/library.slint:898`](../ui/dr-ui/ui/library.slint#L898) |
|
||||||
| 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:2394`](../ui/dr-ui/src/lib.rs#L2394), [`ui/dr-ui/src/lib.rs:2819`](../ui/dr-ui/src/lib.rs#L2819), [`ui/dr-ui/src/lib.rs:3004`](../ui/dr-ui/src/lib.rs#L3004), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:89`](../ui/dr-ui/ui/app.slint#L89), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) |
|
| 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:2394`](../ui/dr-ui/src/lib.rs#L2394), [`ui/dr-ui/src/lib.rs:2819`](../ui/dr-ui/src/lib.rs#L2819), [`ui/dr-ui/src/lib.rs:3004`](../ui/dr-ui/src/lib.rs#L3004), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:89`](../ui/dr-ui/ui/app.slint#L89), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) |
|
||||||
| FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:262`](../core/dr-pipeline/src/framing.rs#L262) |
|
| FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:262`](../core/dr-pipeline/src/framing.rs#L262) |
|
||||||
| 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/library.rs:2807`](../ui/dr-ui/src/library.rs#L2807) |
|
| 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/library.rs:2835`](../ui/dr-ui/src/library.rs#L2835) |
|
||||||
| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1553`](../ui/dr-ui/src/export.rs#L1553), [`ui/dr-ui/src/export.rs:1579`](../ui/dr-ui/src/export.rs#L1579), [`ui/dr-ui/src/export.rs:409`](../ui/dr-ui/src/export.rs#L409), [`ui/dr-ui/src/export.rs:435`](../ui/dr-ui/src/export.rs#L435), [`ui/dr-ui/src/lib.rs:2112`](../ui/dr-ui/src/lib.rs#L2112), [`ui/dr-ui/ui/app.slint:940`](../ui/dr-ui/ui/app.slint#L940), [`ui/dr-ui/ui/library.slint:934`](../ui/dr-ui/ui/library.slint#L934) |
|
| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1553`](../ui/dr-ui/src/export.rs#L1553), [`ui/dr-ui/src/export.rs:1579`](../ui/dr-ui/src/export.rs#L1579), [`ui/dr-ui/src/export.rs:409`](../ui/dr-ui/src/export.rs#L409), [`ui/dr-ui/src/export.rs:435`](../ui/dr-ui/src/export.rs#L435), [`ui/dr-ui/src/lib.rs:2112`](../ui/dr-ui/src/lib.rs#L2112), [`ui/dr-ui/ui/app.slint:940`](../ui/dr-ui/ui/app.slint#L940), [`ui/dr-ui/ui/library.slint:934`](../ui/dr-ui/ui/library.slint#L934) |
|
||||||
| NFR-ARCH-4 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-export/src/error.rs:1`](../core/dr-export/src/error.rs#L1), [`core/dr-thumbs/src/error.rs:1`](../core/dr-thumbs/src/error.rs#L1), [`platform/dr-plat/src/storage.rs:148`](../platform/dr-plat/src/storage.rs#L148), [`ui/dr-ui/src/export.rs:499`](../ui/dr-ui/src/export.rs#L499) |
|
| NFR-ARCH-4 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-export/src/error.rs:1`](../core/dr-export/src/error.rs#L1), [`core/dr-thumbs/src/error.rs:1`](../core/dr-thumbs/src/error.rs#L1), [`platform/dr-plat/src/storage.rs:148`](../platform/dr-plat/src/storage.rs#L148), [`ui/dr-ui/src/export.rs:499`](../ui/dr-ui/src/export.rs#L499) |
|
||||||
| NFR-OPS-1 | [`tools/traceability/src/lib.rs:266`](../tools/traceability/src/lib.rs#L266) |
|
| NFR-OPS-1 | [`tools/traceability/src/lib.rs:266`](../tools/traceability/src/lib.rs#L266) |
|
||||||
| NFR-P1 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) |
|
| NFR-P1 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) |
|
||||||
| NFR-P13 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) |
|
| NFR-P13 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) |
|
||||||
| NFR-P5 | [`core/dr-catalog/src/schema.rs:318`](../core/dr-catalog/src/schema.rs#L318), [`ui/dr-ui/src/library.rs:3771`](../ui/dr-ui/src/library.rs#L3771), [`ui/dr-ui/src/library.rs:4563`](../ui/dr-ui/src/library.rs#L4563), [`ui/dr-ui/src/library_ui.rs:4055`](../ui/dr-ui/src/library_ui.rs#L4055), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) |
|
| NFR-P5 | [`core/dr-catalog/src/schema.rs:318`](../core/dr-catalog/src/schema.rs#L318), [`ui/dr-ui/src/library.rs:3799`](../ui/dr-ui/src/library.rs#L3799), [`ui/dr-ui/src/library.rs:4591`](../ui/dr-ui/src/library.rs#L4591), [`ui/dr-ui/src/library_ui.rs:4055`](../ui/dr-ui/src/library_ui.rs#L4055), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) |
|
||||||
| NFR-P9 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/export.rs:942`](../ui/dr-ui/src/export.rs#L942), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1) |
|
| NFR-P9 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/export.rs:942`](../ui/dr-ui/src/export.rs#L942), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1) |
|
||||||
| NFR-PORT-1 | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302), [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`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:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) |
|
| NFR-PORT-1 | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302), [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`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:287`](../platform/dr-plat/src/storage.rs#L287), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`platform/dr-plat/src/volumes.rs:1`](../platform/dr-plat/src/volumes.rs#L1), [`platform/dr-plat/src/volumes.rs:62`](../platform/dr-plat/src/volumes.rs#L62) |
|
||||||
| NFR-PORT-3 | [`platform/dr-plat/src/storage.rs:1`](../platform/dr-plat/src/storage.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/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-sync/src/account.rs:220`](../core/dr-sync/src/account.rs#L220), [`ui/dr-ui/src/library.rs:1040`](../ui/dr-ui/src/library.rs#L1040) |
|
| NFR-R1 | [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-sync/src/account.rs:220`](../core/dr-sync/src/account.rs#L220), [`ui/dr-ui/src/library.rs:1068`](../ui/dr-ui/src/library.rs#L1068) |
|
||||||
| NFR-R2 | [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1) |
|
| NFR-R2 | [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1) |
|
||||||
| 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) |
|
| 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) |
|
||||||
| NFR-R7 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) |
|
| NFR-R7 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) |
|
||||||
| NFR-R8 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) |
|
| NFR-R8 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) |
|
||||||
| NFR-RES-1 | [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/lib.rs:72`](../ui/dr-ui/src/lib.rs#L72) |
|
| NFR-RES-1 | [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/lib.rs:72`](../ui/dr-ui/src/lib.rs#L72) |
|
||||||
| NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`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), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/library.rs:2702`](../ui/dr-ui/src/library.rs#L2702) |
|
| NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/schema.rs:613`](../core/dr-catalog/src/schema.rs#L613), [`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), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/library.rs:2730`](../ui/dr-ui/src/library.rs#L2730) |
|
||||||
| NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) |
|
| NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) |
|
||||||
| NFR-SEC-2 | [`core/dr-sync/src/account.rs:298`](../core/dr-sync/src/account.rs#L298), [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) |
|
| NFR-SEC-2 | [`core/dr-sync/src/account.rs:298`](../core/dr-sync/src/account.rs#L298), [`platform/dr-plat/src/secrets.rs:82`](../platform/dr-plat/src/secrets.rs#L82) |
|
||||||
| NFR-SEC-5 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
| NFR-SEC-5 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:442`](../core/dr-catalog/src/schema.rs#L442), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
||||||
@@ -143,7 +143,7 @@ _None._
|
|||||||
|
|
||||||
## Not yet tagged
|
## Not yet tagged
|
||||||
|
|
||||||
71 of 178 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
|
72 of 179 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
|
||||||
|
|
||||||
<details><summary>Show untagged requirements</summary>
|
<details><summary>Show untagged requirements</summary>
|
||||||
|
|
||||||
@@ -157,6 +157,7 @@ _None._
|
|||||||
- FR-DSP-2
|
- FR-DSP-2
|
||||||
- FR-DSP-4
|
- FR-DSP-4
|
||||||
- FR-NC-11
|
- FR-NC-11
|
||||||
|
- FR-NC-6d
|
||||||
- FR-PLAT-AND-2
|
- FR-PLAT-AND-2
|
||||||
- FR-PLAT-AND-4
|
- FR-PLAT-AND-4
|
||||||
- FR-PLAT-AND-5
|
- FR-PLAT-AND-5
|
||||||
|
|||||||
+31
-2
@@ -25,7 +25,7 @@ use std::path::PathBuf;
|
|||||||
use std::sync::mpsc::{Receiver, Sender};
|
use std::sync::mpsc::{Receiver, Sender};
|
||||||
|
|
||||||
use dr_catalog::{Catalog, JobKind, Priority};
|
use dr_catalog::{Catalog, JobKind, Priority};
|
||||||
use dr_sync::{Account, Connection, RemoteBackend, RemoteId, RemotePath};
|
use dr_sync::{Account, Connection, RemoteBackend, RemoteError, RemoteId, RemotePath};
|
||||||
use dr_thumbs::ThumbStore;
|
use dr_thumbs::ThumbStore;
|
||||||
|
|
||||||
use crate::sidecar_cache::SidecarCache;
|
use crate::sidecar_cache::SidecarCache;
|
||||||
@@ -944,7 +944,35 @@ async fn drain_one(
|
|||||||
|
|
||||||
let path = RemotePath::new(path_str.to_string());
|
let path = RemotePath::new(path_str.to_string());
|
||||||
let id = RemoteId::Path(path.clone());
|
let id = RemoteId::Path(path.clone());
|
||||||
let remote = backend.get(&id, None).await.ok();
|
|
||||||
|
// TRACES: FR-NC-6c
|
||||||
|
// A miss and a placeholder are not the same answer, and conflating them
|
||||||
|
// destroys work. This read decides whether the sidecar already on the
|
||||||
|
// remote is merged in; treating "the content is not on this device" as
|
||||||
|
// "there is no sidecar" writes a fresh document over an existing one and
|
||||||
|
// discards every edit another device put there — the exact loss the
|
||||||
|
// format's unknown-key preservation exists to prevent.
|
||||||
|
//
|
||||||
|
// A sidecar is a few kilobytes, so the right response to a placeholder is
|
||||||
|
// to fetch it, not to give up. Where that is impossible — no client
|
||||||
|
// running — the entry stays queued, which is what the outbox is for.
|
||||||
|
let remote = match backend.get(&id, None).await {
|
||||||
|
Ok(bytes) => Some(bytes),
|
||||||
|
Err(RemoteError::NotFound(_)) => None,
|
||||||
|
Err(RemoteError::NotMaterialised(_)) => {
|
||||||
|
backend
|
||||||
|
.materialise(&id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("sidecar is not on this device ({e})"))?;
|
||||||
|
match backend.get(&id, None).await {
|
||||||
|
Ok(bytes) => Some(bytes),
|
||||||
|
Err(e) => return Err(format!("sidecar could not be read ({e})")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Anything else — a refused read, a dead connection — leaves the entry
|
||||||
|
// queued rather than resolved by overwriting.
|
||||||
|
Err(e) => return Err(format!("sidecar could not be read ({e})")),
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(bytes) = remote.as_deref() {
|
if let Some(bytes) = remote.as_deref() {
|
||||||
if !bytes.is_empty() {
|
if !bytes.is_empty() {
|
||||||
@@ -5440,6 +5468,7 @@ mod tests {
|
|||||||
size,
|
size,
|
||||||
modified: None,
|
modified: None,
|
||||||
has_preview: false,
|
has_preview: false,
|
||||||
|
materialised: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -25,7 +25,7 @@ use std::sync::{Arc, OnceLock};
|
|||||||
|
|
||||||
use dr_sync::{Account, BackendProvider, BackendRegistry, Connection, RemoteBackend, RemoteError};
|
use dr_sync::{Account, BackendProvider, BackendRegistry, Connection, RemoteBackend, RemoteError};
|
||||||
use dr_sync_folder::FolderProvider;
|
use dr_sync_folder::FolderProvider;
|
||||||
use dr_sync_nextcloud::NextcloudProvider;
|
use dr_sync_nextcloud::{NextcloudProvider, NextcloudVfs};
|
||||||
|
|
||||||
/// Every storage backend this build has, in the order the launch screen
|
/// Every storage backend this build has, in the order the launch screen
|
||||||
/// offers them.
|
/// offers them.
|
||||||
@@ -37,7 +37,16 @@ pub fn registry() -> &'static BackendRegistry {
|
|||||||
REGISTRY.get_or_init(|| {
|
REGISTRY.get_or_init(|| {
|
||||||
let mut r = BackendRegistry::new();
|
let mut r = BackendRegistry::new();
|
||||||
r.register(Arc::new(NextcloudProvider));
|
r.register(Arc::new(NextcloudProvider));
|
||||||
r.register(Arc::new(FolderProvider));
|
// TRACES: FR-NC-6c
|
||||||
|
// The folder connector does the filesystem work and knows nothing
|
||||||
|
// about sync clients; the placeholder convention is supplied here,
|
||||||
|
// which is the one place that may name one. Detection is per folder
|
||||||
|
// and per connection — the same directory offers hydration while the
|
||||||
|
// client is running and not while it is down (ARCH §9.0).
|
||||||
|
r.register(Arc::new(FolderProvider::with_vfs_detector(|root| {
|
||||||
|
NextcloudVfs::looks_synced(root)
|
||||||
|
.then(|| Arc::new(NextcloudVfs::detect()) as Arc<dyn dr_sync_folder::Vfs>)
|
||||||
|
})));
|
||||||
r
|
r
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user