feat(webdav): extend HTTP PATCH to the NextCloud surface (RFC 5789)

The NextCloud-compatible WebDAV surface (/remote.php/dav/…) had no PATCH
dispatch arm at all — requests fell through to 405 — unlike the plain-file
surface (see the sibling commit on this repo's rfc-5789-http-patch work).

Adds handle_patch to nextcloud/webdav_handler.rs, reusing the plain
surface's X-Update-Range mechanism directly instead of duplicating it:
- api/handlers/webdav_handler.rs::parse_update_range is now pub(crate)
  so both surfaces share the same header-parsing/validation logic.
- upload_ingest::ingest_range_patch_to_cas (already surface-agnostic)
  splices the request body between the file's untouched prefix/suffix
  byte ranges and re-ingests through the same content-addressable
  pipeline handle_put uses.

Follows this file's own handle_put conventions rather than the plain
handler's: no active-lock guard (the NC surface has no LOCK/UNLOCK
dispatch arm at all) and no explicit storage-quota check (handle_put
doesn't do one either on this surface) — matching the sibling handler
instead of importing behavior the NC surface doesn't otherwise have.

Adds PATCH to the OPTIONS Allow header.

Also fixes a pre-existing clippy::useless_borrows_in_formatting warning
in thumbnail_service.rs (unrelated to this change, but blocking a clean
clippy run on this branch).

Adds tests/api/nc_webdav_patch.hurl covering explicit-range and append
PATCH, the Content-Range rejection, the missing-header 400, and PATCH
on a nonexistent file.
This commit is contained in:
M.Schmidt
2026-07-11 18:52:41 +02:00
parent d3546305f6
commit 93ae7ab142
3 changed files with 352 additions and 5 deletions
@@ -2006,7 +2006,11 @@ async fn handle_put(
/// range isn't supported, use `append` or PUT for that.
///
/// Returns `(start, end)`; `end` is `None` for `append`.
fn parse_update_range(header: &str, size: u64) -> Result<(u64, Option<u64>), AppError> {
///
/// `pub(crate)` so the NextCloud-surface PATCH handler
/// (`nextcloud/webdav_handler.rs::handle_patch`) can reuse it instead of
/// duplicating the parsing logic.
pub(crate) fn parse_update_range(header: &str, size: u64) -> Result<(u64, Option<u64>), AppError> {
let header = header.trim();
if header.eq_ignore_ascii_case("append") {
return Ok((size, None));