Thirteen requirements were surveyed as built but untagged. Eight of them were: R3, R6, FR-DEV-1, FR-UI-6, FR-NC-6d, NFR-OPS-3, NFR-PORT-2 and NFR-SEC-3. Each was read against its full text in requirements.md and against the code before the tag was added, because a tag that is wrong is worse than an absent one — it turns a visible gap into an invisible one. The five that were refused, and why, because the reasoning is the part worth keeping: R2 carries "(figure TBD)" in its own acceptance criterion and asks for a stated prefetch margin and cache-hit rate; neither figure exists anywhere in the tree and neither quantity is measured, while TD-2 and TD-3 both describe the thumbnail path falling short of it. R5 asks for three things and the code does one. The display pipeline does run at viewport resolution, but "only visible tiles are computed" and "panning recomputes only newly exposed tiles" need a tile scheduler that does not exist — and frame_budget.rs currently argues for striking tiled computation from the interactive path rather than building it. FR-RAW-2 asks for a trait taking a SourceRef, so that a second decoder can be added without changing callers. What exists is free functions over &[u8]. That meets the requirement's stated *purpose* — the same decoder serves a local file, a SAF document and a byte range, which is exactly why it takes bytes — but there is no trait and no second implementation seam, so the requirement should probably be amended rather than tagged. NFR-ARCH-1 asks for named executors with stated thread counts. architecture.md §7.1 states the table; nothing implements it. Workers are twenty-odd ad-hoc std::thread::spawn sites, each building its own one-worker tokio runtime, with no decode pool, no GPU-submit executor and no I/O pool. The requirement's own text says R4 and NFR-P9 "assert an outcome with no stated means", and that is still true. NFR-SEC-4 is satisfied by absence — there is no telemetry — and absence has no module to tag. A tag would point at nothing. NFR-OPS-3 was the closest call of the eight taken. The store is single, separate from the catalog, survives a catalog rebuild and does not sync between devices; it has no version *field*, deliberately, and settings.rs argues why and names the condition that would need one. The substance is met and the reasoning is recorded where it belongs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
132 lines
5.2 KiB
Rust
132 lines
5.2 KiB
Rust
// TRACES: FR-NC-6c | FR-NC-6d
|
|
//! 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());
|
|
}
|
|
}
|