Skip to content

Commit 9d635a5

Browse files
committed
docs: add documentation for DAV collection preloading event
Signed-off-by: Salvatore Martire <4652631+salmart-dev@users.noreply.github.com>
1 parent 8270062 commit 9d635a5

File tree

5 files changed

+188
-0
lines changed

5 files changed

+188
-0
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
.. _dav_extensions:
2+
3+
========================
4+
Extending the DAV server
5+
========================
6+
7+
Nextcloud apps can extend the DAV server by registering SabreDAV plugins that hook into
8+
different phases of a DAV request. Plugins can add handlers for custom methods and
9+
properties, adjust response behavior, and more. See `Writing Plugins - sabre/dav
10+
<https://sabre.io/dav/writing-plugins/>`_ for additional possibilities.
11+
12+
Registering a DAV plugin
13+
------------------------
14+
15+
To register a server plugin in your app, register an event listener for
16+
``OCA\DAV\Events\SabrePluginAddEvent`` (introduced in Nextcloud 28). In the
17+
listener's handler, add your DAV plugin to the server.
18+
19+
For example:
20+
21+
.. code-block:: php
22+
:caption: MyApplication.php
23+
24+
class MyApplication extends App implements IBootstrap {
25+
public function register(IRegistrationContext $context): void {
26+
$context->registerEventListener(SabrePluginAddEvent::class, MyListener::class);
27+
}
28+
}
29+
30+
.. code-block:: php
31+
:caption: MyListener.php
32+
33+
use OCP\EventDispatcher\Event;
34+
use OCP\EventDispatcher\IEventListener;
35+
use OCA\DAV\Events\SabrePluginAddEvent;
36+
37+
class MyListener implements IEventListener {
38+
public function handle(Event $event): void {
39+
if (!$event instanceof SabrePluginAddEvent) {
40+
return;
41+
}
42+
$server = $event->getServer();
43+
$server->addPlugin(new MyDavPlugin());
44+
}
45+
}
46+
47+
Handling DAV events
48+
-------------------
49+
50+
In this example, we register a handler for the ``propFind`` event and add a
51+
custom property that is returned in PROPFIND requests.
52+
53+
.. code-block:: php
54+
:caption: MyDavPlugin.php
55+
56+
use Sabre\DAV\ServerPlugin;
57+
use Sabre\DAV\PropFind;
58+
59+
class MyDavPlugin extends ServerPlugin {
60+
61+
public function initialize(\Sabre\DAV\Server $server): void {
62+
// Register your property handler
63+
$server->on('propFind', $this->handleGetProperties(...));
64+
}
65+
66+
private function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node): void {
67+
// Add a property called "my-property" with the value "custom"
68+
$propFind->handle('{myapplication.example}my-property', fn() => 'custom');
69+
}
70+
}
71+
72+
73+
Performance considerations
74+
--------------------------
75+
76+
In the example above, if you replace the "custom" value with a database lookup, you introduce a
77+
performance issue: a query runs every time the property is loaded. This may
78+
not be obvious at first, but it quickly becomes a problem, because a
79+
``propFind`` event is emitted for every file in a collection to discover its
80+
properties. If a collection contains 1,000 children, the code issues 1,000
81+
queries that are likely very similar, differing only by the file identifier.
82+
83+
To mitigate this, Nextcloud 32 introduces a new event that signals your app to
84+
preload data for a collection and its immediate children. Read more in
85+
:ref:`collection_preload`.

developer_manual/app_development/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ App development
1111
info
1212
init
1313
dependency_management
14+
dav_extension
1415
translation_setup

developer_manual/app_publishing_maintenance/app_upgrade_guide/upgrade_to_32.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ Back-end changes
8282
- These new attributes will be applied on a "defacto standard" basis to the best of our knowledge.
8383
In case an API was flagged unexpectedly, leave a comment on the respective pull request in the server repository asking for clarification.
8484

85+
Added Events
86+
^^^^^^^^^^^^
87+
88+
- New ``preloadCollection`` event emitted by the DAV server during PROPFIND requests. See :ref:`collection_preload` for details.
89+
8590
Added APIs
8691
^^^^^^^^^^
8792

