Skip to content

feat: Add open extensions for DriveItems#35

Open
dschmidt wants to merge 3 commits intomainfrom
feat/opentypeextensions
Open

feat: Add open extensions for DriveItems#35
dschmidt wants to merge 3 commits intomainfrom
feat/opentypeextensions

Conversation

@dschmidt
Copy link
Copy Markdown

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:

Method Path Operation Description
GET .../items/{item-id}/extensions ListExtensions List all extensions on a DriveItem
GET .../items/{item-id}/extensions/{extensionName} GetExtension Get a specific extension
PUT .../items/{item-id}/extensions/{extensionName} UpsertExtension Create or update an extension (merge semantics)
DELETE .../items/{item-id}/extensions/{extensionName} DeleteExtension Delete an entire extension

DriveItem schema extension

The driveItem schema gains an extensions property (array of openTypeExtension), returned only when the client requests $expand=extensions.

Schemas

  • openTypeExtension: Object with a read-only extensionName and additionalProperties: true for the free-form data.
  • openTypeExtensionUpdate: Object with additionalProperties: { nullable: true } to allow null values for property deletion.

Example flow

Create/set properties:

PUT /v1beta1/drives/{drive-id}/items/{item-id}/extensions/com.example.project
Content-Type: application/json

{
  "status": "reviewed",
  "assignee": "alice",
  "priority": 3
}

201 Created

Update a single property (merge):

PUT /v1beta1/drives/{drive-id}/items/{item-id}/extensions/com.example.project
Content-Type: application/json

{
  "status": "approved"
}

200 OK. assignee and priority remain unchanged.

Remove a property:

PUT /v1beta1/drives/{drive-id}/items/{item-id}/extensions/com.example.project
Content-Type: application/json

{
  "priority": null
}

200 OK priority is removed, other properties remain.

Read:

GET /v1beta1/drives/{drive-id}/items/{item-id}/extensions/com.example.project
{
  "extensionName": "com.example.project",
  "status": "approved",
  "assignee": "alice"
}

Delete entire extension:

DELETE /v1beta1/drives/{drive-id}/items/{item-id}/extensions/com.example.project

→ 204 No Content

Design considerations

PUT with upsert instead of POST + PATCH

Microsoft Graph uses POST to create and PATCH to 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, PUT is the semantically correct HTTP method.

This is consistent with existing Libre Graph API patterns: profile photos use PUT with upsert semantics (UpsertProfilePhoto), and tags use PUT for 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 null are removed. We adopt the same behavior on PUT:

  • Omitted properties remain unchanged (merge)
  • Properties set to null removed from the extension
  • DELETE on the extension removes the extension and all its properties

Note: 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 for extensionName.

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:

Aspect MS Graph (beta) Libre Graph
Create POST .../extensions PUT .../extensions/{name} (upsert)
Update PATCH .../extensions/{name} PUT .../extensions/{name} (upsert)
Update semantics Merge (for non-directory resources) Merge
Delete property Set to null Set to null
DriveItem support Beta only, unreliable First-class support

Planned implementation

Storage mapping

The implementation can build directly on OpenCloud's existing ArbitraryMetadata infrastructure in the CS3/reva layer no storage-layer changes are required.

Each extension maps to a set of ArbitraryMetadata keys using a fixed namespace prefix:

Extension:  com.example.project
Property:   status = "reviewed"

ArbitraryMetadata key:   http://opencloud.eu/ns/extensions/com.example.project/status
ArbitraryMetadata value: "reviewed"

The Graph service handler translates between the JSON representation and the flat key-value pairs:

  • PUT calls SetArbitraryMetadata for all non-null properties, UnsetArbitraryMetadata for null properties
  • GET calls GetMD with the extension's key prefix, strips the prefix, groups by extension name, returns as JSON
  • DELETE calls UnsetArbitraryMetadata for all keys matching the extension's prefix
  • $expand=extensions requests all keys with the http://opencloud.eu/ns/extensions/ prefix, groups them into extension objects

Cross-protocol access via WebDAV

Because the extension data is stored as standard ArbitraryMetadata with a well-defined key format, it is automatically accessible via WebDAV without any additional implementation:

Extension name:  com.example.project
WebDAV namespace: http://opencloud.eu/ns/extensions/com.example.project

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:

  1. New routes in the Graph service (services/graph/pkg/service/v0/service.go)
  2. New handler functions for List, Get, Upsert, Delete (new file, e.g. api_driveitem_extensions.go)
  3. Extend the DriveItem conversion in driveitems.go to populate the extensions field when $expand=extensions is requested
  4. No changes to the storage layer, decomposedfs, reva, or the WebDAV handler

Future work (out of scope)

  • Search/filter by extension properties: Would require indexing extension data in the Bleve search index. Not needed for v1.
  • Schema validation: Optional typed extensions (similar to MS Graph's schema extensions). Not needed for v1.
  • Drive-wide extension listing: "Which extension names exist across all items in this drive?" Requires index support.

Of course I'm willing to implement this.

Added endpoints for managing open extensions on DriveItems, including listing, retrieving, creating, updating, and deleting extensions.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/DELETE endpoints under /v1beta1/drives/{drive-id}/items/{item-id}/extensions for list/get/upsert/delete.
  • Extends the driveItem schema with an extensions collection (documented as returned only on $expand).
  • Introduces openTypeExtension and openTypeExtensionUpdate schemas to model extension read/update payloads.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread api/openapi-spec/v1.0.yaml
Comment thread api/openapi-spec/v1.0.yaml Outdated
Comment thread api/openapi-spec/v1.0.yaml Outdated
Comment thread api/openapi-spec/v1.0.yaml
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/openapi-spec/v1.0.yaml Outdated
Comment thread api/openapi-spec/v1.0.yaml
Comment thread api/openapi-spec/v1.0.yaml Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@dschmidt
Copy link
Copy Markdown
Author

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

@dschmidt
Copy link
Copy Markdown
Author

dschmidt commented Apr 14, 2026

We need to be careful with the implementation, iirc arbitrarymetadata stores strings, we need to keep track of the original type

edit:
maybe we can store actual JSON values (with " around strings, eg) for indexing we need to json decode them

Copy link
Copy Markdown
Member

@butonic butonic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@dschmidt
Copy link
Copy Markdown
Author

dschmidt commented Apr 21, 2026

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.

Hmm - we could also roll out a nested structure to flat keys:
extensions.foobar.top.x.y.z: "test"
when storing { x: y: z: "test" } in a foobar extension.
I kinda like having all of them namespaced by default through the extension.
Makes it less likely for different apps to clash on keys (when using a reverse dns name prefix is only a suggestion)

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.

Yeah, probably.

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.

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).

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.

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.

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.

That sounds pretty complicated, I'd like to avoid that.
Easiest approach for me would be to just store json byte value representation. so "string", 34, null, undefined, true, false are all valid values and can just be unmarshalled as they are.
This would even allow storing json object, but then it gets awkward if we use flat keys only in the search index - or maybe not... maybe we could also just store the whole json object in arbitrary metadata and roll it out only in the index 🤔

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.

What do you mean, leave types out of the spec?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants