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.

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