Conversation
Added endpoints for managing open extensions on DriveItems, including listing, retrieving, creating, updating, and deleting extensions.
There was a problem hiding this comment.
Pull request overview
Adds Libre Graph v1beta1 OpenAPI documentation for DriveItem open extensions, allowing clients to attach arbitrary key/value metadata to files and folders and (optionally) retrieve it via $expand=extensions.
Changes:
- Adds
GET/PUT/DELETEendpoints under/v1beta1/drives/{drive-id}/items/{item-id}/extensionsfor list/get/upsert/delete. - Extends the
driveItemschema with anextensionscollection (documented as returned only on$expand). - Introduces
openTypeExtensionandopenTypeExtensionUpdateschemas to model extension read/update payloads.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
I'm not sure about indexing the extension data - maybe we should have a rough idea how we want to do that (or not) even if we dont implement it straight away |
|
We need to be careful with the implementation, iirc arbitrarymetadata stores strings, we need to keep track of the original type edit: |
butonic
left a comment
There was a problem hiding this comment.
extension vs fieldvalueset
Hm, the ms graph openType extensions are complex objects, not just key -> (typed) value pairs. see this response example: https://learn.microsoft.com/en-us/graph/api/opentypeextension-get?view=graph-rest-beta&tabs=http#response-1
This PR uses a more key value like approach, but maybe we should just add the fieldValueSet that ms recommends as an alternative when treating driveItens as listItems to our driveItems. 'extensions' does not seem to correctly capture the idea of being able to store arbitrary metatada.
Key names should mention reverse domain namespace
One way to help make sure extension names are unique is to use a reverse domain name system (DNS) format that is dependent on your own domain, for example, com.contoso.ContactInfo. Don't use the Microsoft domain (com.microsoft or com.onmicrosoft) in an extension name.
We should add this reverse domain namespace as a recommendation to the docs.
Datatypes for WebDAV
Webdav properties are untyped. We could annotate the property tag with xsi:type from the XMLSchema-instance namespace as proposed by Datatypes for Web Distributed Authoring and Versioning (WebDAV) Properties
RFC 4316, but that has never been standardized. It does not violate any spec we use and RFC4316 just defines how to respond if a property cannot be parsed as the annotated type.
>>Request
PROPPATCH /bar.html HTTP/1.1
Host: example.org
Content-Type: text/xml; charset="utf-8"
Content-Length: xxxx
<?xml version="1.0" encoding="utf-8" ?>
<D:propertyupdate xmlns:D="DAV:"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:Z="http://ns.example.org/standards/z39.50">
<D:set>
<D:prop>
<Z:released xsi:type="xs:boolean">false</Z:released>
</D:prop>
</D:set>
</D:propertyupdate>
>>Response
HTTP/1.1 207 Multi-Status
Content-Type: text/xml; charset="utf-8"
Content-Length: xxxx
<?xml version="1.0" encoding="utf-8" ?>
<D:multistatus xmlns:D="DAV:"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:Z="http://ns.example.org/standards/z39.50">
<D:response>
<D:href>http://example.org/bar.html</D:href>
<D:propstat>
<D:prop><Z:released xsi:type="xs:boolean" /></D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
</D:multistatus>This would allow representing typed values in webdav as well.
Persisting type information
This is an open question, I think. We currently treat all properties as string and write them to extended attributes as string. In reality extended attributes are just binary and we just write our string as binary (0 terminated AFAIR).
We could try to detect the type whenever we read the attribute, however then we can no longer really write a string 34 because when reading it back we would interpret it as a number. Hilariously, that is not a problem for XML where numbers and booleans appear as a string representation between <tag>false</tag> and we would anly add an annotation like <tag xsi:type="xs:boolean">false</tag>. old clients would still read it as string and parse it themselves. New clients trying to use the type would likely get confused when they explicitly write a string true via JSON in the graph api (or via webdav) but then get a boolean type back.
The only other option would be to somehow annotate the value. While there is a risk of collision for legacy values starting with any of these prefixes:
i:34
b:true
f:3.14
j:{"a":1}
s:hello world
I think that might be acceptible. if a value is not prefixed we assume string. We can even detect this: if the file has been modified before we started interpreting the value prefix we can always assume string.
For now, I would leave the type out of the spec. Clients know the datatype and should be able to parse it properly. The server really has no good way of storing the type and it can be proposed as an ADR.
Hmm - we could also roll out a nested structure to flat keys:
Yeah, probably.
Oh wow, I honestly didn't think of this - but that's great there is some standard we can use (even if it's slightly loose).
Yeah, I'm against trying to detect the type for exactly the reason you mentioned. IMHO we need to store the type information somehow, this could be handled only in the extension/fieldSet part though. I don't think we need to make arbitrarymetada handle it for all values. For regular facets it's no issue because reflection tells us what type we are converting to.
That sounds pretty complicated, I'd like to avoid that.
What do you mean, leave types out of the spec? |
Summary
Add API endpoints for open extensions on DriveItems, enabling applications to attach arbitrary custom metadata to files and folders via the Libre Graph API.
Motivation
Currently, there is no way to attach application-specific metadata to DriveItems through the Libre Graph API. While OpenCloud's WebDAV layer supports arbitrary properties via PROPPATCH/PROPFIND, this functionality is not exposed through the Graph API. This means applications that use the Graph API cannot store and retrieve custom metadata on files.
Open extensions close this gap. They provide a simple, schema-less mechanism for any application to store key-value data on a DriveItem, identified by a unique extension name.
Use case
Avoid small extension services having to introduce their own shadow metadata storage that needs to be kept in sync with the storage/index in OpenCloud.
I wouldn't do it in the first iteration, but in the long run the data could potentially be indexed by the search service as well. Needs more thought and planning.
API design
Endpoints
Four new endpoints under
v1beta1:GET.../items/{item-id}/extensionsGET.../items/{item-id}/extensions/{extensionName}PUT.../items/{item-id}/extensions/{extensionName}DELETE.../items/{item-id}/extensions/{extensionName}DriveItem schema extension
The
driveItemschema gains anextensionsproperty (array ofopenTypeExtension), returned only when the client requests$expand=extensions.Schemas
openTypeExtension: Object with a read-onlyextensionNameandadditionalProperties: truefor the free-form data.openTypeExtensionUpdate: Object withadditionalProperties: { nullable: true }to allownullvalues for property deletion.Example flow
Create/set properties:
201 CreatedUpdate a single property (merge):
’
200 OK.assigneeandpriorityremain unchanged.Remove a property:
200 OKpriorityis removed, other properties remain.Read:
{ "extensionName": "com.example.project", "status": "approved", "assignee": "alice" }Delete entire extension:
→
204 No ContentDesign considerations
PUT with upsert instead of POST + PATCH
Microsoft Graph uses
POSTto create andPATCHto update extensions, requiring the client to know whether the extension already exists. This separation makes sense when the server generates the resource identifier, but extension names are client-chosen (reverse DNS convention). Since the client determines the target URI,PUTis the semantically correct HTTP method.This is consistent with existing Libre Graph API patterns: profile photos use
PUTwith upsert semantics (UpsertProfilePhoto), and tags usePUTfor assignment (AssignTags). Neither uses the POST-to-create / PATCH-to-update split.Merge semantics on PUT
For DriveItems specifically, Microsoft Graph's beta API uses merge semantics on PATCH: properties not included in the request body remain unchanged, and properties set to
nullare removed. We adopt the same behavior on PUT:nullremoved from the extensionDELETEon the extension removes the extension and all its propertiesNote: Microsoft's documentation for open extensions is internally contradictory on this point - for directory objects it describes replace semantics, for other resources (including DriveItems) it describes merge semantics. We follow the DriveItem-specific behavior.
Naming convention
Extension names should use reverse DNS notation (e.g.
com.example.myApp) to avoid collisions between applications. This matches the Microsoft Graph convention forextensionName.Relation to Microsoft Graph API
Microsoft Graph supports open extensions on DriveItems only in its beta API it is not available in v1.0. In practice, developers report that the DriveItem support is unreliable, and Microsoft recommends using SharePoint listItem fields as a workaround instead.
This means there is no stable MS Graph API to be compatible with. We are free to design the cleanest API for this use case. The key differences from MS Graph beta:
POST .../extensionsPUT .../extensions/{name}(upsert)PATCH .../extensions/{name}PUT .../extensions/{name}(upsert)nullnullPlanned implementation
Storage mapping
The implementation can build directly on OpenCloud's existing
ArbitraryMetadatainfrastructure in the CS3/reva layer no storage-layer changes are required.Each extension maps to a set of
ArbitraryMetadatakeys using a fixed namespace prefix:The Graph service handler translates between the JSON representation and the flat key-value pairs:
SetArbitraryMetadatafor all non-null properties,UnsetArbitraryMetadatafor null propertiesGetMDwith the extension's key prefix, strips the prefix, groups by extension name, returns as JSONUnsetArbitraryMetadatafor all keys matching the extension's prefix$expand=extensionsrequests all keys with thehttp://opencloud.eu/ns/extensions/prefix, groups them into extension objectsCross-protocol access via WebDAV
Because the extension data is stored as standard
ArbitraryMetadatawith a well-defined key format, it is automatically accessible via WebDAV without any additional implementation:A WebDAV PROPFIND requesting properties in this namespace will return the extension data. A PROPPATCH setting properties in this namespace will update it. This means applications can read and write the same metadata through both protocols interchangeably.
Scope of implementation
The implementation requires:
services/graph/pkg/service/v0/service.go)api_driveitem_extensions.go)driveitems.goto populate theextensionsfield when$expand=extensionsis requestedFuture work (out of scope)
Of course I'm willing to implement this.