
WebDAV
Storage
Source: packages/laikacms/src/impl/storage-webdav
A StorageRepository implementation backed by any RFC 4918 WebDAV server — Nextcloud, ownCloud, Apache mod_dav, rclone serve webdav, hacdias/webdav, and friends. Runtime-agnostic: the only requirement is a fetch implementation, so it runs on Node, Bun, Deno, Cloudflare Workers, and the browser.
Why WebDAV?
WebDAV maps almost one-to-one onto Laika's storage contract: collections are folders, resources are objects, and PROPFIND/GET/PUT/DELETE/MKCOL cover every operation. If you already host a Nextcloud instance, a Synology NAS, or rclone serve webdav in front of S3, you can point Laika at it without standing up a new backend.
Usage
import { markdownSerializer } from 'laikacms/serializers/markdown';
import { WebDavStorageRepository } from 'laikacms/storage/webdav';
const repo = new WebDavStorageRepository(
{
baseUrl: 'https://cloud.example.com/remote.php/dav/files/alice',
basePath: 'laika-content', // optional subfolder under the user's root
auth: { username: 'alice', password: process.env.NEXTCLOUD_PASS },
},
{ md: markdownSerializer },
'md',
);
const stream = repo.listAtomSummaries('', { depth: 1, pagination: { offset: 0, limit: 50 } });Bearer-token auth
new WebDavStorageRepository(
{
baseUrl: 'https://dav.example.com',
auth: { token: process.env.DAV_TOKEN },
},
serializers,
'json',
);Extra request headers (auth.headers)
auth.headers is a plain object of header name → value pairs merged into every request alongside the standard auth credentials. Use it for server-specific requirements such as Nextcloud's OCS-APIRequest header:
// Nextcloud: add OCS-APIRequest header alongside basic auth
new WebDavStorageRepository(
{
baseUrl: 'https://cloud.example.com',
basePath: '/remote.php/dav/files/alice/',
auth: {
username: 'alice',
password: 'secret',
headers: { 'OCS-APIRequest': 'true' },
},
},
{ md: markdownSerializer },
'md',
);auth.headers can be combined with any auth style (basic, bearer, or anonymous). The extra headers are applied first; the Authorization header is always appended after them, so you cannot accidentally override credentials via auth.headers.
Custom fetch
Pass fetch for non-standard runtimes or to inject a test double:
new WebDavStorageRepository(
{ baseUrl: '...', fetch: myInstrumentedFetch },
serializers,
'json',
);Custom extension policy
The optional 4th argument overrides how the on-server file extension is chosen when writing a new object. The default behaviour is metadata.extension ?? defaultFileExtension.
import { markdownSerializer } from 'laikacms/serializers/markdown';
import { yamlSerializer } from 'laikacms/serializers/yaml';
import { WebDavStorageRepository } from 'laikacms/storage/webdav';
// Always store as .yaml regardless of metadata.extension or defaultFileExtension.
new WebDavStorageRepository(
{
baseUrl: 'https://cloud.example.com/remote.php/dav/files/alice',
basePath: 'laika-content',
auth: { username: 'alice', password: process.env.NEXTCLOUD_PASS },
},
{ md: markdownSerializer, yaml: yamlSerializer },
'md',
(_key, { metadata, defaultExtension }) => metadata?.extension ?? 'yaml',
);The callback receives the object key and a context object with:
metadata— optional metadata from the create/update call (may contain anextensionhint)defaultExtension— the storage's configureddefaultFileExtension
Returning undefined falls back to defaultFileExtension. Returning a non-undefined string that has no registered serializer causes createObject / createOrUpdateObject to fail immediately with a BadRequestError listing the available extensions — the object is not written.
Behaviour notes
- Extension hiding. Keys are extension-free at the boundary, exactly like
storage-fs. The on-server file extension is chosen viadetermineExtension(default:metadata.extension ?? defaultFileExtension) and looked up on read by probing each registered serializer extension. - Parent collections.
PUTandcreateFolderissueMKCOLtop-down so deeply nested writes succeed against servers that require parents to exist (most of them). - Safe deletes.
removeAtomsrefuses to delete a non-empty collection — even though WebDAVDELETEon a collection is recursive — so an accidental folder delete cannot wipe nested content. The refusal is surfaced as a stream warning, not a fatal error. - Pagination. Cursor pagination is not supported (WebDAV
PROPFINDreturns the full listing); offset and page styles are emulated in memory after a natural-order sort. - Listings on missing folders are reported as
recoverableErrors(aNotFoundError), matching every otherStorageRepositoryimplementation.
What this does not do
- No
LOCK/UNLOCK. Concurrent writers will race; if your server enforces locking you'll see423 Lockedcome back as aConflictError. - No multipart/chunked upload — objects are written with a single
PUTof the serialized body. Use a different repository if you store very large blobs. - No streaming reads.
GETbodies are buffered into memory before deserialization. Suitable for documents, not for large binary assets — use the assets API for those.
Compatibility
Tested against the parser fixtures Nextcloud, Apache mod_dav, and rclone serve webdav produce. The XML parser is namespace-agnostic, so servers that emit D:, d:, or no prefix at all all work.