Documentation
WebDAV behavior and storage reference
This page covers Everlock's WebDAV behavior and the storage model behind it.
What it supports
- served through
frontend-http - multi-share and multi-vhost, each share on its own mount path
- one dedicated
versioned-storagestore per share - Everlock-user HTTP authentication (Basic)
- Everlock access control on
http/files/<share> DAV: 1, 2— collections, files, and exclusive write locksRangerequests,ETag, andLast-Modifiedon every file- conditional requests, so two clients cannot silently overwrite each other
- properties a client sets, kept beside the resource they describe
- a media type per file, and an HTML listing for a browser pointed at a collection
- streaming reads and uploads: a large file is never held whole in memory
Methods
| Method | Behavior |
|---|---|
OPTIONS | advertises DAV: 1, 2 and the Allow list |
PROPFIND | Depth: 0 and Depth: 1, answered as a 207 multistatus |
PROPPATCH | sets and removes the properties a client owns |
GET / HEAD | file content, with Range answered 206; a listing for a collection |
PUT | 201 for a new file, 204 for a replaced one |
MKCOL | 201; 405 on something that exists; 409 with no parent collection |
DELETE | a file, or a collection and everything under it |
MOVE / COPY | Destination header; Overwrite: F refuses with 412; overlapping ends refuse with 403 |
LOCK / UNLOCK | exclusive write locks with a token and a timeout |
Depth: infinity on PROPFIND is refused: walking a whole share to answer one
request is how a file server falls over.
URL shape
https://files.example.com/dav/ https://files.example.com/dav/reports/ https://files.example.com/dav/reports/q1.pdf
The mount path defaults to /dav and is set per share with
/files set <name> mount=<path>.
Storage shape
A share is one versioned store, and the DAV path is the store path:
reports/q1.pdf reports/deep/notes.txt photos/2026/summer.jpg
Each request that changes something is one commit — a whole subtree move
included — so cloning the store and running git log reads as the sequence of
changes clients made. Nothing lands half-done: a transfer that fails leaves
both ends as they were.
Git has no empty directories, so a collection with nothing in it holds a
.keep marker file. The marker is never shown to a client: it appears when a
collection is created or loses its last file, and disappears when the
collection gains one.
Properties
PROPFIND answers what its body asks for — allprop, propname, or a named
list — and reports a property the resource does not have as 404 inside the
multistatus, leaving the rest of the answer intact.
The properties the store answers for are displayname, resourcetype,
getcontentlength, getcontenttype, getetag, getlastmodified,
creationdate, supportedlock, and lockdiscovery. They describe what is
stored, so they are read-only: a PROPPATCH that tries to set one is refused
with 403, and the rest of that update does not happen either.
The ETag of a file is the commit that last touched it, and Last-Modified is
that commit's timestamp — so both change exactly when the file does.
Anything else a client sets with PROPPATCH is kept exactly as written — text,
character data, nested markup with its attributes, and the xml:lang it was
written in. Windows stores file attributes this way, and Finder its own. Those properties live beside the
resource they describe, so they follow it through a MOVE, are copied by a
COPY, and go away when it is deleted — and they are versioned like
everything else in the share.
Conditional requests
A client that already has the current copy is told so rather than sent it again, and a client writing over a version it has not seen is refused:
| Header | On | Answer |
|---|---|---|
If-None-Match matching | GET, HEAD | 304 Not Modified |
If-Modified-Since | GET, HEAD | 304 Not Modified |
If-Match not matching | any write | 412 Precondition Failed |
If-None-Match: * on something that exists | any write | 412 Precondition Failed |
If-Range naming an older version | GET | the whole resource, not a piece of a newer one |
The DAV If header is evaluated in full — tagged and untagged lists, Not,
lock tokens and entity tags together — so a client can say "the lock I hold, on
the version I read" and be held to exactly that. A list tagged for one resource
speaks only for that resource.
Conditions apply where the RFCs put them: If-Match, If-None-Match, and an
untagged If list are about the resource the request is addressed to, even for
a COPY that writes elsewhere. A transfer's destination is held to what the
client tagged for it.
The condition is settled by the store as it commits, not before: two clients
that both read the same version and both send If-Match do not both succeed —
the one that arrives second is told the file moved on. A transfer carries a
condition for both of its ends, because both were read to plan it.
Media types and browsing
A file is served with the media type its name implies, so a browser shows a PDF
or an image instead of downloading it; anything unrecognised is
application/octet-stream, which is the safe answer for content someone else
uploaded.
GET on a collection answers with a plain HTML listing of its members. DAV
clients never ask — they use PROPFIND — so this is for the browser someone
inevitably points at the share.
Reserved names
.keep (the marker that keeps an empty collection present) and .davprops
(where properties live) belong to the server. A client neither sees them in a
listing nor reaches them by URL, and a write to one is refused with 403.
Locks
Locks are exclusive write locks. A LOCK returns a token in Lock-Token,
which the client presents in an If header on the writes that follow:
If: (<opaquelocktoken:…>)
A write that a lock covers, sent without the token, is refused with 423 Locked. A lock on a collection covers everything inside it. A LOCK on a name
that holds nothing creates an empty file to hold the lock, which is how clients
reserve a name before writing it.
Locks live in memory for as long as the server runs, and carry a timeout capped at one hour; a client that still holds the file refreshes it. A restart releases every lock rather than leaving one nobody can release.
A write registers itself with the lock table in the same step that checks it,
and stays registered until it commits, so a LOCK cannot be granted over a
write already on its way — that request is answered 409 Conflict and the
client retries.
Authentication and access
Basic authentication, resolved against Everlock users, on the access path
http/files/<share>:
| Method | Needs |
|---|---|
OPTIONS, PROPFIND, GET, HEAD | reader |
PUT, MKCOL, DELETE, MOVE, COPY, PROPPATCH, LOCK, UNLOCK | writer |
/users grant mika http/files/team writer /groups grant staff http/files/team reader
The user who creates a share owns it. Everyone else needs a grant — creating a share does not open it to anybody.
Discovery
A share on a .local vhost is announced over mDNS as _webdav._tcp, with the
mount path in its TXT record — the service type file managers browse for when
they list network shares, so the share can be reached without anyone typing a
URL.
Configuration
config/files-http.toml in the system store:
enabled = true [instances.team] store = "team-files" vhosts = ["files.example.com"] mount = "/dav"
Managed with /files create, /files set, /files unset, /files delete —
each applies immediately to the running server.