Make storage pluggable, and prove it with a folder backend
`RemoteBackend` existed from the first release and bought nothing it was
designed for. Seven files in `dr-ui` constructed a `NextcloudBackend`
directly, an account *was* a server URL beside a DAV user id, the local
cache directory was named after a hostname, and the launch screen knew
that signing in meant a browser handshake. The trait was real; the seam
was documentation.
A trait over operations is only a quarter of it. Pluggable storage needs
four things, and this adds the other three:
- **Capabilities** — already there, and the reason the engine can drive
two backends at the speed each actually runs at.
- **Configuration** — `dr_sync::Account`: where a library lives, in
whatever form its connector addresses, with no server in it. Loads
every existing config unchanged (`backend` defaults to `nextcloud`,
`endpoint` is stored under its historical `server` key), and
`Account::namespace()` reproduces the old catalog directory byte for
byte, because changing it would abandon a catalog, its thumbnail
shards, and the sidecars holding unsynced offline work.
- **Registration** — `BackendProvider` and `BackendRegistry`.
`ui/dr-ui/src/remote.rs` is now the only file above `dr-sync` that
names a connector.
`Connection` (an account plus an optional `Secret`) replaces the
credentials-and-user-id pair that was threaded through fifteen
signatures in an order that could be swapped. `Secret`'s inner string is
reachable only through `expose()` and its `Debug` prints `Secret(***)`,
so the indirect leak — a `{:?}` on anything holding one — no longer
compiles into a leak.
Nextcloud is unchanged and keeps every peculiarity: propagating ETags,
chunked upload v2, `oc:fileid`, the `oc:permissions` probe on a refused
PUT, the 423 retry classification, Login Flow v2. Those are what the
capability model exists to serve, not something to hide.
`dr-sync-folder` is the second connector: a local disk, a network mount,
an external drive, or a folder a Nextcloud client already syncs. No
account, no credential — the route that works where no secrets daemon
does. It declares `LocalEtags` rather than claiming propagation a POSIX
directory cannot provide, which costs nothing because 50k `stat` calls
are not 50k PROPFINDs. Identity is a path hash, not an inode: an inode
survives a rename but differs between devices and is reused after a
delete, so two machines would disagree about which photograph a
thumbnail belonged to. Re-deriving a thumbnail is a cost; showing the
wrong one is a bug.
docs/storage.md is the contract — the traits, the four steps to add a
backend, and what each connector declares. ARCH §8.0 and §8.4a, and
FR-NC-13, say why.
This commit is contained in:
@@ -33,6 +33,7 @@ use slint::ComponentHandle;
|
||||
|
||||
use crate::settings_store::SettingsStore;
|
||||
use crate::AppWindow;
|
||||
use dr_sync::Connection;
|
||||
|
||||
/// Shared settings state for the running window.
|
||||
pub struct SettingsController {
|
||||
@@ -53,7 +54,8 @@ pub struct SettingsController {
|
||||
/// it browses a remote tree and nothing about it is specific to what the
|
||||
/// chosen folder is *for*. `None` means the picker is closed, which is
|
||||
/// also the only state a device destination ever has — a path on this
|
||||
/// machine is typed or chosen by the platform, not walked over WebDAV.
|
||||
/// machine is typed or chosen by the platform, not walked through a
|
||||
/// backend.
|
||||
pub browser: RefCell<Option<crate::launch::FolderBrowser>>,
|
||||
/// Polls the folder listing while one is in flight.
|
||||
///
|
||||
@@ -603,9 +605,9 @@ pub fn wire<F, G>(
|
||||
/// List the folders under `path`, for the export destination picker.
|
||||
///
|
||||
/// A near-twin of `launch_ui::spawn_folder_list` and deliberately not shared
|
||||
/// with it. That one reaches into the `LaunchController` for its session and
|
||||
/// reports failures onto the launch screen's error line; this one is handed
|
||||
/// credentials and writes to the settings page. Factoring them together would
|
||||
/// with it. That one reaches into the `LaunchController` for its account and
|
||||
/// reports failures onto the launch screen's error line; this one is handed a
|
||||
/// connection and writes to the settings page. Factoring them together would
|
||||
/// mean a function taking both controllers, or a trait implemented twice to
|
||||
/// abstract two call sites — more machinery than the twenty lines it saves.
|
||||
///
|
||||
@@ -615,8 +617,7 @@ pub fn wire<F, G>(
|
||||
pub fn spawn_folder_list(
|
||||
weak: slint::Weak<AppWindow>,
|
||||
ctl: Rc<SettingsController>,
|
||||
creds: dr_sync_nextcloud::AppCredentials,
|
||||
user_id: String,
|
||||
conn: Connection,
|
||||
path: String,
|
||||
) {
|
||||
use dr_sync::RemotePath;
|
||||
@@ -637,7 +638,7 @@ pub fn spawn_folder_list(
|
||||
return;
|
||||
};
|
||||
rt.block_on(async {
|
||||
match crate::remote::connect(&creds, &user_id) {
|
||||
match crate::remote::connect(&conn) {
|
||||
Ok(b) => match b.list(&RemotePath::new(&path), None).await {
|
||||
Ok(entries) => {
|
||||
let mut dirs: Vec<String> = entries
|
||||
|
||||
Reference in New Issue
Block a user