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.

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.

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

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.

2 participants