WebDAV
Crate hopf-webdav: WebDAV Class 1 + Class 2 filesystem backend as an HTTP ServerHandler (RFC 4918). It is not a standalone TCP service — wire it under hopf-http (CleartextHttpEndpoint / AlpnHttpEndpoint) on a Hopf Runtime. Blocking filesystem work runs on StorageExecutor.
Locks default to in-process per factory; dead properties default to xattrs (optional Cargo feature) or sidecar files next to their resources. Both can instead be moved out of the content tree — see WebDavConfig::lock_root / sidecar_root below — so several handlers can share one lock table and one property store for the same content, including a read-only content tree.
Contents
Standards
| Reference | Role |
|---|---|
| RFC 4918 | WebDAV Class 1+2; locks §6; If §10.4; Multistatus §13; dead props §4 |
| RFC 9110 | GET / HEAD file serving semantics |
| DAV: | Namespace DAV:; OPTIONS advertises DAV: 1,2 |
Architecture
HTTP endpoint (H1 / H2 / H3)
│
▼
WebDavFactory → WebDavHandler (ServerHandler)
│
├── StorageExecutor (FS read/write off reactor)
├── WebDavLockManager (in-memory, or file-backed under lock_root)
└── DeadPropertyStore (xattr | sidecar [next to resource | under sidecar_root] | none)
Auth is not implemented in this crate. Put HTTP Basic/Digest/Bearer (Auth / hopf-http auth factories) or ACL (Access control) around the listener.
Methods and Allow matrix
webdav_enabled |
allow_write |
Allow header |
|---|---|---|
| true | true | OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK |
| true | false | OPTIONS, GET, HEAD, PROPFIND |
| false | true | OPTIONS, GET, HEAD, PUT, DELETE |
| false | false | OPTIONS, GET, HEAD |
Method summary
| Method | Notes |
|---|---|
| OPTIONS | DAV: 1,2 when WebDAV enabled |
| GET / HEAD | Welcome files, weak ETag and Last-Modified, conditional requests (If-None-Match, If-Modified-Since, If-Match, If-Unmodified-Since answered with 304 or 412), optional Cache-Control via WebDavConfig::cache_control |
| PUT | Create / overwrite (write); honours If-None-Match: * (create-only) and If-Unmodified-Since before truncating the file. If-Match needs a strong ETag, and file ETags are weak, so it always answers 412 |
| DELETE | Remove (write); honours If-Unmodified-Since / If-None-Match before deleting |
| PROPFIND | allprop / propname / prop; Depth 0/1/infinity |
| PROPPATCH | Set / remove dead properties |
| MKCOL | Create collection |
| COPY / MOVE | Destination, Overwrite |
| LOCK / UNLOCK | Exclusive/shared; opaquelocktoken: |
Constants: MAX_WEBDAV_REQUEST_BODY = 1 MiB (PROPFIND/PROPPATCH/LOCK bodies); MAX_WEBDAV_PUT_BODY = 16 MiB (matches default HttpLimits::max_request_body — raise both for larger PUTs); DEFAULT_MAX_TREE_ENTRIES = 10 000.
Depth, headers, and locks
Supported request headers (as applicable): Depth (0 / 1 / infinity), Destination, Overwrite, If, Lock-Token, Timeout.
Depth: infinity (the PROPFIND default when Depth is omitted) is allowed, but PROPFIND and recursive COPY stop with 507 once WebDavConfig::max_tree_entries resources have been visited. Depth 0/1 are unchanged. MOVE still uses atomic rename (no tree walk).
Locks (WebDavLockManager):
| Knob | Value |
|---|---|
| Default timeout | 3600 s |
| Max timeout | 604800 s (7 days) |
| Token scheme | opaquelocktoken: |
| Scope / type | LockScope, LockType |
Locks are per WebDavFactory process by default — not shared across hosts. Setting WebDavConfig::lock_root (issue #415) switches WebDavLockManager to file-backed records under that directory instead, keyed by each resource's path relative to root_path: every handler pointed at the same lock_root then shares the same lock table, which is what a second handler process serving the same content tree needs (its in-memory table would otherwise start empty and grant a conflicting lock). Every lock operation then does blocking file I/O, so lock_root needs a filesystem where a create-new open is atomic and one handler's write is visible to the others before they finish their own conflict check — a local disk or a true ReadWriteOnce volume, not NFS's attribute-cached semantics.
Live and dead properties
Live properties (examples): creationdate, displayname, getcontentlength, getcontenttype, getetag, getlastmodified, lockdiscovery, resourcetype, supportedlock, …
Dead properties — DeadPropMode:
| Mode | Behaviour |
|---|---|
Auto |
Prefer xattr when available; else sidecar |
Xattr |
Extended attributes (needs Cargo feature xattr) |
Sidecar |
Files .webdav_*, normally next to the resource; with WebDavConfig::sidecar_root set (issue #415), the sidecar for a resource is instead the file at its path relative to root_path, mirrored under sidecar_root — nothing is written into the content tree, and a content file actually named .webdav_* is never mistaken for one |
None |
No dead-prop persistence |
Configuration
WebDavConfig (Default)
| Property | Type | Default | Notes |
|---|---|---|---|
root_path |
PathBuf |
"." |
Document root |
allow_write |
bool |
false |
Mutating methods |
webdav_enabled |
bool |
false |
DAV methods + DAV: header |
welcome_file |
String |
"index.html" |
Comma-separated list OK |
dead_property_storage |
DeadPropMode |
Auto |
See above |
max_put_body |
u64 |
16 MiB | PUT size cap (align with HTTP max_request_body) |
max_tree_entries |
usize |
10 000 | Depth: infinity PROPFIND / recursive COPY; 507 when exceeded |
allow_unauthenticated_access |
bool |
false |
Required when write/WebDAV enabled without acknowledging no built-in auth |
lock_root |
Option<PathBuf> |
None |
File-backed, cross-handler-shared lock records under this directory instead of in-memory (issue #415); see with_lock_root |
sidecar_root |
Option<PathBuf> |
None |
Dead-property sidecars mirrored under this directory instead of next to their resources (issue #415); see with_sidecar_root |
Cargo features
| Feature | Effect |
|---|---|
| (default) | No xattr dependency |
xattr |
Real xattr backend |
integration |
Integration tests |
Wiring
use std::sync::Arc;
use hopf_core::{Runtime, RuntimeConfig, StorageConfig, TcpListenerConfig};
use hopf_http::{CleartextHttpEndpoint, HttpLimits};
use hopf_webdav::{WebDavConfig, WebDavFactory, DeadPropMode};
let rt = Arc::new(Runtime::start(RuntimeConfig {
storage: StorageConfig::default(),
..RuntimeConfig::default()
})?);
let cfg = WebDavConfig {
root_path: "/var/webdav".into(),
allow_write: true,
webdav_enabled: true,
welcome_file: "index.html".into(),
dead_property_storage: DeadPropMode::Auto,
};
let factory = Arc::new(WebDavFactory::new(cfg, Arc::clone(rt.storage()))?);
let limits = HttpLimits::default();
let endpoint_factory = {
let f = Arc::clone(&factory);
Arc::new(move || {
Box::new(CleartextHttpEndpoint::new(
Arc::clone(&f) as Arc<dyn hopf_http::ServerHandlerFactory>,
limits,
)) as Box<dyn hopf_core::ProtocolHandler>
})
};
rt.add_tcp_listener(TcpListenerConfig::new(
"127.0.0.1:8080".parse()?,
endpoint_factory,
))?;For HTTPS, use the same stack as HTTP TLS + ALPN: acceptor_from_pem with &[b"h2", b"http/1.1"], AlpnHttpEndpoint as the handler factory, TcpListenerConfig::with_tls(acceptor). See HTTP server and TLS → Wiring.
WebDavFactory::new creates the root directory if needed, builds the Allow header from flags, installs a default MIME map, and shares one lock manager + dead-prop store across handlers from that factory.
Parser / multistatus APIs
Useful lower-level types (also for tests / custom handlers):
| Type | Purpose |
|---|---|
PropfindRequest / PropfindType |
PROPFIND body model |
ProppatchRequest / PropertyUpdate |
PROPPATCH |
LockRequest |
LOCK body |
WebDavRequestParser / parse_webdav_body |
XML parse |
MultistatusWriter / ResponseWriter |
207 responses |
MultiStatusParser / parse_multistatus |
Client-side 207 |
canonicalize_path / resolve_path_lexical |
Path safety |
Examples
See Cookbook: WebDAV.
# Cleartext WebDAV+write (example forces webdav_enabled; write default true in example)
WEBDAV_ROOT=/tmp/dav WEBDAV_WRITE=1 \
cargo run -p webdav -- 127.0.0.1:8080 /tmp/dav| Package | Demonstrates | Knobs |
|---|---|---|
examples/webdav |
Cleartext DAV + write | Optional addr + root; WEBDAV_ROOT; WEBDAV_WRITE=1/true |
Note: the example defaults WEBDAV_WRITE to true, while WebDavConfig defaults allow_write to false.
Limitations
- Locks are in-memory per factory by default (not clustered); set
WebDavConfig::lock_rootto share file-backed lock records across handlers pointed at the same content, but only on a filesystem with atomic create-new and immediately-visible writes (a local disk, not NFS). - No in-crate auth or ACL — compose with HTTP auth /
PeerAcl. - Feature
xattrrequired for real xattrs;Autofalls back to sidecar. - Not a standalone service crate; always behind HTTP.
- No Gumdrop-style separate WebDAV RBAC surface in this crate.