webdav
WebDAV Class 1 + 2 and CalDAV/CardDAV core for Nim
Made with the PowPow event library
nimble install webdav
WebDAV file sharing plus calendar and contact hosting in one embeddable server.
It runs on PowPow (async event loop,
HTTP/1 + HTTP/2), stores data through any
flysystem driver, and parses
iCalendar and vCard via openparser.
The extra HTTP verbs (PROPFIND, LOCK, REPORT, ...) are registered at
compile time through voodoo extensible
enums, so powpow itself stays generic.
Import rule:
import webdavmust come before any directimport powpow, so the DAV verbs are staged before powPow compiles.
General
- Built on PowPow, a fast event-driven networking library, so one process serves many clients at once without threads
- Quick downloads through zero-copy file serving
- Storage through Flysystem: sandboxed to its own folder, crash-safe writes, and ready for new backends such as cloud disks
- Calendars and contacts parsed with OpenParser, in the standard iCalendar and vCard formats
- Runs on Linux, macOS and Windows
File sharing
- Upload, download, copy, move and delete files and folders
- Folder listings with file details like size, type and modification time
- Custom metadata that sticks to files, even across copy and move
- Malformed requests are rejected safely
File locking
- Lock files so concurrent edits do not overwrite each other
- Shared and exclusive locks, with timeouts and tokens
- Locked files refuse changes until they are unlocked
Calendars
- Host calendars with events
- Search events by type and time range, fetch many at once
- Recurring events: daily, weekly, monthly and yearly
- Only valid calendar data gets stored
Contacts
- Host address books with contacts
- Search with field filters and text matching
- Sync support, so clients fetch only what changed
- One contact per file, duplicates rejected
- Standard vCard download format
Plumbing
- Any storage backend works, disk included, in-memory for tests
- No background threads, friendly to a single event loop
Client library
- One helper per operation, for files, calendars and contacts
- Helpers to build requests and read responses
clue build examples/dav_server.nim --out:bin/dav_server
./bin/dav_server ./davroot 9001 # args optional, these are the defaultsclue build # drops the server binary in bin/
./bin/webdav init # writes webdav.config.toml with defaults
./bin/webdav init --force # overwrite an existing config file
./bin/webdav serve # picks up ./webdav.config.toml when present
./bin/webdav serve --root=./davroot --port=9001 --address=127.0.0.1
./bin/webdav serve --config=webdav.config.toml # explicit flags override it
./bin/webdav --help # full help, flags and version (via kapsis)
./bin/webdav serve # no options, no config file: error, exit 1webdav.config.toml (all keys optional, as written by init):
root = "./davroot"
port = 9001
address = "127.0.0.1"# Class 1: upload and inspect
curl -X PUT http://localhost:9001/hello.txt -d 'hi' -i
curl -X PROPFIND http://localhost:9001/ -H 'Depth: 1' -i
# Locking: lock, fail without a token, succeed with one
TOK=$(curl -s -i -X LOCK http://localhost:9001/hello.txt \
-H 'Depth: 0' -H 'Content-Type: application/xml' \
-d '<D:lockinfo xmlns:D="DAV:"><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockinfo>' \
| grep -o 'Lock-Token: <[^>]*>')
curl -X PUT http://localhost:9001/hello.txt -d 'no' -i # 423
curl -X PUT http://localhost:9001/hello.txt -d 'yes' -H "If: (<${TOK#Lock-Token: <})" -i
# CalDAV: calendar, event, time-range query
curl -X MKCALENDAR http://localhost:9001/cal -i
curl -X PUT http://localhost:9001/cal/ev.ics -H 'Content-Type: text/calendar' \
-d 'BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example//EN
BEGIN:VEVENT
UID:ev1
DTSTAMP:20260101T000000Z
DTSTART:20260105T100000Z
DTEND:20260105T110000Z
SUMMARY:Hi
END:VEVENT
END:VCALENDAR' -i
curl -X REPORT http://localhost:9001/cal -H 'Depth: 1' \
-H 'Content-Type: application/xml' \
-d '<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav"><D:prop><D:getetag/><C:calendar-data/></D:prop><C:filter><C:comp-filter name="VCALENDAR"><C:comp-filter name="VEVENT"><C:time-range start="20260105T000000Z" end="20260106T000000Z"/></C:comp-filter></C:comp-filter></C:filter></C:calendar-query>' -i
# CardDAV: addressbook, contact, FN query
curl -X MKCOL http://localhost:9001/ab -H 'Content-Type: application/xml' \
-d '<D:mkcol xmlns:D="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav"><D:set><D:prop><D:resourcetype><D:collection/><CR:addressbook/></D:resourcetype><D:displayname>Contacts</D:displayname></D:prop></D:set></D:mkcol>' -i
curl -X PUT http://localhost:9001/ab/ada.vcf -H 'Content-Type: text/vcard' \
-d 'BEGIN:VCARD
VERSION:4.0
FN:Ada Lovelace
N:Lovelace;Ada;;;
END:VCARD' -i
curl -X REPORT http://localhost:9001/ab -H 'Depth: 1' \
-H 'Content-Type: application/xml' \
-d '<CR:addressbook-query xmlns:D="DAV:" xmlns:CR="urn:ietf:params:xml:ns:carddav"><D:prop><D:getetag/><CR:address-data/></D:prop><CR:filter><CR:prop-filter name="FN"><CR:text-match collation="i;unicode-casemap" match-type="contains">ada</CR:text-match></CR:prop-filter></CR:filter></CR:addressbook-query>' -iimport webdav # before powpow, see the import rule above
let srv = newDavServer(newLocalDriver("./davroot"))
newHttpServer().start(srv.davHandler(), Port(9001))Use newMemoryDriver() instead of newLocalDriver() for tests (see
tests/t_server_mem.nim for the loopback pattern).
import webdav
let dav = newDavClient("http://localhost:9001")
dav.mkcalendar("/cal").ensure(Http201)
dav.put("/cal/ev.ics", readFile("ev.ics")).ensure(Http201)
let found = dav.report("/cal",
buildCalendarQuery("VEVENT", "20260105T000000Z", "20260106T000000Z"),
).multistatus()
for r in found:
echo r.href
dav.closeClient()| Module | Job |
|---|---|
webdav/davmethod |
Compile-time verb registration (PROPFIND … REPORT, MKCALENDAR) |
webdav/types |
Shared DAV types |
webdav/davxml |
Hardened DAV XML parsing + multistatus builder |
webdav/backend |
flysystem pairing, dead props, calendar/addressbook markers |
webdav/props |
Live property computation |
webdav/locks |
Lock manager, Timeout/If parsing |
webdav/caldav |
REPORT parsing, time-range + recurrence matching |
webdav/carddav |
REPORT parsing, prop/param-filter + text-match matching |
webdav/server |
Request router (DavServer, davHandler) |
webdav/client |
Sync client (DavClient, builders, response helpers) |
webdav/config |
Server config (DavConfig, TOML overlay, flag precedence) |
clue build tests/t_davmethod.nim --out:/tmp/t_davmethod && /tmp/t_davmethod
clue build tests/t_davxml.nim --out:/tmp/t_davxml && /tmp/t_davxml
clue build tests/t_locks.nim --out:/tmp/t_locks && /tmp/t_locks
clue build tests/t_server_mem.nim --out:/tmp/t_server_mem && /tmp/t_server_mem
clue build tests/t_caldav.nim --out:/tmp/t_caldav && /tmp/t_caldav
clue build tests/t_carddav.nim --out:/tmp/t_carddav && /tmp/t_carddav
clue build tests/t_client.nim --out:/tmp/t_client && /tmp/t_client
clue build tests/t_cli.nim --out:/tmp/t_cli && /tmp/t_cli440+ checks total across unit suites and loopback servers (in-memory backend plus live curl runs against the disk-backed example).
Depth: infinityonPROPFINDis capped to depth 1PROPPATCHapplies best-effort in order (no atomic all-or-nothing)Ifevaluation is a subset (Notsupported, etag conditions ignored)- No auth or principal model yet (any valid lock token satisfies a lock)
GETon a collection answers403(no HTML listing view)- Recurrence and timezone handling follow the documented subset in
src/webdav/caldav.nim(clamped month overflow, UTC-normalized times) - CardDAV handling follows the documented subset in
src/webdav/carddav.nim(UID presence not required but unique when present, unknownaddress-dataversions fall back to stored bytes,sync-collectionkeeps no delete tombstones so deletions surface as a full resync, noprincipal-property-search/ACLs yet)
- WebDAV client to match the server
- CardDAV (requires
openparser >= 0.3.3for vCard support) - CalDAV
sync-collectionREPORT parity with CardDAV - Sync delete tombstones (404 entries instead of full resync)
- Discovery + principals (
/.well-known,current-user-principal,*-home-set,principal-property-search) - CalDAV scheduling and
free-busy-queryREPORTs - Auth + principal collections (
calendar-home-set,current-user-principal) - Full
Depth: infinityand atomicPROPPATCH - Collection listing view for
GET - Interop pass against real clients (Thunderbird, DAVx⁵, macOS)
- 🐛 Found a bug? Create a new Issue
- 👋 Wanna help? Fork it!
MIT license | Nim Community.