Add remote move and mkdir; distinguish 403 from 401

Soft delete needs to move a photograph into the trash folder and back, and
the stable id must survive the trip. WebDAV MOVE is one request and preserves
oc:fileid; a copy-then-delete would allocate a new one, orphaning the
thumbnail shard entry and the sidecar mapping and turning a restore into a
full re-download. Overwrite: F, because a header that permits overwriting is
one that eventually does.

create_dir does MKCOL outermost-first and treats 405 — Nextcloud's answer for
an existing collection — as the goal state rather than an error. Nothing else
creates the trash folder, so without it the first trashed image of every
library fails with a 409 that reads like a permission problem.

PermissionDenied is now separate from AuthFailed. Folding 403 into 401 sent a
user to re-check a credential that was working perfectly, with reads
succeeding and only the write refused (observed against a real server). The
usual cause is an app password created without "Allow filesystem access" —
which signing in again will not fix.

Assisted-by: LLM
This commit is contained in:
2026-08-09 20:55:04 +02:00
parent 57f8d42a5c
commit 81e89de7ea
4 changed files with 230 additions and 2 deletions
+23
View File
@@ -115,6 +115,29 @@ pub trait RemoteBackend: Send + Sync {
async fn delete(&self, id: &RemoteId, precond: Option<Precondition>)
-> Result<(), RemoteError>;
/// Move an object, keeping its identity.
///
/// TRACES: FR-CAT-15
/// **The stable id must survive.** This is what a soft delete uses to put a
/// photograph in the trash folder, and what a restore uses to bring it back.
/// A move implemented as copy-then-delete would allocate a *new*
/// `oc:fileid`, which orphans the thumbnail shard entry and the sidecar
/// mapping and turns a restore into a full re-download. WebDAV `MOVE` is one
/// request and preserves the id, which is why this is its own method rather
/// than something the caller composes.
///
/// Creates missing parent directories of `to`: the trash folder does not
/// exist until the first image is trashed, and requiring the caller to
/// create it separately makes the first trash of every library a two-step
/// dance with a failure mode in the middle.
async fn move_to(&self, from: &RemoteId, to: &RemotePath) -> Result<(), RemoteError>;
/// Create a directory, and any missing parents.
///
/// Succeeds if it already exists — callers use this to guarantee a
/// destination, not to claim they created it.
async fn create_dir(&self, path: &RemotePath) -> Result<(), RemoteError>;
// ---- optional ---------------------------------------------------------
/// Server-rendered thumbnail, where available.