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.
109 lines
4.6 KiB
Rust
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}");
|
|
}
|
|
}
|