Let the interface hold a backend without knowing whose it is

`dr-sync` defines `RemoteBackend` and a capability model the engine adapts
to, so a second backend can be added without touching the code that uses
one. That boundary was documentation. Seven files in `dr-ui` constructed a
`NextcloudBackend` directly, ten functions took one by concrete type, and
exactly two call sites in the tree — both inside `dr-sync` itself — ever held
the trait object. A WebDAV or local-folder backend would have had a
well-written trait to implement and nowhere to go afterwards.

The change is smaller than the finding suggests, because the trait was
already right. Every method the UI has ever called on a backend — `get`,
`put`, `list`, `delete`, `create_dir`, `move_to` — was already on it, so
nothing had to be added and no behaviour moved. Ten signatures widened to
`&dyn RemoteBackend`, sixteen constructions became `remote::connect`, and
`remote.rs` is now the only file in the interface that names a connector.

`connect` returns `Result<Box<dyn RemoteBackend>, RemoteError>`. The error
type is `dr-sync`'s rather than the connector's, which is why every call site
kept its shape — the `match`, the `let Ok(..) else`, and
`.map_err(ScanFailure::local)?` all still read as they did.

One wrinkle worth recording: `&Box<dyn Trait>` does not reach `&dyn Trait` on
its own. The compiler reaches for unsizing, which wants
`Box<dyn RemoteBackend>: RemoteBackend`, and reports a confusing missing impl
rather than suggesting a deref. Twelve call sites therefore say `&*backend`,
and two say `let backend: &dyn RemoteBackend = &*backend` where a borrow is
shared across lanes.

What this does *not* do is abstract credentials. `AppCredentials` is an app
password from Login Flow v2 — a Nextcloud protocol, not a general notion of
authenticating to a remote — and seven files still name it. An OAuth token, a
bucket key pair and an app password have no useful common shape, so deciding
what an account is across backends before a second one exists would be a
confident guess. code-health.md CH-2 now records that as the remaining half,
and it should wait for the backend that forces it.

Verified: fmt clean, clippy clean at -D warnings, 2041 tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 19:51:23 +02:00
co-authored by Claude Opus 5
parent 617262b4da
commit 242374fd0f
11 changed files with 127 additions and 75 deletions
+5 -5
View File
@@ -56,7 +56,7 @@ use std::sync::Arc;
use dr_ingest::{Candidate, DupKey, Imported, Ingest, Options, Report, Shot, TransferMode};
use dr_plat::{DirRef, LocalStorage, Storage, WritableStorage};
use dr_sync::{RemoteBackend, RemotePath};
use dr_sync_nextcloud::{AppCredentials, NextcloudBackend};
use dr_sync_nextcloud::AppCredentials;
use dr_types::{FormatFilter, RootId};
/// Which root the card is granted as, and which the library is.
@@ -385,7 +385,7 @@ fn upload_all(
};
rt.block_on(async {
let backend = match NextcloudBackend::new(&upload.credentials, &upload.user_id) {
let backend = match crate::remote::connect(&upload.credentials, &upload.user_id) {
Ok(b) => b,
Err(e) => {
log::warn!("connecting to upload: {e}");
@@ -423,7 +423,7 @@ fn upload_all(
// The same folder segments the local copy went into, so the two
// libraries have the same shape (FR-NC-7a).
match dr_sync::upload_original(&backend, &root, &image.folders, &image.name, bytes)
match dr_sync::upload_original(&*backend, &root, &image.folders, &image.name, bytes)
.await
{
Ok(placed) => {
@@ -445,7 +445,7 @@ fn upload_all(
Err(e) => log::warn!("uploading {}: {e}", image.name),
}
}
let filed = file_thumbnails(&backend, &root, imported, &sent, &upload.thumbs).await;
let filed = file_thumbnails(&*backend, &root, imported, &sent, &upload.thumbs).await;
(sent, filed)
})
}
@@ -502,7 +502,7 @@ fn make_thumbnail(bytes: &[u8]) -> Option<dr_thumbs::Thumbnail> {
/// Best-effort throughout. A thumbnail that cannot be filed costs the grid one
/// preview fetch later; failing the import over it would be the wrong trade.
async fn file_thumbnails(
backend: &NextcloudBackend,
backend: &dyn RemoteBackend,
library: &RemotePath,
imported: &[Imported],
sent: &[Sent],