// 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()); } }