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:
@@ -0,0 +1,172 @@
|
||||
# =============================================================
|
||||
# OxiCloud — NextCloud HTTP PATCH partial content updates (RFC 5789)
|
||||
# =============================================================
|
||||
# The NextCloud-compatible WebDAV surface (/remote.php/dav/…) had no
|
||||
# PATCH dispatch arm at all (fell through to 405), unlike the plain-file
|
||||
# surface (see api/handlers/webdav_handler.rs::handle_patch). See
|
||||
# nextcloud/webdav_handler.rs::handle_patch, which reuses the plain
|
||||
# surface's `parse_update_range` and `upload_ingest::
|
||||
# ingest_range_patch_to_cas` — both surface-agnostic.
|
||||
#
|
||||
# Coverage:
|
||||
# 1. PATCH an explicit byte range (`X-Update-Range: bytes=<start>-<end>`)
|
||||
# → 204, Content-Range header, and the resulting content reflects
|
||||
# the patched span with the untouched prefix/suffix intact.
|
||||
# 2. PATCH with `X-Update-Range: append` → 204, content grows.
|
||||
# 3. PATCH with a Content-Range header → 400 (must use X-Update-Range).
|
||||
# 4. PATCH without X-Update-Range → 400.
|
||||
# 5. PATCH on a nonexistent file → 404.
|
||||
# =============================================================
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Setup 1 — JWT login (to mint the app password used for NC Basic Auth).
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/auth/login
|
||||
Content-Type: application/json
|
||||
{ "username": "{{username}}", "password": "{{password}}" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
jwt: jsonpath "$.access_token"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Setup 2 — Mint an app password for NC Basic Auth.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
POST {{base_url}}/api/auth/app-passwords
|
||||
Authorization: Bearer {{jwt}}
|
||||
Content-Type: application/json
|
||||
{ "label": "nc_webdav_patch hurl test" }
|
||||
|
||||
HTTP 200
|
||||
[Captures]
|
||||
nc_username: jsonpath "$.username"
|
||||
nc_password: jsonpath "$.password"
|
||||
ap_id: jsonpath "$.id"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 1 — Seed a 10-byte probe file: "0123456789".
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PUT {{base_url}}/remote.php/dav/files/{{nc_username}}/nc-patch-probe.txt
|
||||
[BasicAuth]
|
||||
{{nc_username}}: {{nc_password}}
|
||||
Content-Type: text/plain
|
||||
```
|
||||
0123456789
|
||||
```
|
||||
|
||||
HTTP 201
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 2 — PATCH bytes 3-5 ("345") with "XYZ".
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PATCH {{base_url}}/remote.php/dav/files/{{nc_username}}/nc-patch-probe.txt
|
||||
[BasicAuth]
|
||||
{{nc_username}}: {{nc_password}}
|
||||
X-Update-Range: bytes=3-5
|
||||
Content-Type: text/plain
|
||||
```
|
||||
XYZ
|
||||
```
|
||||
|
||||
HTTP 204
|
||||
[Asserts]
|
||||
header "Content-Range" == "bytes 3-9/10"
|
||||
|
||||
|
||||
GET {{base_url}}/remote.php/dav/files/{{nc_username}}/nc-patch-probe.txt
|
||||
[BasicAuth]
|
||||
{{nc_username}}: {{nc_password}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "012XYZ6789"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 3 — PATCH append.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PATCH {{base_url}}/remote.php/dav/files/{{nc_username}}/nc-patch-probe.txt
|
||||
[BasicAuth]
|
||||
{{nc_username}}: {{nc_password}}
|
||||
X-Update-Range: append
|
||||
Content-Type: text/plain
|
||||
```
|
||||
END
|
||||
```
|
||||
|
||||
HTTP 204
|
||||
[Asserts]
|
||||
header "Content-Range" == "bytes 10-12/13"
|
||||
|
||||
|
||||
GET {{base_url}}/remote.php/dav/files/{{nc_username}}/nc-patch-probe.txt
|
||||
[BasicAuth]
|
||||
{{nc_username}}: {{nc_password}}
|
||||
|
||||
HTTP 200
|
||||
[Asserts]
|
||||
body == "012XYZ6789END"
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 4 — Content-Range header on PATCH is rejected.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PATCH {{base_url}}/remote.php/dav/files/{{nc_username}}/nc-patch-probe.txt
|
||||
[BasicAuth]
|
||||
{{nc_username}}: {{nc_password}}
|
||||
X-Update-Range: bytes=0-2
|
||||
Content-Range: bytes 0-2/13
|
||||
Content-Type: text/plain
|
||||
```
|
||||
abc
|
||||
```
|
||||
|
||||
HTTP 400
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 5 — Missing X-Update-Range header.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PATCH {{base_url}}/remote.php/dav/files/{{nc_username}}/nc-patch-probe.txt
|
||||
[BasicAuth]
|
||||
{{nc_username}}: {{nc_password}}
|
||||
Content-Type: text/plain
|
||||
```
|
||||
abc
|
||||
```
|
||||
|
||||
HTTP 400
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Step 6 — PATCH on a nonexistent file → 404.
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
PATCH {{base_url}}/remote.php/dav/files/{{nc_username}}/nc-patch-does-not-exist.txt
|
||||
[BasicAuth]
|
||||
{{nc_username}}: {{nc_password}}
|
||||
X-Update-Range: append
|
||||
Content-Type: text/plain
|
||||
```
|
||||
abc
|
||||
```
|
||||
|
||||
HTTP 404
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# Cleanup
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
DELETE {{base_url}}/remote.php/dav/files/{{nc_username}}/nc-patch-probe.txt
|
||||
[BasicAuth]
|
||||
{{nc_username}}: {{nc_password}}
|
||||
|
||||
HTTP 204
|
||||
|
||||
|
||||
DELETE {{base_url}}/api/auth/app-passwords/{{ap_id}}
|
||||
Authorization: Bearer {{jwt}}
|
||||
HTTP 200
|
||||
Reference in New Issue
Block a user