// TRACES: FR-NC-12 | FR-NC-13 //! The one place the interface names a backend. //! //! `dr-sync` defines [`RemoteBackend`] and a capability model the engine adapts //! to, so that a second backend can be added without touching the code that //! uses one (ARCH §8.1). Until this module existed that boundary was //! documentation: seven files in `dr-ui` constructed a `NextcloudBackend` //! directly and ten functions took one by concrete type, so the abstraction //! bought nothing it was designed for and a WebDAV or local-folder backend //! would have had nowhere to go. //! //! Everything above this module works through `&dyn RemoteBackend`, and every //! account it opens is a [`dr_sync::Account`] — configuration with no server //! in it. Adding a backend is implementing two traits and adding a line to //! [`registry`]; nothing else in `dr-ui` changes. See `docs/storage.md`. //! //! # Why the registry is built here and not in `dr-sync` //! //! `dr-sync` must not depend on any connector, or the engine would drag a TLS //! stack into a build that only wanted a folder. So the crate that already //! depends on all of them — the interface — is where the list lives. It is //! the only file in the application that names one. use std::sync::{Arc, OnceLock}; use dr_sync::{Account, BackendProvider, BackendRegistry, Connection, RemoteBackend, RemoteError}; use dr_sync_folder::FolderProvider; use dr_sync_nextcloud::{NextcloudProvider, NextcloudVfs}; /// Every storage backend this build has, in the order the launch screen /// offers them. /// /// Built once. A provider is stateless — it holds no connection and no /// credential — so one instance serves every thread that asks. pub fn registry() -> &'static BackendRegistry { static REGISTRY: OnceLock = OnceLock::new(); REGISTRY.get_or_init(|| { let mut r = BackendRegistry::new(); r.register(Arc::new(NextcloudProvider)); // 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) }))); r }) } /// The connector serving an account. /// /// Errors when this build has none — a configuration file outlives the binary /// that wrote it, and saying *which* backend is missing beats "could not open /// library". pub fn provider_for(account: &Account) -> Result<&'static Arc, RemoteError> { registry().for_account(account) } /// Whether an account's credential has to be fetched from secure storage. /// /// Asked of the connector rather than inferred from the account, because an /// empty login might be a folder library or might be a damaged record, and /// guessing turns the second into a silent unauthenticated connection. pub fn needs_secret(account: &Account) -> bool { provider_for(account).is_ok_and(|p| p.sign_in().needs_secret()) } /// Open a connection to a configured remote. /// /// Returns the trait object every caller should hold. The error type is /// `dr-sync`'s rather than a connector's, so a caller handles a failure /// without learning which backend produced it. pub(crate) fn connect(conn: &Connection) -> Result, RemoteError> { registry().connect(conn) } #[cfg(test)] mod tests { use super::*; #[test] fn both_backends_are_registered() { // The list the launch screen offers. A backend missing from here is a // backend the user cannot choose, however complete its connector is. let ids: Vec<&str> = registry().providers().iter().map(|p| p.id()).collect(); assert!(ids.contains(&"nextcloud"), "{ids:?}"); assert!(ids.contains(&"folder"), "{ids:?}"); } #[test] fn only_the_backend_with_a_login_wants_a_credential() { assert!(needs_secret(&Account::new("nextcloud", "https://x"))); assert!(!needs_secret(&Account::new("folder", "/mnt/photos"))); } #[test] fn an_account_for_an_unknown_backend_names_it() { let e = provider_for(&Account::new("s3", "bucket")) .err() .expect("no such connector") .to_string(); assert!(e.contains("s3"), "{e}"); } }