Documentation

Last updated: 2026-08-26

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-storage store per share
  • Everlock-user HTTP authentication (Basic)
  • Everlock access control on http/files/<share>
  • DAV: 1, 2 — collections, files, and exclusive write locks
  • Range requests, ETag, and Last-Modified on 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

MethodBehavior
OPTIONSadvertises DAV: 1, 2 and the Allow list
PROPFINDDepth: 0 and Depth: 1, answered as a 207 multistatus
PROPPATCHsets and removes the properties a client owns
GET / HEADfile content, with Range answered 206; a listing for a collection
PUT201 for a new file, 204 for a replaced one
MKCOL201; 405 on something that exists; 409 with no parent collection
DELETEa file, or a collection and everything under it
MOVE / COPYDestination header; Overwrite: F refuses with 412; overlapping ends refuse with 403
LOCK / UNLOCKexclusive 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:

HeaderOnAnswer
If-None-Match matchingGET, HEAD304 Not Modified
If-Modified-SinceGET, HEAD304 Not Modified
If-Match not matchingany write412 Precondition Failed
If-None-Match: * on something that existsany write412 Precondition Failed
If-Range naming an older versionGETthe 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>:

MethodNeeds
OPTIONS, PROPFIND, GET, HEADreader
PUT, MKCOL, DELETE, MOVE, COPY, PROPPATCH, LOCK, UNLOCKwriter
/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.

files backend webdav dav storage