developer_manual/digging_deeper/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,5 @@ Digging deeper
4848
user_migration
4949
users
5050
web_host_metadata
51+
webdav_collection_preload
5152
time
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
.. _collection_preload:
2+
3+
=================================
4+
WebDAV collection preload events
5+
=================================
6+
7+
Overview
8+
--------
9+
10+
During a WebDAV PROPFIND on a SabreDAV collection with ``Depth > 0``,
11+
Nextcloud emits a preload event so DAV plugins can fetch data for the
12+
collection's children in one go. The goal is to avoid N+1 database queries by
13+
preloading what your property handlers need before per-node property handling
14+
starts. In practice, this means your plugin can fill a cache up front, then
15+
read from it in the usual ``propFind`` handlers for a faster and more
16+
efficient response.
17+
18+
When the event is emitted
19+
-------------------------
20+
21+
The event is emitted during PROPFIND requests, before the ``propFind`` event,
22+
for nodes implementing ``Sabre\DAV\ICollection`` with ``Depth > 0``.
23+
24+
The event may be emitted multiple times for a given request path; plugins should
25+
check their caches to avoid duplicate work.
26+
27+
Subscribing to the event in a plugin
28+
------------------------------------
29+
30+
Register a listener in your implementation of Sabre's ``ServerPlugin::initialize()`` method:
31+
32+
.. code-block:: php
33+
34+
use Sabre\DAV\ICollection;
35+
use Sabre\DAV\PropFind;
36+
use Sabre\DAV\ServerPlugin;
37+
38+
class MyDavPlugin extends ServerPlugin {
39+
40+
public function initialize(\Sabre\DAV\Server $server): void {
41+
// Called before per-node property handlers
42+
$server->on('preloadCollection', $this->preloadCollection(...));
43+
44+
// Your usual property handlers
45+
$server->on('propFind', $this->handleGetProperties(...));
46+
}
47+
48+
private function preloadCollection(PropFind $propFind, ICollection $collection): void {
49+
// Only preload when your properties were actually requested
50+
$requested = [
51+
'{http://appdomain.example/ns}your-prop',
52+
'{http://appdomain.example/ns}another-prop',
53+
];
54+
$anyRequested = array_reduce(
55+
$requested,
56+
fn($result, $property) => $result || $propFind->getStatus($property) !== null,
57+
false
58+
);
59+
if (!$anyRequested) {
60+
return;
61+
}
62+
63+
// Fetch data for the collection and its children in bulk
64+
// and cache results for use in your propFind handler
65+
$this->cache = $this->bulkLoadDataForCollection($collection); // implement your own caching
66+
}
67+
68+
private function handleGetProperties(PropFind $propFind, \Sabre\DAV\INode $node): void {
69+
// Read and return values from $this->cache to avoid per-node queries
70+
71+
// Handle per-node property loading here to support Depth = 0
72+
// and cases where the 'preloadCollection' event is not emitted.
73+
}
74+
75+
}
76+
77+
78+
Built-in examples
79+
-----------------
80+
81+
- Tags: ``OCA\DAV\Connector\Sabre\TagsPlugin`` preloads tags and favorite
82+
info for a folder and its children.
83+
- Shares: ``OCA\DAV\Connector\Sabre\SharesPlugin`` preloads share types and
84+
sharees for items within a folder.
85+
- Comments: ``OCA\DAV\Connector\Sabre\CommentPropertiesPlugin`` preloads unread
86+
comment counts for items within a folder.
87+
88+
Best practices
89+
--------------
90+
91+
- Check requested properties: Use ``$propFind->getStatus('{ns}property')`` to
92+
confirm that your properties were actually requested before querying.
93+
- Cache results: The event can fire multiple times; cache by file ID or path
94+
to avoid redundant work during the same request.
95+
- Scope your preload: Only fetch data for the current collection and (at most)
96+
its direct children; avoid fetching across the whole tree.

0 commit comments

Comments
 (0)