Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions developer_manual/app_development/dav_extension.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
.. _dav_extensions:

========================
Extending the DAV server
========================

Nextcloud apps can extend the DAV server by registering SabreDAV plugins that hook into
different phases of a DAV request. Plugins can add handlers for custom methods and
properties, adjust response behavior, and more. See `Writing Plugins - sabre/dav
<https://sabre.io/dav/writing-plugins/>`_ for additional possibilities.

Registering a DAV plugin
------------------------

To register a server plugin in your app, register an event listener for
``OCA\DAV\Events\SabrePluginAddEvent`` (introduced in Nextcloud 28). In the
listener's handler, add your DAV plugin to the server.

For example:

.. code-block:: php
:caption: MyApplication.php

class MyApplication extends App implements IBootstrap {
public function register(IRegistrationContext $context): void {
$context->registerEventListener(SabrePluginAddEvent::class, MyListener::class);
}
}

.. code-block:: php
:caption: MyListener.php

use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCA\DAV\Events\SabrePluginAddEvent;

class MyListener implements IEventListener {
public function handle(Event $event): void {
if (!$event instanceof SabrePluginAddEvent) {
return;
}
$server = $event->getServer();
$server->addPlugin(new MyDavPlugin());
}
}

Handling DAV events
-------------------

In this example, we register a handler for the ``propFind`` event and add a
custom property that is returned in PROPFIND requests.

.. code-block:: php
:caption: MyDavPlugin.php

use Sabre\DAV\ServerPlugin;
use Sabre\DAV\PropFind;

class MyDavPlugin extends ServerPlugin {

public function initialize(\Sabre\DAV\Server $server): void {
// Register your property handler
$server->on('propFind', $this->handleGetProperties(...));
}

private function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node): void {
// Add a property called "my-property" with the value "custom"
$propFind->handle('{myapplication.example}my-property', fn() => 'custom');
}
}


Performance considerations
--------------------------

In the example above, if you replace the "custom" value with a database lookup, you introduce a
performance issue: a query runs every time the property is loaded. This may
not be obvious at first, but it quickly becomes a problem, because a
``propFind`` event is emitted for every file in a collection to discover its
properties. If a collection contains 1,000 children, the code issues 1,000
queries that are likely very similar, differing only by the file identifier.

To mitigate this, Nextcloud 32 introduces a new event that signals your app to
preload data for a collection and its immediate children. Read more in
:ref:`collection_preload`.
1 change: 1 addition & 0 deletions developer_manual/app_development/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ App development
info
init
dependency_management
dav_extension
translation_setup
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ Back-end changes
- These new attributes will be applied on a "defacto standard" basis to the best of our knowledge.
In case an API was flagged unexpectedly, leave a comment on the respective pull request in the server repository asking for clarification.

Added Events
^^^^^^^^^^^^

- New ``preloadCollection`` event emitted by the DAV server during PROPFIND requests. See :ref:`collection_preload` for details.

Added APIs
^^^^^^^^^^

Expand Down
1 change: 1 addition & 0 deletions developer_manual/digging_deeper/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ Digging deeper
user_migration
users
web_host_metadata
webdav_collection_preload
time
96 changes: 96 additions & 0 deletions developer_manual/digging_deeper/webdav_collection_preload.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
.. _collection_preload:

=================================
WebDAV collection preload events
=================================

Overview
--------

During a WebDAV PROPFIND on a SabreDAV collection with ``Depth > 0``,
Nextcloud emits a preload event so DAV plugins can fetch data for the
collection's children in one go. The goal is to avoid N+1 database queries by
preloading what your property handlers need before per-node property handling
starts. In practice, this means your plugin can fill a cache up front, then
read from it in the usual ``propFind`` handlers for a faster and more
efficient response.

When the event is emitted
-------------------------

The event is emitted during PROPFIND requests, before the ``propFind`` event,
for nodes implementing ``Sabre\DAV\ICollection`` with ``Depth > 0``.

The event may be emitted multiple times for a given request path; plugins should
check their caches to avoid duplicate work.

Subscribing to the event in a plugin
------------------------------------

Register a listener in your implementation of Sabre's ``ServerPlugin::initialize()`` method:

.. code-block:: php

use Sabre\DAV\ICollection;
use Sabre\DAV\PropFind;
use Sabre\DAV\ServerPlugin;

class MyDavPlugin extends ServerPlugin {

public function initialize(\Sabre\DAV\Server $server): void {
// Called before per-node property handlers
$server->on('preloadCollection', $this->preloadCollection(...));

// Your usual property handlers
$server->on('propFind', $this->handleGetProperties(...));
}

private function preloadCollection(PropFind $propFind, ICollection $collection): void {
// Only preload when your properties were actually requested
$requested = [
'{http://appdomain.example/ns}your-prop',
'{http://appdomain.example/ns}another-prop',
];
$anyRequested = array_reduce(
$requested,
fn($result, $property) => $result || $propFind->getStatus($property) !== null,
false
);
if (!$anyRequested) {
return;
}

// Fetch data for the collection and its children in bulk
// and cache results for use in your propFind handler
$this->cache = $this->bulkLoadDataForCollection($collection); // implement your own caching
}

private function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node): void {
// Read and return values from $this->cache to avoid per-node queries

// Handle per-node property loading here to support Depth = 0
// and cases where the 'preloadCollection' event is not emitted.
}

}


Built-in examples
-----------------

- Tags: ``OCA\DAV\Connector\Sabre\TagsPlugin`` preloads tags and favorite
info for a folder and its children.
- Shares: ``OCA\DAV\Connector\Sabre\SharesPlugin`` preloads share types and
sharees for items within a folder.
- Comments: ``OCA\DAV\Connector\Sabre\CommentPropertiesPlugin`` preloads unread
comment counts for items within a folder.

Best practices
--------------

- Check requested properties: Use ``$propFind->getStatus('{ns}property')`` to
confirm that your properties were actually requested before querying.
- Cache results: The event can fire multiple times; cache by file ID or path
to avoid redundant work during the same request.
- Scope your preload: Only fetch data for the current collection and (at most)
its direct children; avoid fetching across the whole tree.