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:
@@ -175,6 +175,7 @@ export component AppWindow inherits Window {
|
||||
in property <string> launch-account: "";
|
||||
in property <string> launch-root: "";
|
||||
in property <string> launch-server: "";
|
||||
in property <string> launch-folder: "";
|
||||
in property <bool> launch-busy: false;
|
||||
in property <string> launch-status: "";
|
||||
in property <string> launch-error: "";
|
||||
@@ -184,6 +185,8 @@ export component AppWindow inherits Window {
|
||||
in-out property <[bool]> launch-format-checked;
|
||||
|
||||
callback launch-sign-in(string);
|
||||
/// The path of a folder library — no account, no credential.
|
||||
callback launch-use-folder(string);
|
||||
/// server, username, app password
|
||||
callback launch-sign-in-direct(string, string, string);
|
||||
callback launch-sign-out();
|
||||
@@ -1228,6 +1231,7 @@ in property <bool> panel-visible: true;
|
||||
account: root.launch-account;
|
||||
library-root: root.launch-root;
|
||||
server-url: root.launch-server;
|
||||
folder-path: root.launch-folder;
|
||||
busy: root.launch-busy;
|
||||
status: root.launch-status;
|
||||
error: root.launch-error;
|
||||
@@ -1237,6 +1241,7 @@ in property <bool> panel-visible: true;
|
||||
format-checked: root.launch-format-checked;
|
||||
|
||||
sign-in(server) => { root.launch-sign-in(server); }
|
||||
use-folder(path) => { root.launch-use-folder(path); }
|
||||
sign-in-direct(server, user, pw) => {
|
||||
root.launch-sign-in-direct(server, user, pw);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ export component LaunchScreen inherits Rectangle {
|
||||
in property <string> account: "";
|
||||
in property <string> library-root: "";
|
||||
in property <string> server-url: "";
|
||||
// Two endpoints, shown together. Someone deciding between a server and a
|
||||
// folder should not have to clear one field to try the other.
|
||||
in property <string> folder-path: "";
|
||||
in property <bool> busy: false;
|
||||
in property <string> status: "";
|
||||
in property <string> error: "";
|
||||
@@ -58,6 +61,9 @@ export component LaunchScreen inherits Rectangle {
|
||||
|
||||
// --- events out ---
|
||||
callback sign-in(string);
|
||||
// A folder library: no browser, no credential, no waiting state — the
|
||||
// whole sign-in is checking the directory is there.
|
||||
callback use-folder(string);
|
||||
/// server, username, app password
|
||||
callback sign-in-direct(string, string, string);
|
||||
callback sign-out();
|
||||
@@ -114,7 +120,7 @@ export component LaunchScreen inherits Rectangle {
|
||||
Label {
|
||||
text: root.signed-in
|
||||
? "Connected"
|
||||
: "Connect a Nextcloud account to begin";
|
||||
: "Connect a Nextcloud account, or open a folder";
|
||||
body: true;
|
||||
}
|
||||
}
|
||||
@@ -208,6 +214,46 @@ export component LaunchScreen inherits Rectangle {
|
||||
text: "Create one in Nextcloud under Settings › Security › Devices & sessions. It is device-scoped and can be revoked on its own.";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
|
||||
// --- or: a folder on this machine ---
|
||||
//
|
||||
// A local disk, a mounted share, or the folder the
|
||||
// Nextcloud desktop client already syncs. No account and
|
||||
// no credential, so this is the route that works on a
|
||||
// machine with no keyring at all.
|
||||
HorizontalLayout {
|
||||
spacing: Theme.gap;
|
||||
alignment: center;
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.rule;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
Caption { text: "or"; }
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.rule;
|
||||
horizontal-stretch: 1;
|
||||
}
|
||||
}
|
||||
|
||||
PanelHeading { text: "FOLDER"; }
|
||||
folder-input := Field {
|
||||
text: root.folder-path;
|
||||
placeholder: "/home/you/Pictures";
|
||||
accepted(path) => { root.use-folder(path); }
|
||||
}
|
||||
|
||||
FormButton {
|
||||
text: "Open folder";
|
||||
enabled: !root.busy && folder-input.text != "";
|
||||
clicked => { root.use-folder(folder-input.text); }
|
||||
}
|
||||
|
||||
Caption {
|
||||
text: "Any folder this machine can read: a local disk, a network mount, or one your Nextcloud client already syncs. Nothing is uploaded and no password is needed.";
|
||||
wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
// --- login pending: the browser step ---
|
||||
|
||||
@@ -949,7 +949,7 @@ component HeaderActions inherits HorizontalLayout {
|
||||
text: root.exporting
|
||||
? "Cancel export"
|
||||
: (root.export-to-server
|
||||
? "Export " + root.selected-count + " to Nextcloud"
|
||||
? "Export " + root.selected-count + " to the library"
|
||||
: "Export " + root.selected-count);
|
||||
active: root.exporting;
|
||||
y: root.centred ? (root.row-height - self.height) / 2 : 0;
|
||||
|
||||
Reference in New Issue
Block a user