diff --git a/developer_manual/app_development/dav_extension.rst b/developer_manual/app_development/dav_extension.rst new file mode 100644 index 00000000000..f8bcbd14020 --- /dev/null +++ b/developer_manual/app_development/dav_extension.rst @@ -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 +`_ 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`. diff --git a/developer_manual/app_development/index.rst b/developer_manual/app_development/index.rst index 69d5762561c..06ab34e010d 100644 --- a/developer_manual/app_development/index.rst +++ b/developer_manual/app_development/index.rst @@ -11,4 +11,5 @@ App development info init dependency_management + dav_extension translation_setup diff --git a/developer_manual/app_publishing_maintenance/app_upgrade_guide/upgrade_to_32.rst b/developer_manual/app_publishing_maintenance/app_upgrade_guide/upgrade_to_32.rst index a74080035e2..e5a2f1b496a 100644 --- a/developer_manual/app_publishing_maintenance/app_upgrade_guide/upgrade_to_32.rst +++ b/developer_manual/app_publishing_maintenance/app_upgrade_guide/upgrade_to_32.rst @@ -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 ^^^^^^^^^^ diff --git a/developer_manual/digging_deeper/index.rst b/developer_manual/digging_deeper/index.rst index b7a568ed4c2..8d449e75150 100644 --- a/developer_manual/digging_deeper/index.rst +++ b/developer_manual/digging_deeper/index.rst @@ -48,4 +48,5 @@ Digging deeper user_migration users web_host_metadata + webdav_collection_preload time diff --git a/developer_manual/digging_deeper/webdav_collection_preload.rst b/developer_manual/digging_deeper/webdav_collection_preload.rst new file mode 100644 index 00000000000..fcc4e0e10a8 --- /dev/null +++ b/developer_manual/digging_deeper/webdav_collection_preload.rst @@ -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.