You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[RFC] One file representation for DAV and PHP: node attribute providers #65395
Use the 👍 reaction to show support for this feature.
Avoid commenting unless you have relevant information to add; unnecessary comments create noise for subscribers.
Subscribe to receive notifications about status changes and new comments.
Is your feature request related to a problem? Please describe.
The only complete representation of a file is the WebDAV one. Its attributes come from Sabre plugins during a PROPFIND: FilesPlugin, SharesPlugin, TagsPlugin, CommentPropertiesPlugin, SystemTagPlugin, files_reminders, files metadata, and whatever apps add on top. The frontend turns that response into a File/Folder with resultToNode() from @nextcloud/files, and apps add their props on that side with registerDavProperty() (server alone registers 12: nc:sharees, oc:share-types, nc:reminder-due-date, nc:system-tags...).
There is no PHP equivalent. Code that returns files without going through DAV builds its own subset, each with its own shape:
apps/files_trashbin/lib/Helper.php:104: a hand-built mimetype, etag, permissions
None of these carry the attributes plugins add, and none can be turned into the same Node the Files app uses. Each app that needs file data either re-implements part of it or sends a PROPFIND:
Talk can't PROPFIND its attachments folder, it is too large. spreed/src/composables/useViewer.js:85 maps its own message data to a viewer file by hand: it converts the permission bitmask to the DAV string (CKGWDR) and derives hasPreview from preview-available === 'yes'.
It also costs performance. Fetching a few known files means a DAV SEARCH or one PROPFIND per file, through the full Sabre stack and the per-node prop handlers. While working on #2742 (live refresh from notify_push), a SEARCH for 10 file ids took 2.4 s on a test account with 5,000 incoming shares, mostly spent in searching all mounted storages and in per-node share lookups.
Describe the solution you'd like
One registry that both DAV and PHP read from.
Apps register providers with the attributes they serve, in Clark notation like the DAV props, so the server only loads a provider when one of its attributes is requested:
$context->registerNodeAttributeProvider(SharesAttributeProvider::class, [
'{http://owncloud.org/ns}share-types',
'{http://nextcloud.org/ns}sharees',
]);
interface INodeAttributeProvider {
/** Load what is needed for all the nodes of a response at once */publicfunctionpreload(INodeAttributeContext$context, array$nodes, array$attributes): void;
/** @return array<string, mixed> scalars, or NodeAttributeList for lists */publicfunctiongetAttributes(INodeAttributeContext$context, Node$node, array$attributes): array;
}
A public serializer, INodeSerializer::serialize(INodeAttributeContext $context, array $nodes, ?array $attributes = null): array, returns the NodeData shape @nextcloud/files already uses, attributes keyed by local name like resultToNode() does today. Lists use the same shape the webdav client produces ({ 'share-type': [0, 3] }).
A generic Sabre plugin serves registered attributes on propFind when no other plugin handled them, and calls preload() on preloadCollection and preloadProperties. Existing plugins keep working and move over one by one, and SEARCH results get batched loading for every migrated attribute.
The registered attribute names reach the frontend through initial state, so @nextcloud/files requests them without registerDavProperty(). A counterpart to resultToNode(), for example dataToNode(), builds a Node from the serializer output.
Public pages are a requirement, not a follow-up. Public share pages have no user, only a share token, so providers get an INodeAttributeContext holding either the user or the share, and the serializer and DAV adapter work the same on remote.php and public.php.
Providers load everything in preload(), and getAttributes() doesn't query per node. A test helper asserts the same number of queries for 1 and 100 nodes, so the N+1 we see in SEARCH today (about 13 queries per node) doesn't come back.
Rollout:
Phase 1: the registry, the serializer and the DAV adapter, with the five most used attributes: favorite, etag, has-preview, share-attributes and share-types. share-attributes is in on purpose, so the DAV/PHP parity tests for permission related attributes exist from the start.
Phase 2: the remaining server plugins.
Phase 3: props registered by apps (files_lock, Photos, Text).
Describe alternatives you've considered
Keep DAV as the only source and fetch with SEARCH or PROPFIND. That is what we do today, with the cost above.
Make formatFileInfo() the shared format. It doesn't know about plugin attributes, so the gap stays.
Additional context
What to migrate first. fileid, permissions, size, getlastmodified, getcontenttype, owner-id and displayname become top-level Node fields in resultToNode() and are needed by every view, so they are the core set. Among the other attributes, ranked by reads of node.attributes.* in server and in Photos, Text, Deck, Talk, Viewer, Office, Collectives, Activity, Mail, Recognize, files_lock, Forms and files_pdfviewer:
which actions apply to shared, external or groupfolder nodes
sharees
8
files_sharing
"Shared with" avatars
owner-display-name
6
files, files_sharing
owner of shared files
hide-download
3
files; Talk's backend sends its own ('yes'/'no')
hiding downloads on restricted shares
is-mount-root
2
files
rename, move and delete rules on mount roots
e2ee-is-encrypted
2
text, viewer
end-to-end encrypted folders
Apps also register their own props with registerDavProperty(), and would become providers too: files_lock (nc:lock, nc:lock-owner, nc:lock-owner-displayname, nc:lock-owner-editor, nc:lock-owner-type, nc:lock-time), Photos (nc:metadata-photos-*, nc:metadata-blurhash, nc:metadata-files-live-photo), Text (nc:rich-workspace-flat, nc:rich-workspace-file-flat).
share-attributes, hide-download and share-permissions drive download and permission decisions, and Talk's backend already sends its own hide-download in message parameters (spreed/src/composables/useMessageInfo.ts:115). A mismatch between the DAV and PHP output there is a security issue, not a display bug, so they need tests comparing both outputs.
Open questions:
Attribute values: XML props are parsed by the webdav client into nested shapes (for example share-types becomes { 'share-type': [0, 3] }). The serializer either reproduces them, or both sides move to one clean shape, which touches every attribute consumer.
JSON keys are local names, as in resultToNode(), so two apps registering the same local name in different namespaces would collide. The registry would refuse the second one.
Permissions, share permissions and hide-download depend on the user's mount or on the public share, so the serializer has to work on user or share scoped nodes only, never on raw cache entries.
Existing registerDavProperty() apps need a transition period where both keep working.
@julien-nc@Antreesy@juliusknorr@mejo-: Assistant, Talk, Text, files_lock, Deck and Collectives build file objects by hand or send a PROPFIND per file today. Your expertise is also highly appreciated 🙇
I think allowing us to move away from webdav entirely for some specific use cases would be very effective and increase our performances. Like getting a specific file by its id without having to do a PROPFIND or a SEARCH
Its likely far cheaper to instead do a PROPSTAT per file (usually only a low number of files change for a user).
This does not need the server to do a full search but only need to setup the filesystem for that id (will is currently being improved performance-wise with partial share setup we work on for 36).
@susnux right! I tried it on my heavy test account (70k files in the root, 5k incoming shares, MariaDB).
A Depth 0 PROPFIND on a single file takes ~180 ms, and 10 of them in parallel ~630 ms. The SEARCH for the same 10 ids takes 2.4 s. So yes, way cheaper. Yet, even 180ms is a bit too much for my taste. And it's only one file on a not-so-busy test instance...
There are two things I'm not sure how to handle yet:
notify_push only gives us ids. For files we already list that's fine, we know their path. But for a new file or a rename we only have the id, and there's no DAV route by id. So we'd still need some cheap lookup by id for those.
And for incoming share roots, Depth 0 and the folder listing don't return the same share-types/sharees. Depth 0 includes the incoming share, the listing doesn't. So refreshing a share root that way would change its sharing icon.
Tip
Help move this idea forward
Is your feature request related to a problem? Please describe.
The only complete representation of a file is the WebDAV one. Its attributes come from Sabre plugins during a PROPFIND:
FilesPlugin,SharesPlugin,TagsPlugin,CommentPropertiesPlugin,SystemTagPlugin, files_reminders, files metadata, and whatever apps add on top. The frontend turns that response into aFile/FolderwithresultToNode()from@nextcloud/files, and apps add their props on that side withregisterDavProperty()(server alone registers 12:nc:sharees,oc:share-types,nc:reminder-due-date,nc:system-tags...).There is no PHP equivalent. Code that returns files without going through DAV builds its own subset, each with its own shape:
apps/files/lib/Helper.php:71formatFileInfo():parentId,mtime * 1000,shareOwner,isShareMountPoint,mountTypeapps/files_sharing/lib/Controller/ShareAPIController.php:204:storage_id,file_target,item_mtimeapps/files_trashbin/lib/Helper.php:104: a hand-builtmimetype,etag,permissionsNone of these carry the attributes plugins add, and none can be turned into the same
Nodethe Files app uses. Each app that needs file data either re-implements part of it or sends a PROPFIND:spreed/src/composables/useViewer.js:85maps its own message data to a viewer file by hand: it converts the permission bitmask to the DAV string (CKGWDR) and deriveshasPreviewfrompreview-available === 'yes'.Nodefor the Viewer (Properly build the node we pass to the Viewer assistant#675), which doesn't scale for huge folders or accounts with many shares.It also costs performance. Fetching a few known files means a DAV SEARCH or one PROPFIND per file, through the full Sabre stack and the per-node prop handlers. While working on #2742 (live refresh from notify_push), a SEARCH for 10 file ids took 2.4 s on a test account with 5,000 incoming shares, mostly spent in searching all mounted storages and in per-node share lookups.
Describe the solution you'd like
One registry that both DAV and PHP read from.
Apps register providers with the attributes they serve, in Clark notation like the DAV props, so the server only loads a provider when one of its attributes is requested:
A public serializer,
INodeSerializer::serialize(INodeAttributeContext $context, array $nodes, ?array $attributes = null): array, returns theNodeDatashape@nextcloud/filesalready uses, attributes keyed by local name likeresultToNode()does today. Lists use the same shape the webdav client produces ({ 'share-type': [0, 3] }).A generic Sabre plugin serves registered attributes on
propFindwhen no other plugin handled them, and callspreload()onpreloadCollectionandpreloadProperties. Existing plugins keep working and move over one by one, and SEARCH results get batched loading for every migrated attribute.The registered attribute names reach the frontend through initial state, so
@nextcloud/filesrequests them withoutregisterDavProperty(). A counterpart toresultToNode(), for exampledataToNode(), builds aNodefrom the serializer output.Public pages are a requirement, not a follow-up. Public share pages have no user, only a share token, so providers get an
INodeAttributeContextholding either the user or the share, and the serializer and DAV adapter work the same onremote.phpandpublic.php.Providers load everything in
preload(), andgetAttributes()doesn't query per node. A test helper asserts the same number of queries for 1 and 100 nodes, so the N+1 we see in SEARCH today (about 13 queries per node) doesn't come back.Rollout:
favorite,etag,has-preview,share-attributesandshare-types.share-attributesis in on purpose, so the DAV/PHP parity tests for permission related attributes exist from the start.Describe alternatives you've considered
formatFileInfo()the shared format. It doesn't know about plugin attributes, so the gap stays.Additional context
What to migrate first.
fileid,permissions,size,getlastmodified,getcontenttype,owner-idanddisplaynamebecome top-levelNodefields inresultToNode()and are needed by every view, so they are the core set. Among the other attributes, ranked by reads ofnode.attributes.*in server and in Photos, Text, Deck, Talk, Viewer, Office, Collectives, Activity, Mail, Recognize, files_lock, Forms and files_pdfviewer:favoriteetaghas-previewshare-attributesshare-typesmount-typeshareesowner-display-namehide-download'yes'/'no')is-mount-roote2ee-is-encryptedApps also register their own props with
registerDavProperty(), and would become providers too: files_lock (nc:lock,nc:lock-owner,nc:lock-owner-displayname,nc:lock-owner-editor,nc:lock-owner-type,nc:lock-time), Photos (nc:metadata-photos-*,nc:metadata-blurhash,nc:metadata-files-live-photo), Text (nc:rich-workspace-flat,nc:rich-workspace-file-flat).share-attributes,hide-downloadandshare-permissionsdrive download and permission decisions, and Talk's backend already sends its ownhide-downloadin message parameters (spreed/src/composables/useMessageInfo.ts:115). A mismatch between the DAV and PHP output there is a security issue, not a display bug, so they need tests comparing both outputs.Open questions:
share-typesbecomes{ 'share-type': [0, 3] }). The serializer either reproduces them, or both sides move to one clean shape, which touches every attribute consumer.resultToNode(), so two apps registering the same local name in different namespaces would collide. The registry would refuse the second one.hide-downloaddepend on the user's mount or on the public share, so the serializer has to work on user or share scoped nodes only, never on raw cache entries.registerDavProperty()apps need a transition period where both keep working.