mholt

caddy-webdav

WebDAV handler module for Caddy

by mholt

Go

Go100%

Commit activity

Last 12 weeks · 0 commits

Full graph →

Community health

2 of 6 standards met

Community profile →
42
✓README✓License○Contributing○Code of Conduct○Issue Template○PR Template

Recent PRs & issues

Quiet · 4 in progress · Last activity 8mo ago
See all on GitHub →
Rirmach

[!NOTE] This PR was assisted by AI, but the top-level design and key trade-offs were led by the contributor. The code itself has been reviewed. PR Description: Add Atomic Upload Support to Caddy WebDAV 1. Summary This PR introduces an optional Atomic Upload mechanism for . When enabled, PUT requests are first staged as temporary files and then atomically moved to their final destination only after the upload completes successfully, ensuring readers never see incomplete files. This feature is disabled by default and is fully backward-compatible with upstream behavior. 2. Major Changes 2.1 Core Feature: Disabled by default: When off, behavior is identical to upstream (direct writes, no temp directory overhead). Temp file staging: PUT uploads are intercepted by / and written to a temporary file created via . Atomic publish: On , the file is moved to its final path via ; if the temp directory and destination reside on different filesystems (), it automatically falls back to a destination-side copy + atomic rename. Scope limited: Atomic upload only applies to full overwrite writes (). Append and partial-update operations pass through directly without staging. 2.2 Client Disconnect & Integrity Detection Introduces + : wraps the request body to record read-side failures (client disconnects, network interruptions). performs three layers of validation before publishing: 1. Whether a read-side error occurred () 2. Whether the declared matches bytes actually written (defense against silent proxy truncation) 3. Whether any write error occurred () On any failure, the temp file is discarded and the destination path remains untouched. 2.3 Dynamic Root & Temp Directory Lifecycle Root resolved per request: Consistent with upstream behavior; supports dynamic roots using placeholders (e.g., multi-tenant setups). TempFileDir resolved exactly once: Determined on the first request and fixed thereafter for the process lifetime. Configuration constraint: If is enabled and contains a placeholder, must be explicitly set; otherwise provisioning will hard-fail. This prevents an unbounded number of temp directories (one per potential root) that could not be covered by a single cleanup routine. Default temp directory: When is static and is omitted, a subdirectory is created automatically under . Configuration Matrix () 2.4 Temp File Cleanup Age threshold: 7 days (). Leftover temp files exceeding this age are considered orphaned and removed. Cleanup interval: Every 24 hours (), executed in the background via . Deduplication: Uses to ensure multiple WebDAV instances sharing the same temp directory do not scan it concurrently. Leak prevention: The cleanup goroutine is bound to ; it exits cleanly on config reload / unload, preventing goroutine leaks. 2.5 Performance Optimizations Dual buffer pools: For the copy fallback, buffer size is selected based on file size: ≤ 128KB: 128KB buffer (small-file optimization, lower memory footprint) \> 128KB: 1MB buffer (large-file optimization, higher copy throughput) Uses to recycle buffers and avoid per-upload heap allocations. 2.6 Logging & Observability Client upload aborts () are downgraded from to , reducing noise in production logs. When atomic upload is enabled, the resolved path is logged once at level for operational verification. Cleanup failures are logged per-file at level without aborting the overall process. 2.7 Caddyfile Support New subdirective (boolean flag, no arguments). New subdirective. Duplicate detection and error handling follow the existing code style. 3. Design Decisions & Trade-offs Why is disabled by default? To maintain 100% behavioral compatibility with upstream. Temp directories, staging mechanics, and cleanup goroutines are only activated when explicitly opted in—zero intrusion. Why must dynamic roots explicitly set ? Allowing each potential root to auto-generate its own temp directory would result in multiple directories that a single cleanup goroutine cannot cover, leading to resource leaks. Requiring explicit configuration pushes this complexity to the user in exchange for predictable lifecycle management. Why is resolved only once? WebDAV is a stateful file protocol that requires a stable filesystem view. The temp directory serves as a global staging area whose lifecycle must be independent of individual requests. If you absolutely need a per-request staging directory, this is not currently supported due to the complexity of managing cleanup lifecycles across dynamic paths; leave disabled instead. Why a 7-day age threshold? This is long enough to cover abnormal scenarios such as process crashes, temporary network filesystem unavailability, and container orchestration delays, while preventing indefinite disk bloat. Combined with the 24-hour cleanup interval, orphaned files survive at most 7–8 days. Why is the default temp directory placed under ? The default temp directory () is intentionally co-located with the destination files. On typical deployments, this means the staging directory and the final destination reside on the same filesystem, allowing to complete as a cheap metadata-only operation. If the temp directory were placed on a separate filesystem (e.g., system ), every upload would incur the copy-fallback path—an unnecessary full read-write copy plus an additional temp file. Keeping them on the same device avoids this overhead by default. 4. Security Considerations Path traversal: anchors via , trims the leading slash, and joins with ; it explicitly rejects null bytes and misplaced Windows path separators. Race safety: calls on an already-open file handle, not a re-looked-up path; the final is atomic. Temp file naming: uses internally; filenames are unpredictable and not user-controllable. 5. Compatibility Backward compatible: Fully compatible. When the new directives are omitted or is left unset, behavior is identical to the original. JSON config: Adds (bool) and (string) fields. Caddyfile**: Adds and subdirectives. 6. Documentation Updates Updated with new sections covering Atomic Upload, configuration rules, temp file management, and permissions. Updated to support parsing of the new subdirectives.

