//! What a backend can do. //! //! Declared rather than assumed, because the operations that matter most for //! performance are not universal (ARCH §8.1). /// TRACES: FR-NC-4 /// How a backend reports what changed. /// /// This is the single most consequential capability: it determines whether a /// no-op sync over a large library costs one request or thousands. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ChangeDetection { /// The backend maintains a change feed; we present a cursor and receive /// what changed since. Cheapest possible. DeltaCursor, /// Directory ETags propagate upward, so an unchanged parent proves an /// unchanged subtree. Nextcloud. One request proves a whole library /// unchanged. PropagatingEtags, /// ETags exist per entry but do not propagate. The tree must be walked, /// though ETags still prevent re-downloading unchanged content. LocalEtags, /// Modification times only. Walk and compare — vulnerable to clock skew /// and coarse timestamp granularity. Timestamps, } /// Constraints on chunked upload. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ChunkConstraints { pub min_chunk: u64, pub max_chunk: u64, /// Bodies at or below this go in a single request. pub single_shot_below: u64, pub max_chunks: u32, } /// TRACES: FR-NC-3 /// Whether the server can render thumbnails, and for what. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ServerPreviews { /// No server-side rendering. None, /// Common web formats only. **This is stock Nextcloud** — no RAW preview /// provider ships with it, so RAW thumbnails must come from local /// embedded-preview extraction (ARCH §6.7). CommonFormatsOnly, /// RAW included, e.g. via the `camerarawpreviews` app or an Imaginary /// backend. Detected per-account, never assumed. IncludingRaw, } /// The full capability set. #[derive(Debug, Clone)] pub struct Capabilities { pub change_detection: ChangeDetection, /// Identity survives server-side rename and move, so a move is not /// mistaken for delete-plus-add of a large file. pub stable_ids: bool, /// Byte-range reads. Without these, embedded-preview extraction is /// impossible and remote browsing must download whole files. pub range_reads: bool, pub chunked_upload: Option, /// Many small objects in one request. pub bulk_upload: bool, /// Conditional write (If-Match), for conflict-safe sidecar updates. pub conditional_write: bool, pub server_previews: ServerPreviews, } impl Capabilities { /// The weakest backend the engine will still drive: listing and whole-file /// transfer, nothing more. Everything degrades but stays correct. pub fn minimal() -> Self { Self { change_detection: ChangeDetection::Timestamps, stable_ids: false, range_reads: false, chunked_upload: None, bulk_upload: false, conditional_write: false, server_previews: ServerPreviews::None, } } /// Whether remote browsing can avoid downloading whole files. /// /// Where false, the UI must not browse a remote library on a metered /// connection without explicit consent (FR-NC-12). pub fn can_browse_cheaply(&self) -> bool { self.range_reads || matches!(self.server_previews, ServerPreviews::IncludingRaw) } /// Whether sidecar conflicts can be detected reliably. /// /// Without conditional writes the engine falls back to comparing revision /// counters inside the sidecar, which narrows the race but does not close /// it — surfaced as a reduced-safety mode. pub fn safe_concurrent_writes(&self) -> bool { self.conditional_write } } #[cfg(test)] mod tests { use super::*; #[test] fn minimal_backend_degrades_but_stays_usable() { let caps = Capabilities::minimal(); assert!(!caps.can_browse_cheaply()); assert!(!caps.safe_concurrent_writes()); } #[test] fn server_raw_previews_substitute_for_range_reads() { // A server that renders RAW thumbnails makes browsing cheap even // without range support. let caps = Capabilities { server_previews: ServerPreviews::IncludingRaw, ..Capabilities::minimal() }; assert!(caps.can_browse_cheaply()); } #[test] fn common_format_previews_do_not_help_raw() { // Stock Nextcloud: previews exist, but not for RAW, so range reads // remain the only cheap path. let caps = Capabilities { server_previews: ServerPreviews::CommonFormatsOnly, ..Capabilities::minimal() }; assert!(!caps.can_browse_cheaply()); } }