Files
DarkRoom/ui/dr-ui/src/remote.rs
T
dtourolle 84fade99ec Put the developer docs under docs/dev and index the folder for users first
docs/ had 26 developer documents flat beside the manual, and the two
audiences are very differently sized: most readers want the manual and
the gesture reference, a few want the register, the designs and the
measurements. The manual and gestures.md stay at the top; everything for
someone changing the code moves to docs/dev/, and the two documents that
name their own successors — the v0.1 milestone and the UI-refinement plan
— go to docs/dev/archive/ rather than being deleted, since both are still
cited. docs/README.md is the index, users first.

Every reference follows: code comments, Cargo manifests, the workflows,
the pre-commit hook, the bench and traceability tools (which locate the
repo root by docs/dev/requirements.md now), packaging, the Docker READMEs,
CLAUDE.md, CONTRIBUTING.md and the README. The matrix links one level
deeper and is regenerated. Links out of the moved documents into the tree
gain a level; a link checker over every Markdown file finds none broken.
2026-09-20 21:16:03 +02:00

109 lines
4.6 KiB
Rust

// 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/dev/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<BackendRegistry> = 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<dyn dr_sync_folder::Vfs>)
})));
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<dyn BackendProvider>, 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<Box<dyn RemoteBackend>, 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}");
}
}