Rirmach · 3d ago
zystem

Depends on #61 (its commit is the first commit here; review the second one, ). A into a collection that does not exist is refused today with 409 Conflict (from x/net, as RFC 4918 section 9.7.1 asks). Some clients cannot make the collections first, and an application that stores objects under made-up paths does not want to every level. This adds an opt-in that makes the missing parent directories. The default is unchanged. The name is cleaned as an absolute path () before it is joined to the root, so a in the request path cannot make a directory outside the root. A naive followed by can: creates above the root, even though the PUT itself is then refused (I found this in a fork that had such a change). The prefix of the share is checked on a path boundary ( does not match ). Tests (the module has none yet, hence #61): parents are made; refused without the option and nothing is created; no variant (, , , ) leaves the root; a path outside the prefix creates nothing; the Caddyfile parses . The atomic write that I would pair it with is left to #60. 🤖 Generated with Claude Code

zystem · 4d ago
zystem

Built on its own (, ) the module does not compile on any recent Go: brings quic-go v0.28. xcaddy never notices because it builds with the Caddy of the build, not the one in , but anything that builds the module by itself (including tests, of which there are none yet) fails. This raises the requirement to . and the toolchain line stay as they are; , and are clean. No code change. It is the base of the next pull request, which adds tests. 🤖 Generated with Claude Code

zystem · 4d ago
dsabala

Hi I am trying to use WebDAV to store my files and access them using two methods: anonymous read-only access through web browser WebDAV access using Linux Mint Nemo file explorer (3 users: admin, uploader, viewer) This is my Dockerfile Caddyfile docker-compose.yml tree This does not work And this works Do you have any hints how to solve it so I can access root directory of my web file server using WebDAV ? And maybe how to simplify my Caddyfile?

dsabala · 12h ago
griffi-gh

Would you consider doing tagged releases at any point on github? It would make it much easier to use with on NixOS, as the currently packages that don't have tags get implicitly updated to latest commit occasionally, which breaks builds until hash is updated manually

griffi-gh · 7mo ago
q2dg

I don't know if/how to use Xcaddy in order to get webdav module running. I just want to start a simple webdav server and it's a pity this module doesn't comes prepacked Could you add a brief section in order to get a Caddy server with this module integrated? Something like this: https://github.com/mholt/caddy-l4?tab=readme-ov-file#compiling Thanks a lot

q2dg · 8mo ago
Structured data for AI agents

Repository: mholt/caddy-webdav. Description: WebDAV handler module for Caddy Stars: 316, Forks: 38. Primary language: Go. Languages: Go (100%). License: Apache-2.0. Topics: caddy, webdav, webdav-server. Open PRs: 4, open issues: 10. Last activity: 8mo ago. Community health: 42%. Top contributors: mholt, wader, francislavoie, mohammed90, Nicholas42, pgaskin, ledlamp, heimoshuiyu.