From b5af7eed4d2927cdb91f635f570777bad541f454 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sun, 5 Jul 2026 18:08:16 +0200 Subject: [PATCH 1/8] chore(deps): bump libre-graph-api-go for motionPhoto facet Adds the generated MotionPhoto model + DriveItem.LibreGraphMotionPhoto (libre-graph-api#36). --- go.mod | 2 +- go.sum | 4 +- .../opencloud-eu/libre-graph-api-go/README.md | 10 + .../libre-graph-api-go/api_drive_item.go | 153 +++++++ .../libre-graph-api-go/api_invitations.go | 328 ++++++++++++++ .../opencloud-eu/libre-graph-api-go/client.go | 3 + .../model_collection_of_invitations.go | 126 ++++++ .../libre-graph-api-go/model_drive_item.go | 73 +++ .../libre-graph-api-go/model_email_address.go | 164 +++++++ .../libre-graph-api-go/model_invitation.go | 421 ++++++++++++++++++ .../model_invited_user_message_info.go | 201 +++++++++ .../libre-graph-api-go/model_motion_photo.go | 201 +++++++++ .../libre-graph-api-go/model_recipient.go | 126 ++++++ vendor/modules.txt | 2 +- 14 files changed, 1810 insertions(+), 4 deletions(-) create mode 100644 vendor/github.com/opencloud-eu/libre-graph-api-go/api_invitations.go create mode 100644 vendor/github.com/opencloud-eu/libre-graph-api-go/model_collection_of_invitations.go create mode 100644 vendor/github.com/opencloud-eu/libre-graph-api-go/model_email_address.go create mode 100644 vendor/github.com/opencloud-eu/libre-graph-api-go/model_invitation.go create mode 100644 vendor/github.com/opencloud-eu/libre-graph-api-go/model_invited_user_message_info.go create mode 100644 vendor/github.com/opencloud-eu/libre-graph-api-go/model_motion_photo.go create mode 100644 vendor/github.com/opencloud-eu/libre-graph-api-go/model_recipient.go diff --git a/go.mod b/go.mod index 4a9237cc86..77b4c88380 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/onsi/gomega v1.42.1 github.com/open-policy-agent/opa v1.17.1 github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 - github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d + github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260508132157-0223d2e98d28 github.com/opencloud-eu/reva/v2 v2.46.4-0.20260625152426-8cff2a7032ec github.com/opensearch-project/opensearch-go/v4 v4.6.0 github.com/orcaman/concurrent-map v1.0.0 diff --git a/go.sum b/go.sum index 61856f3ca0..c3c622acd6 100644 --- a/go.sum +++ b/go.sum @@ -947,8 +947,8 @@ github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-202505121527 github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a/go.mod h1:pjcozWijkNPbEtX5SIQaxEW/h8VAVZYTLx+70bmB3LY= github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+lP5lUUIzjRGDg93WrQfZJZCaV1ZP3KeyXi8bzY= github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI= -github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d h1:JcqGDiyrcaQwVyV861TUyQgO7uEmsjkhfm7aQd84dOw= -github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q= +github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260508132157-0223d2e98d28 h1:/sYlOEZav7FNOUwBMw99/VZ6QhljRCOBF6zsXWYE6O4= +github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260508132157-0223d2e98d28/go.mod h1:pzatilMEHZFT3qV7C/X3MqOa3NlRQuYhlRhZTL+hN6Q= github.com/opencloud-eu/reva/v2 v2.46.4-0.20260625152426-8cff2a7032ec h1:kL9P87cVpe8GKvo5vzLpJstpiVXFvkZn2+Dco53nfuM= github.com/opencloud-eu/reva/v2 v2.46.4-0.20260625152426-8cff2a7032ec/go.mod h1:l4CG5u8tdWZj6IyavXO9xDdnNTV85oaFudGwEDPvsnk= github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI= diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/README.md b/vendor/github.com/opencloud-eu/libre-graph-api-go/README.md index 353bd46b7a..c69e777b36 100644 --- a/vendor/github.com/opencloud-eu/libre-graph-api-go/README.md +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/README.md @@ -83,6 +83,7 @@ Class | Method | HTTP request | Description *ApplicationsApi* | [**ListApplications**](docs/ApplicationsApi.md#listapplications) | **Get** /v1.0/applications | Get all applications *DriveItemApi* | [**DeleteDriveItem**](docs/DriveItemApi.md#deletedriveitem) | **Delete** /v1beta1/drives/{drive-id}/items/{item-id} | Delete a DriveItem. *DriveItemApi* | [**GetDriveItem**](docs/DriveItemApi.md#getdriveitem) | **Get** /v1beta1/drives/{drive-id}/items/{item-id} | Get a DriveItem. +*DriveItemApi* | [**GetDriveItemContent**](docs/DriveItemApi.md#getdriveitemcontent) | **Get** /v1beta1/drives/{drive-id}/items/{item-id}/content | Download the content of a DriveItem *DriveItemApi* | [**UpdateDriveItem**](docs/DriveItemApi.md#updatedriveitem) | **Patch** /v1beta1/drives/{drive-id}/items/{item-id} | Update a DriveItem. *DrivesApi* | [**CreateDrive**](docs/DrivesApi.md#createdrive) | **Post** /v1.0/drives | Create a new drive of a specific type *DrivesApi* | [**DeleteDrive**](docs/DrivesApi.md#deletedrive) | **Delete** /v1.0/drives/{drive-id} | Delete a specific space @@ -141,6 +142,9 @@ Class | Method | HTTP request | Description *GroupApi* | [**UpdateGroup**](docs/GroupApi.md#updategroup) | **Patch** /v1.0/groups/{group-id} | Update entity in groups *GroupsApi* | [**CreateGroup**](docs/GroupsApi.md#creategroup) | **Post** /v1.0/groups | Add new entity to groups *GroupsApi* | [**ListGroups**](docs/GroupsApi.md#listgroups) | **Get** /v1.0/groups | Get entities from groups +*InvitationsApi* | [**CreateInvitation**](docs/InvitationsApi.md#createinvitation) | **Post** /v1.0/invitations | Create a new invitation +*InvitationsApi* | [**GetInvitation**](docs/InvitationsApi.md#getinvitation) | **Get** /v1.0/invitations/{invitation-id} | Get an invitation by key +*InvitationsApi* | [**ListInvitations**](docs/InvitationsApi.md#listinvitations) | **Get** /v1.0/invitations | Get a list of invitations *MeChangepasswordApi* | [**ChangeOwnPassword**](docs/MeChangepasswordApi.md#changeownpassword) | **Post** /v1.0/me/changePassword | Change your own password *MeDriveApi* | [**FollowDriveItem**](docs/MeDriveApi.md#followdriveitem) | **Post** /v1.0/me/drive/items/{item-id}/follow | Follow a DriveItem *MeDriveApi* | [**GetHome**](docs/MeDriveApi.md#gethome) | **Get** /v1.0/me/drive | Get personal space for user @@ -197,6 +201,7 @@ Class | Method | HTTP request | Description - [CollectionOfEducationClass](docs/CollectionOfEducationClass.md) - [CollectionOfEducationUser](docs/CollectionOfEducationUser.md) - [CollectionOfGroup](docs/CollectionOfGroup.md) + - [CollectionOfInvitations](docs/CollectionOfInvitations.md) - [CollectionOfPermissions](docs/CollectionOfPermissions.md) - [CollectionOfPermissionsWithAllowedValues](docs/CollectionOfPermissionsWithAllowedValues.md) - [CollectionOfSchools](docs/CollectionOfSchools.md) @@ -214,6 +219,7 @@ Class | Method | HTTP request | Description - [EducationSchool](docs/EducationSchool.md) - [EducationUser](docs/EducationUser.md) - [EducationUserReference](docs/EducationUserReference.md) + - [EmailAddress](docs/EmailAddress.md) - [ExportPersonalDataRequest](docs/ExportPersonalDataRequest.md) - [FileSystemInfo](docs/FileSystemInfo.md) - [Folder](docs/Folder.md) @@ -224,8 +230,11 @@ Class | Method | HTTP request | Description - [Identity](docs/Identity.md) - [IdentitySet](docs/IdentitySet.md) - [Image](docs/Image.md) + - [Invitation](docs/Invitation.md) + - [InvitedUserMessageInfo](docs/InvitedUserMessageInfo.md) - [ItemReference](docs/ItemReference.md) - [MemberReference](docs/MemberReference.md) + - [MotionPhoto](docs/MotionPhoto.md) - [ObjectIdentity](docs/ObjectIdentity.md) - [OdataError](docs/OdataError.md) - [OdataErrorDetail](docs/OdataErrorDetail.md) @@ -236,6 +245,7 @@ Class | Method | HTTP request | Description - [Permission](docs/Permission.md) - [Photo](docs/Photo.md) - [Quota](docs/Quota.md) + - [Recipient](docs/Recipient.md) - [RemoteItem](docs/RemoteItem.md) - [SharePointIdentitySet](docs/SharePointIdentitySet.md) - [SharingInvitation](docs/SharingInvitation.md) diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/api_drive_item.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/api_drive_item.go index a502b6abac..d11ce21051 100644 --- a/vendor/github.com/opencloud-eu/libre-graph-api-go/api_drive_item.go +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/api_drive_item.go @@ -137,6 +137,13 @@ type ApiGetDriveItemRequest struct { ApiService *DriveItemApiService driveId string itemId string + select_ *[]string +} + +// Select additional properties to be returned. +func (r ApiGetDriveItemRequest) Select_(select_ []string) ApiGetDriveItemRequest { + r.select_ = &select_ + return r } func (r ApiGetDriveItemRequest) Execute() (*DriveItem, *http.Response, error) { @@ -186,6 +193,9 @@ func (a *DriveItemApiService) GetDriveItemExecute(r ApiGetDriveItemRequest) (*Dr localVarQueryParams := url.Values{} localVarFormParams := url.Values{} + if r.select_ != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "$select", r.select_, "form", "csv") + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} @@ -248,6 +258,149 @@ func (a *DriveItemApiService) GetDriveItemExecute(r ApiGetDriveItemRequest) (*Dr return localVarReturnValue, localVarHTTPResponse, nil } +type ApiGetDriveItemContentRequest struct { + ctx context.Context + ApiService *DriveItemApiService + driveId string + itemId string +} + +func (r ApiGetDriveItemContentRequest) Execute() (*OdataError, *http.Response, error) { + return r.ApiService.GetDriveItemContentExecute(r) +} + +/* +GetDriveItemContent Download the content of a DriveItem + +Download the contents of the primary stream (file) of a driveItem. Only +driveItem objects with a `file` facet can be downloaded. + +The response is a `302 Found` redirecting to a pre-authenticated download +URL for the file. This is the same URL that is returned via the +`@microsoft.graph.downloadUrl` instance annotation on the driveItem when +requested via `$select`. Choose between the two based on whether you +want to call the redirecting `/content` endpoint directly (for example, +with a client that follows redirects automatically) or you want to +inspect / schedule / prefetch the URL yourself via the annotation. + +The pre-authenticated URL is short-lived and does not require an +`Authorization` header. + +To download a partial range of bytes, apply the `Range` header to the +redirect target (the pre-authenticated URL), not to the `/content` +request. + + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param driveId key: id of drive + @param itemId key: id of item + @return ApiGetDriveItemContentRequest +*/ +func (a *DriveItemApiService) GetDriveItemContent(ctx context.Context, driveId string, itemId string) ApiGetDriveItemContentRequest { + return ApiGetDriveItemContentRequest{ + ApiService: a, + ctx: ctx, + driveId: driveId, + itemId: itemId, + } +} + +// Execute executes the request +// @return OdataError +func (a *DriveItemApiService) GetDriveItemContentExecute(r ApiGetDriveItemContentRequest) (*OdataError, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *OdataError + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DriveItemApiService.GetDriveItemContent") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1beta1/drives/{drive-id}/items/{item-id}/content" + localVarPath = strings.Replace(localVarPath, "{"+"drive-id"+"}", url.PathEscape(parameterValueToString(r.driveId, "driveId")), -1) + localVarPath = strings.Replace(localVarPath, "{"+"item-id"+"}", url.PathEscape(parameterValueToString(r.itemId, "itemId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 404 { + var v OdataError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v OdataError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiUpdateDriveItemRequest struct { ctx context.Context ApiService *DriveItemApiService diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/api_invitations.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/api_invitations.go new file mode 100644 index 0000000000..bafad29ea7 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/api_invitations.go @@ -0,0 +1,328 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" + "strings" +) + + +// InvitationsApiService InvitationsApi service +type InvitationsApiService service + +type ApiCreateInvitationRequest struct { + ctx context.Context + ApiService *InvitationsApiService + invitation *Invitation +} + +// New invitation +func (r ApiCreateInvitationRequest) Invitation(invitation Invitation) ApiCreateInvitationRequest { + r.invitation = &invitation + return r +} + +func (r ApiCreateInvitationRequest) Execute() (*Invitation, *http.Response, error) { + return r.ApiService.CreateInvitationExecute(r) +} + +/* +CreateInvitation Create a new invitation + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiCreateInvitationRequest +*/ +func (a *InvitationsApiService) CreateInvitation(ctx context.Context) ApiCreateInvitationRequest { + return ApiCreateInvitationRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// @return Invitation +func (a *InvitationsApiService) CreateInvitationExecute(r ApiCreateInvitationRequest) (*Invitation, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *Invitation + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "InvitationsApiService.CreateInvitation") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1.0/invitations" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.invitation + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiGetInvitationRequest struct { + ctx context.Context + ApiService *InvitationsApiService + invitationId string +} + +func (r ApiGetInvitationRequest) Execute() (*Invitation, *http.Response, error) { + return r.ApiService.GetInvitationExecute(r) +} + +/* +GetInvitation Get an invitation by key + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param invitationId key: id of invitation + @return ApiGetInvitationRequest +*/ +func (a *InvitationsApiService) GetInvitation(ctx context.Context, invitationId string) ApiGetInvitationRequest { + return ApiGetInvitationRequest{ + ApiService: a, + ctx: ctx, + invitationId: invitationId, + } +} + +// Execute executes the request +// @return Invitation +func (a *InvitationsApiService) GetInvitationExecute(r ApiGetInvitationRequest) (*Invitation, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *Invitation + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "InvitationsApiService.GetInvitation") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1.0/invitations/{invitation-id}" + localVarPath = strings.Replace(localVarPath, "{"+"invitation-id"+"}", url.PathEscape(parameterValueToString(r.invitationId, "invitationId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiListInvitationsRequest struct { + ctx context.Context + ApiService *InvitationsApiService +} + +func (r ApiListInvitationsRequest) Execute() (*CollectionOfInvitations, *http.Response, error) { + return r.ApiService.ListInvitationsExecute(r) +} + +/* +ListInvitations Get a list of invitations + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiListInvitationsRequest +*/ +func (a *InvitationsApiService) ListInvitations(ctx context.Context) ApiListInvitationsRequest { + return ApiListInvitationsRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// @return CollectionOfInvitations +func (a *InvitationsApiService) ListInvitationsExecute(r ApiListInvitationsRequest) (*CollectionOfInvitations, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *CollectionOfInvitations + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "InvitationsApiService.ListInvitations") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1.0/invitations" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/client.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/client.go index 55ea08b14f..8e5063afee 100644 --- a/vendor/github.com/opencloud-eu/libre-graph-api-go/client.go +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/client.go @@ -75,6 +75,8 @@ type APIClient struct { GroupsApi *GroupsApiService + InvitationsApi *InvitationsApiService + MeChangepasswordApi *MeChangepasswordApiService MeDriveApi *MeDriveApiService @@ -131,6 +133,7 @@ func NewAPIClient(cfg *Configuration) *APIClient { c.EducationUserApi = (*EducationUserApiService)(&c.common) c.GroupApi = (*GroupApiService)(&c.common) c.GroupsApi = (*GroupsApiService)(&c.common) + c.InvitationsApi = (*InvitationsApiService)(&c.common) c.MeChangepasswordApi = (*MeChangepasswordApiService)(&c.common) c.MeDriveApi = (*MeDriveApiService)(&c.common) c.MeDriveRootApi = (*MeDriveRootApiService)(&c.common) diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_collection_of_invitations.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_collection_of_invitations.go new file mode 100644 index 0000000000..181d030ce7 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_collection_of_invitations.go @@ -0,0 +1,126 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the CollectionOfInvitations type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &CollectionOfInvitations{} + +// CollectionOfInvitations struct for CollectionOfInvitations +type CollectionOfInvitations struct { + Value []Invitation `json:"value,omitempty"` +} + +// NewCollectionOfInvitations instantiates a new CollectionOfInvitations object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewCollectionOfInvitations() *CollectionOfInvitations { + this := CollectionOfInvitations{} + return &this +} + +// NewCollectionOfInvitationsWithDefaults instantiates a new CollectionOfInvitations object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewCollectionOfInvitationsWithDefaults() *CollectionOfInvitations { + this := CollectionOfInvitations{} + return &this +} + +// GetValue returns the Value field value if set, zero value otherwise. +func (o *CollectionOfInvitations) GetValue() []Invitation { + if o == nil || IsNil(o.Value) { + var ret []Invitation + return ret + } + return o.Value +} + +// GetValueOk returns a tuple with the Value field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CollectionOfInvitations) GetValueOk() ([]Invitation, bool) { + if o == nil || IsNil(o.Value) { + return nil, false + } + return o.Value, true +} + +// HasValue returns a boolean if a field has been set. +func (o *CollectionOfInvitations) HasValue() bool { + if o != nil && !IsNil(o.Value) { + return true + } + + return false +} + +// SetValue gets a reference to the given []Invitation and assigns it to the Value field. +func (o *CollectionOfInvitations) SetValue(v []Invitation) { + o.Value = v +} + +func (o CollectionOfInvitations) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o CollectionOfInvitations) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Value) { + toSerialize["value"] = o.Value + } + return toSerialize, nil +} + +type NullableCollectionOfInvitations struct { + value *CollectionOfInvitations + isSet bool +} + +func (v NullableCollectionOfInvitations) Get() *CollectionOfInvitations { + return v.value +} + +func (v *NullableCollectionOfInvitations) Set(val *CollectionOfInvitations) { + v.value = val + v.isSet = true +} + +func (v NullableCollectionOfInvitations) IsSet() bool { + return v.isSet +} + +func (v *NullableCollectionOfInvitations) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCollectionOfInvitations(val *CollectionOfInvitations) *NullableCollectionOfInvitations { + return &NullableCollectionOfInvitations{value: val, isSet: true} +} + +func (v NullableCollectionOfInvitations) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCollectionOfInvitations) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_item.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_item.go index 09e6cf34bb..0f5fa975c7 100644 --- a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_item.go +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_drive_item.go @@ -65,8 +65,11 @@ type DriveItem struct { Permissions []Permission `json:"permissions,omitempty"` Audio *Audio `json:"audio,omitempty"` Video *Video `json:"video,omitempty"` + LibreGraphMotionPhoto *MotionPhoto `json:"@libre.graph.motionPhoto,omitempty"` // Indicates if the item is synchronized with the underlying storage provider. Read-only. ClientSynchronize *bool `json:"@client.synchronize,omitempty"` + // A pre-authenticated URL that can be used to download the item's content without providing an Authorization header. The URL is short-lived and cannot be cached. This annotation is only populated when explicitly requested via `$select`, and only for items that have a `file` facet. The returned URL is valid for a limited time and should be used promptly. + MicrosoftGraphDownloadUrl *string `json:"@microsoft.graph.downloadUrl,omitempty"` // Properties or facets (see UI.Facet) annotated with this term will not be rendered if the annotation evaluates to true. Users can set this to hide permissions. UIHidden *bool `json:"@UI.Hidden,omitempty"` } @@ -1048,6 +1051,38 @@ func (o *DriveItem) SetVideo(v Video) { o.Video = &v } +// GetLibreGraphMotionPhoto returns the LibreGraphMotionPhoto field value if set, zero value otherwise. +func (o *DriveItem) GetLibreGraphMotionPhoto() MotionPhoto { + if o == nil || IsNil(o.LibreGraphMotionPhoto) { + var ret MotionPhoto + return ret + } + return *o.LibreGraphMotionPhoto +} + +// GetLibreGraphMotionPhotoOk returns a tuple with the LibreGraphMotionPhoto field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DriveItem) GetLibreGraphMotionPhotoOk() (*MotionPhoto, bool) { + if o == nil || IsNil(o.LibreGraphMotionPhoto) { + return nil, false + } + return o.LibreGraphMotionPhoto, true +} + +// HasLibreGraphMotionPhoto returns a boolean if a field has been set. +func (o *DriveItem) HasLibreGraphMotionPhoto() bool { + if o != nil && !IsNil(o.LibreGraphMotionPhoto) { + return true + } + + return false +} + +// SetLibreGraphMotionPhoto gets a reference to the given MotionPhoto and assigns it to the LibreGraphMotionPhoto field. +func (o *DriveItem) SetLibreGraphMotionPhoto(v MotionPhoto) { + o.LibreGraphMotionPhoto = &v +} + // GetClientSynchronize returns the ClientSynchronize field value if set, zero value otherwise. func (o *DriveItem) GetClientSynchronize() bool { if o == nil || IsNil(o.ClientSynchronize) { @@ -1080,6 +1115,38 @@ func (o *DriveItem) SetClientSynchronize(v bool) { o.ClientSynchronize = &v } +// GetMicrosoftGraphDownloadUrl returns the MicrosoftGraphDownloadUrl field value if set, zero value otherwise. +func (o *DriveItem) GetMicrosoftGraphDownloadUrl() string { + if o == nil || IsNil(o.MicrosoftGraphDownloadUrl) { + var ret string + return ret + } + return *o.MicrosoftGraphDownloadUrl +} + +// GetMicrosoftGraphDownloadUrlOk returns a tuple with the MicrosoftGraphDownloadUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *DriveItem) GetMicrosoftGraphDownloadUrlOk() (*string, bool) { + if o == nil || IsNil(o.MicrosoftGraphDownloadUrl) { + return nil, false + } + return o.MicrosoftGraphDownloadUrl, true +} + +// HasMicrosoftGraphDownloadUrl returns a boolean if a field has been set. +func (o *DriveItem) HasMicrosoftGraphDownloadUrl() bool { + if o != nil && !IsNil(o.MicrosoftGraphDownloadUrl) { + return true + } + + return false +} + +// SetMicrosoftGraphDownloadUrl gets a reference to the given string and assigns it to the MicrosoftGraphDownloadUrl field. +func (o *DriveItem) SetMicrosoftGraphDownloadUrl(v string) { + o.MicrosoftGraphDownloadUrl = &v +} + // GetUIHidden returns the UIHidden field value if set, zero value otherwise. func (o *DriveItem) GetUIHidden() bool { if o == nil || IsNil(o.UIHidden) { @@ -1212,9 +1279,15 @@ func (o DriveItem) ToMap() (map[string]interface{}, error) { if !IsNil(o.Video) { toSerialize["video"] = o.Video } + if !IsNil(o.LibreGraphMotionPhoto) { + toSerialize["@libre.graph.motionPhoto"] = o.LibreGraphMotionPhoto + } if !IsNil(o.ClientSynchronize) { toSerialize["@client.synchronize"] = o.ClientSynchronize } + if !IsNil(o.MicrosoftGraphDownloadUrl) { + toSerialize["@microsoft.graph.downloadUrl"] = o.MicrosoftGraphDownloadUrl + } if !IsNil(o.UIHidden) { toSerialize["@UI.Hidden"] = o.UIHidden } diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_email_address.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_email_address.go new file mode 100644 index 0000000000..b1370999b4 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_email_address.go @@ -0,0 +1,164 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the EmailAddress type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &EmailAddress{} + +// EmailAddress Represents an email address. +type EmailAddress struct { + // The email address. + Address *string `json:"address,omitempty"` + // The name associated with the email address. + Name *string `json:"name,omitempty"` +} + +// NewEmailAddress instantiates a new EmailAddress object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewEmailAddress() *EmailAddress { + this := EmailAddress{} + return &this +} + +// NewEmailAddressWithDefaults instantiates a new EmailAddress object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewEmailAddressWithDefaults() *EmailAddress { + this := EmailAddress{} + return &this +} + +// GetAddress returns the Address field value if set, zero value otherwise. +func (o *EmailAddress) GetAddress() string { + if o == nil || IsNil(o.Address) { + var ret string + return ret + } + return *o.Address +} + +// GetAddressOk returns a tuple with the Address field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *EmailAddress) GetAddressOk() (*string, bool) { + if o == nil || IsNil(o.Address) { + return nil, false + } + return o.Address, true +} + +// HasAddress returns a boolean if a field has been set. +func (o *EmailAddress) HasAddress() bool { + if o != nil && !IsNil(o.Address) { + return true + } + + return false +} + +// SetAddress gets a reference to the given string and assigns it to the Address field. +func (o *EmailAddress) SetAddress(v string) { + o.Address = &v +} + +// GetName returns the Name field value if set, zero value otherwise. +func (o *EmailAddress) GetName() string { + if o == nil || IsNil(o.Name) { + var ret string + return ret + } + return *o.Name +} + +// GetNameOk returns a tuple with the Name field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *EmailAddress) GetNameOk() (*string, bool) { + if o == nil || IsNil(o.Name) { + return nil, false + } + return o.Name, true +} + +// HasName returns a boolean if a field has been set. +func (o *EmailAddress) HasName() bool { + if o != nil && !IsNil(o.Name) { + return true + } + + return false +} + +// SetName gets a reference to the given string and assigns it to the Name field. +func (o *EmailAddress) SetName(v string) { + o.Name = &v +} + +func (o EmailAddress) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o EmailAddress) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Address) { + toSerialize["address"] = o.Address + } + if !IsNil(o.Name) { + toSerialize["name"] = o.Name + } + return toSerialize, nil +} + +type NullableEmailAddress struct { + value *EmailAddress + isSet bool +} + +func (v NullableEmailAddress) Get() *EmailAddress { + return v.value +} + +func (v *NullableEmailAddress) Set(val *EmailAddress) { + v.value = val + v.isSet = true +} + +func (v NullableEmailAddress) IsSet() bool { + return v.isSet +} + +func (v *NullableEmailAddress) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableEmailAddress(val *EmailAddress) *NullableEmailAddress { + return &NullableEmailAddress{value: val, isSet: true} +} + +func (v NullableEmailAddress) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableEmailAddress) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_invitation.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_invitation.go new file mode 100644 index 0000000000..dcd97e9deb --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_invitation.go @@ -0,0 +1,421 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the Invitation type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Invitation{} + +// Invitation Represents an invitation to a drive item. +type Invitation struct { + // The display name of the user being invited. + InvitedUserDisplayName *string `json:"invitedUserDisplayName,omitempty"` + // The email address of the user being invited. Required. + InvitedUserEmailAddress *string `json:"invitedUserEmailAddress,omitempty"` + InvitedUserMessageInfo *InvitedUserMessageInfo `json:"invitedUserMessageInfo,omitempty"` + // Indicates whether an invitation message should be sent to the user. + SendInvitationMessage *bool `json:"sendInvitationMessage,omitempty"` + // The URL to which the user is redirected after accepting the invitation. Required. + InviteRedirectUrl *string `json:"inviteRedirectUrl,omitempty"` + // The URL that the user can use to redeem the invitation. Read-only. + InviteRedeemUrl *string `json:"inviteRedeemUrl,omitempty"` + // The status of the invitation. Read-only. + Status *string `json:"status,omitempty"` + InvitedUser *User `json:"invitedUser,omitempty"` + // The type of user being invited. + InvitedUserType *string `json:"invitedUserType,omitempty"` +} + +// NewInvitation instantiates a new Invitation object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewInvitation() *Invitation { + this := Invitation{} + return &this +} + +// NewInvitationWithDefaults instantiates a new Invitation object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewInvitationWithDefaults() *Invitation { + this := Invitation{} + return &this +} + +// GetInvitedUserDisplayName returns the InvitedUserDisplayName field value if set, zero value otherwise. +func (o *Invitation) GetInvitedUserDisplayName() string { + if o == nil || IsNil(o.InvitedUserDisplayName) { + var ret string + return ret + } + return *o.InvitedUserDisplayName +} + +// GetInvitedUserDisplayNameOk returns a tuple with the InvitedUserDisplayName field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Invitation) GetInvitedUserDisplayNameOk() (*string, bool) { + if o == nil || IsNil(o.InvitedUserDisplayName) { + return nil, false + } + return o.InvitedUserDisplayName, true +} + +// HasInvitedUserDisplayName returns a boolean if a field has been set. +func (o *Invitation) HasInvitedUserDisplayName() bool { + if o != nil && !IsNil(o.InvitedUserDisplayName) { + return true + } + + return false +} + +// SetInvitedUserDisplayName gets a reference to the given string and assigns it to the InvitedUserDisplayName field. +func (o *Invitation) SetInvitedUserDisplayName(v string) { + o.InvitedUserDisplayName = &v +} + +// GetInvitedUserEmailAddress returns the InvitedUserEmailAddress field value if set, zero value otherwise. +func (o *Invitation) GetInvitedUserEmailAddress() string { + if o == nil || IsNil(o.InvitedUserEmailAddress) { + var ret string + return ret + } + return *o.InvitedUserEmailAddress +} + +// GetInvitedUserEmailAddressOk returns a tuple with the InvitedUserEmailAddress field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Invitation) GetInvitedUserEmailAddressOk() (*string, bool) { + if o == nil || IsNil(o.InvitedUserEmailAddress) { + return nil, false + } + return o.InvitedUserEmailAddress, true +} + +// HasInvitedUserEmailAddress returns a boolean if a field has been set. +func (o *Invitation) HasInvitedUserEmailAddress() bool { + if o != nil && !IsNil(o.InvitedUserEmailAddress) { + return true + } + + return false +} + +// SetInvitedUserEmailAddress gets a reference to the given string and assigns it to the InvitedUserEmailAddress field. +func (o *Invitation) SetInvitedUserEmailAddress(v string) { + o.InvitedUserEmailAddress = &v +} + +// GetInvitedUserMessageInfo returns the InvitedUserMessageInfo field value if set, zero value otherwise. +func (o *Invitation) GetInvitedUserMessageInfo() InvitedUserMessageInfo { + if o == nil || IsNil(o.InvitedUserMessageInfo) { + var ret InvitedUserMessageInfo + return ret + } + return *o.InvitedUserMessageInfo +} + +// GetInvitedUserMessageInfoOk returns a tuple with the InvitedUserMessageInfo field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Invitation) GetInvitedUserMessageInfoOk() (*InvitedUserMessageInfo, bool) { + if o == nil || IsNil(o.InvitedUserMessageInfo) { + return nil, false + } + return o.InvitedUserMessageInfo, true +} + +// HasInvitedUserMessageInfo returns a boolean if a field has been set. +func (o *Invitation) HasInvitedUserMessageInfo() bool { + if o != nil && !IsNil(o.InvitedUserMessageInfo) { + return true + } + + return false +} + +// SetInvitedUserMessageInfo gets a reference to the given InvitedUserMessageInfo and assigns it to the InvitedUserMessageInfo field. +func (o *Invitation) SetInvitedUserMessageInfo(v InvitedUserMessageInfo) { + o.InvitedUserMessageInfo = &v +} + +// GetSendInvitationMessage returns the SendInvitationMessage field value if set, zero value otherwise. +func (o *Invitation) GetSendInvitationMessage() bool { + if o == nil || IsNil(o.SendInvitationMessage) { + var ret bool + return ret + } + return *o.SendInvitationMessage +} + +// GetSendInvitationMessageOk returns a tuple with the SendInvitationMessage field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Invitation) GetSendInvitationMessageOk() (*bool, bool) { + if o == nil || IsNil(o.SendInvitationMessage) { + return nil, false + } + return o.SendInvitationMessage, true +} + +// HasSendInvitationMessage returns a boolean if a field has been set. +func (o *Invitation) HasSendInvitationMessage() bool { + if o != nil && !IsNil(o.SendInvitationMessage) { + return true + } + + return false +} + +// SetSendInvitationMessage gets a reference to the given bool and assigns it to the SendInvitationMessage field. +func (o *Invitation) SetSendInvitationMessage(v bool) { + o.SendInvitationMessage = &v +} + +// GetInviteRedirectUrl returns the InviteRedirectUrl field value if set, zero value otherwise. +func (o *Invitation) GetInviteRedirectUrl() string { + if o == nil || IsNil(o.InviteRedirectUrl) { + var ret string + return ret + } + return *o.InviteRedirectUrl +} + +// GetInviteRedirectUrlOk returns a tuple with the InviteRedirectUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Invitation) GetInviteRedirectUrlOk() (*string, bool) { + if o == nil || IsNil(o.InviteRedirectUrl) { + return nil, false + } + return o.InviteRedirectUrl, true +} + +// HasInviteRedirectUrl returns a boolean if a field has been set. +func (o *Invitation) HasInviteRedirectUrl() bool { + if o != nil && !IsNil(o.InviteRedirectUrl) { + return true + } + + return false +} + +// SetInviteRedirectUrl gets a reference to the given string and assigns it to the InviteRedirectUrl field. +func (o *Invitation) SetInviteRedirectUrl(v string) { + o.InviteRedirectUrl = &v +} + +// GetInviteRedeemUrl returns the InviteRedeemUrl field value if set, zero value otherwise. +func (o *Invitation) GetInviteRedeemUrl() string { + if o == nil || IsNil(o.InviteRedeemUrl) { + var ret string + return ret + } + return *o.InviteRedeemUrl +} + +// GetInviteRedeemUrlOk returns a tuple with the InviteRedeemUrl field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Invitation) GetInviteRedeemUrlOk() (*string, bool) { + if o == nil || IsNil(o.InviteRedeemUrl) { + return nil, false + } + return o.InviteRedeemUrl, true +} + +// HasInviteRedeemUrl returns a boolean if a field has been set. +func (o *Invitation) HasInviteRedeemUrl() bool { + if o != nil && !IsNil(o.InviteRedeemUrl) { + return true + } + + return false +} + +// SetInviteRedeemUrl gets a reference to the given string and assigns it to the InviteRedeemUrl field. +func (o *Invitation) SetInviteRedeemUrl(v string) { + o.InviteRedeemUrl = &v +} + +// GetStatus returns the Status field value if set, zero value otherwise. +func (o *Invitation) GetStatus() string { + if o == nil || IsNil(o.Status) { + var ret string + return ret + } + return *o.Status +} + +// GetStatusOk returns a tuple with the Status field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Invitation) GetStatusOk() (*string, bool) { + if o == nil || IsNil(o.Status) { + return nil, false + } + return o.Status, true +} + +// HasStatus returns a boolean if a field has been set. +func (o *Invitation) HasStatus() bool { + if o != nil && !IsNil(o.Status) { + return true + } + + return false +} + +// SetStatus gets a reference to the given string and assigns it to the Status field. +func (o *Invitation) SetStatus(v string) { + o.Status = &v +} + +// GetInvitedUser returns the InvitedUser field value if set, zero value otherwise. +func (o *Invitation) GetInvitedUser() User { + if o == nil || IsNil(o.InvitedUser) { + var ret User + return ret + } + return *o.InvitedUser +} + +// GetInvitedUserOk returns a tuple with the InvitedUser field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Invitation) GetInvitedUserOk() (*User, bool) { + if o == nil || IsNil(o.InvitedUser) { + return nil, false + } + return o.InvitedUser, true +} + +// HasInvitedUser returns a boolean if a field has been set. +func (o *Invitation) HasInvitedUser() bool { + if o != nil && !IsNil(o.InvitedUser) { + return true + } + + return false +} + +// SetInvitedUser gets a reference to the given User and assigns it to the InvitedUser field. +func (o *Invitation) SetInvitedUser(v User) { + o.InvitedUser = &v +} + +// GetInvitedUserType returns the InvitedUserType field value if set, zero value otherwise. +func (o *Invitation) GetInvitedUserType() string { + if o == nil || IsNil(o.InvitedUserType) { + var ret string + return ret + } + return *o.InvitedUserType +} + +// GetInvitedUserTypeOk returns a tuple with the InvitedUserType field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Invitation) GetInvitedUserTypeOk() (*string, bool) { + if o == nil || IsNil(o.InvitedUserType) { + return nil, false + } + return o.InvitedUserType, true +} + +// HasInvitedUserType returns a boolean if a field has been set. +func (o *Invitation) HasInvitedUserType() bool { + if o != nil && !IsNil(o.InvitedUserType) { + return true + } + + return false +} + +// SetInvitedUserType gets a reference to the given string and assigns it to the InvitedUserType field. +func (o *Invitation) SetInvitedUserType(v string) { + o.InvitedUserType = &v +} + +func (o Invitation) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Invitation) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.InvitedUserDisplayName) { + toSerialize["invitedUserDisplayName"] = o.InvitedUserDisplayName + } + if !IsNil(o.InvitedUserEmailAddress) { + toSerialize["invitedUserEmailAddress"] = o.InvitedUserEmailAddress + } + if !IsNil(o.InvitedUserMessageInfo) { + toSerialize["invitedUserMessageInfo"] = o.InvitedUserMessageInfo + } + if !IsNil(o.SendInvitationMessage) { + toSerialize["sendInvitationMessage"] = o.SendInvitationMessage + } + if !IsNil(o.InviteRedirectUrl) { + toSerialize["inviteRedirectUrl"] = o.InviteRedirectUrl + } + if !IsNil(o.InviteRedeemUrl) { + toSerialize["inviteRedeemUrl"] = o.InviteRedeemUrl + } + if !IsNil(o.Status) { + toSerialize["status"] = o.Status + } + if !IsNil(o.InvitedUser) { + toSerialize["invitedUser"] = o.InvitedUser + } + if !IsNil(o.InvitedUserType) { + toSerialize["invitedUserType"] = o.InvitedUserType + } + return toSerialize, nil +} + +type NullableInvitation struct { + value *Invitation + isSet bool +} + +func (v NullableInvitation) Get() *Invitation { + return v.value +} + +func (v *NullableInvitation) Set(val *Invitation) { + v.value = val + v.isSet = true +} + +func (v NullableInvitation) IsSet() bool { + return v.isSet +} + +func (v *NullableInvitation) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInvitation(val *Invitation) *NullableInvitation { + return &NullableInvitation{value: val, isSet: true} +} + +func (v NullableInvitation) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInvitation) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_invited_user_message_info.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_invited_user_message_info.go new file mode 100644 index 0000000000..0b12ce6275 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_invited_user_message_info.go @@ -0,0 +1,201 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the InvitedUserMessageInfo type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &InvitedUserMessageInfo{} + +// InvitedUserMessageInfo Additional information about the invitation message. +type InvitedUserMessageInfo struct { + // Additional recipients who will receive a copy of the invitation message. + CcRecipients []Recipient `json:"ccRecipients,omitempty"` + // The customized message body that will be included in the invitation message. + CustomizedMessageBody *string `json:"customizedMessageBody,omitempty"` + // The language of the invitation message. + MessageLanguage *string `json:"messageLanguage,omitempty"` +} + +// NewInvitedUserMessageInfo instantiates a new InvitedUserMessageInfo object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewInvitedUserMessageInfo() *InvitedUserMessageInfo { + this := InvitedUserMessageInfo{} + return &this +} + +// NewInvitedUserMessageInfoWithDefaults instantiates a new InvitedUserMessageInfo object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewInvitedUserMessageInfoWithDefaults() *InvitedUserMessageInfo { + this := InvitedUserMessageInfo{} + return &this +} + +// GetCcRecipients returns the CcRecipients field value if set, zero value otherwise. +func (o *InvitedUserMessageInfo) GetCcRecipients() []Recipient { + if o == nil || IsNil(o.CcRecipients) { + var ret []Recipient + return ret + } + return o.CcRecipients +} + +// GetCcRecipientsOk returns a tuple with the CcRecipients field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *InvitedUserMessageInfo) GetCcRecipientsOk() ([]Recipient, bool) { + if o == nil || IsNil(o.CcRecipients) { + return nil, false + } + return o.CcRecipients, true +} + +// HasCcRecipients returns a boolean if a field has been set. +func (o *InvitedUserMessageInfo) HasCcRecipients() bool { + if o != nil && !IsNil(o.CcRecipients) { + return true + } + + return false +} + +// SetCcRecipients gets a reference to the given []Recipient and assigns it to the CcRecipients field. +func (o *InvitedUserMessageInfo) SetCcRecipients(v []Recipient) { + o.CcRecipients = v +} + +// GetCustomizedMessageBody returns the CustomizedMessageBody field value if set, zero value otherwise. +func (o *InvitedUserMessageInfo) GetCustomizedMessageBody() string { + if o == nil || IsNil(o.CustomizedMessageBody) { + var ret string + return ret + } + return *o.CustomizedMessageBody +} + +// GetCustomizedMessageBodyOk returns a tuple with the CustomizedMessageBody field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *InvitedUserMessageInfo) GetCustomizedMessageBodyOk() (*string, bool) { + if o == nil || IsNil(o.CustomizedMessageBody) { + return nil, false + } + return o.CustomizedMessageBody, true +} + +// HasCustomizedMessageBody returns a boolean if a field has been set. +func (o *InvitedUserMessageInfo) HasCustomizedMessageBody() bool { + if o != nil && !IsNil(o.CustomizedMessageBody) { + return true + } + + return false +} + +// SetCustomizedMessageBody gets a reference to the given string and assigns it to the CustomizedMessageBody field. +func (o *InvitedUserMessageInfo) SetCustomizedMessageBody(v string) { + o.CustomizedMessageBody = &v +} + +// GetMessageLanguage returns the MessageLanguage field value if set, zero value otherwise. +func (o *InvitedUserMessageInfo) GetMessageLanguage() string { + if o == nil || IsNil(o.MessageLanguage) { + var ret string + return ret + } + return *o.MessageLanguage +} + +// GetMessageLanguageOk returns a tuple with the MessageLanguage field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *InvitedUserMessageInfo) GetMessageLanguageOk() (*string, bool) { + if o == nil || IsNil(o.MessageLanguage) { + return nil, false + } + return o.MessageLanguage, true +} + +// HasMessageLanguage returns a boolean if a field has been set. +func (o *InvitedUserMessageInfo) HasMessageLanguage() bool { + if o != nil && !IsNil(o.MessageLanguage) { + return true + } + + return false +} + +// SetMessageLanguage gets a reference to the given string and assigns it to the MessageLanguage field. +func (o *InvitedUserMessageInfo) SetMessageLanguage(v string) { + o.MessageLanguage = &v +} + +func (o InvitedUserMessageInfo) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o InvitedUserMessageInfo) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.CcRecipients) { + toSerialize["ccRecipients"] = o.CcRecipients + } + if !IsNil(o.CustomizedMessageBody) { + toSerialize["customizedMessageBody"] = o.CustomizedMessageBody + } + if !IsNil(o.MessageLanguage) { + toSerialize["messageLanguage"] = o.MessageLanguage + } + return toSerialize, nil +} + +type NullableInvitedUserMessageInfo struct { + value *InvitedUserMessageInfo + isSet bool +} + +func (v NullableInvitedUserMessageInfo) Get() *InvitedUserMessageInfo { + return v.value +} + +func (v *NullableInvitedUserMessageInfo) Set(val *InvitedUserMessageInfo) { + v.value = val + v.isSet = true +} + +func (v NullableInvitedUserMessageInfo) IsSet() bool { + return v.isSet +} + +func (v *NullableInvitedUserMessageInfo) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInvitedUserMessageInfo(val *InvitedUserMessageInfo) *NullableInvitedUserMessageInfo { + return &NullableInvitedUserMessageInfo{value: val, isSet: true} +} + +func (v NullableInvitedUserMessageInfo) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInvitedUserMessageInfo) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_motion_photo.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_motion_photo.go new file mode 100644 index 0000000000..dad6fa38e7 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_motion_photo.go @@ -0,0 +1,201 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the MotionPhoto type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &MotionPhoto{} + +// MotionPhoto Motion Photo metadata. A Motion Photo is a still image with a short video clip appended to the end of the file. The presence of this facet on a driveItem indicates that the item is a Motion Photo; absence indicates it is not. Based on the Google Motion Photo format v1.0 specification: https://developer.android.com/media/platform/motion-photo-format +type MotionPhoto struct { + // The file format version of the Motion Photo. Currently always 1. Read-only. + Version *int32 `json:"version,omitempty"` + // Presentation timestamp in microseconds of the video frame that corresponds to the still image. A value of -1 indicates unspecified. If absent, readers should use a timestamp near the middle of the video track. Read-only. + PresentationTimestampUs *int64 `json:"presentationTimestampUs,omitempty"` + // Size in bytes of the embedded video portion of the file. The video is appended at the end of the file, so clients can fetch it with a Range request: `Range: bytes=-`. Read-only. + VideoSize *int64 `json:"videoSize,omitempty"` +} + +// NewMotionPhoto instantiates a new MotionPhoto object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewMotionPhoto() *MotionPhoto { + this := MotionPhoto{} + return &this +} + +// NewMotionPhotoWithDefaults instantiates a new MotionPhoto object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewMotionPhotoWithDefaults() *MotionPhoto { + this := MotionPhoto{} + return &this +} + +// GetVersion returns the Version field value if set, zero value otherwise. +func (o *MotionPhoto) GetVersion() int32 { + if o == nil || IsNil(o.Version) { + var ret int32 + return ret + } + return *o.Version +} + +// GetVersionOk returns a tuple with the Version field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *MotionPhoto) GetVersionOk() (*int32, bool) { + if o == nil || IsNil(o.Version) { + return nil, false + } + return o.Version, true +} + +// HasVersion returns a boolean if a field has been set. +func (o *MotionPhoto) HasVersion() bool { + if o != nil && !IsNil(o.Version) { + return true + } + + return false +} + +// SetVersion gets a reference to the given int32 and assigns it to the Version field. +func (o *MotionPhoto) SetVersion(v int32) { + o.Version = &v +} + +// GetPresentationTimestampUs returns the PresentationTimestampUs field value if set, zero value otherwise. +func (o *MotionPhoto) GetPresentationTimestampUs() int64 { + if o == nil || IsNil(o.PresentationTimestampUs) { + var ret int64 + return ret + } + return *o.PresentationTimestampUs +} + +// GetPresentationTimestampUsOk returns a tuple with the PresentationTimestampUs field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *MotionPhoto) GetPresentationTimestampUsOk() (*int64, bool) { + if o == nil || IsNil(o.PresentationTimestampUs) { + return nil, false + } + return o.PresentationTimestampUs, true +} + +// HasPresentationTimestampUs returns a boolean if a field has been set. +func (o *MotionPhoto) HasPresentationTimestampUs() bool { + if o != nil && !IsNil(o.PresentationTimestampUs) { + return true + } + + return false +} + +// SetPresentationTimestampUs gets a reference to the given int64 and assigns it to the PresentationTimestampUs field. +func (o *MotionPhoto) SetPresentationTimestampUs(v int64) { + o.PresentationTimestampUs = &v +} + +// GetVideoSize returns the VideoSize field value if set, zero value otherwise. +func (o *MotionPhoto) GetVideoSize() int64 { + if o == nil || IsNil(o.VideoSize) { + var ret int64 + return ret + } + return *o.VideoSize +} + +// GetVideoSizeOk returns a tuple with the VideoSize field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *MotionPhoto) GetVideoSizeOk() (*int64, bool) { + if o == nil || IsNil(o.VideoSize) { + return nil, false + } + return o.VideoSize, true +} + +// HasVideoSize returns a boolean if a field has been set. +func (o *MotionPhoto) HasVideoSize() bool { + if o != nil && !IsNil(o.VideoSize) { + return true + } + + return false +} + +// SetVideoSize gets a reference to the given int64 and assigns it to the VideoSize field. +func (o *MotionPhoto) SetVideoSize(v int64) { + o.VideoSize = &v +} + +func (o MotionPhoto) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o MotionPhoto) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Version) { + toSerialize["version"] = o.Version + } + if !IsNil(o.PresentationTimestampUs) { + toSerialize["presentationTimestampUs"] = o.PresentationTimestampUs + } + if !IsNil(o.VideoSize) { + toSerialize["videoSize"] = o.VideoSize + } + return toSerialize, nil +} + +type NullableMotionPhoto struct { + value *MotionPhoto + isSet bool +} + +func (v NullableMotionPhoto) Get() *MotionPhoto { + return v.value +} + +func (v *NullableMotionPhoto) Set(val *MotionPhoto) { + v.value = val + v.isSet = true +} + +func (v NullableMotionPhoto) IsSet() bool { + return v.isSet +} + +func (v *NullableMotionPhoto) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableMotionPhoto(val *MotionPhoto) *NullableMotionPhoto { + return &NullableMotionPhoto{value: val, isSet: true} +} + +func (v NullableMotionPhoto) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableMotionPhoto) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_recipient.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_recipient.go new file mode 100644 index 0000000000..90b22c2dca --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_recipient.go @@ -0,0 +1,126 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the Recipient type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &Recipient{} + +// Recipient Represents a recipient of an invitation. +type Recipient struct { + EmailAddress *EmailAddress `json:"emailAddress,omitempty"` +} + +// NewRecipient instantiates a new Recipient object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewRecipient() *Recipient { + this := Recipient{} + return &this +} + +// NewRecipientWithDefaults instantiates a new Recipient object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewRecipientWithDefaults() *Recipient { + this := Recipient{} + return &this +} + +// GetEmailAddress returns the EmailAddress field value if set, zero value otherwise. +func (o *Recipient) GetEmailAddress() EmailAddress { + if o == nil || IsNil(o.EmailAddress) { + var ret EmailAddress + return ret + } + return *o.EmailAddress +} + +// GetEmailAddressOk returns a tuple with the EmailAddress field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Recipient) GetEmailAddressOk() (*EmailAddress, bool) { + if o == nil || IsNil(o.EmailAddress) { + return nil, false + } + return o.EmailAddress, true +} + +// HasEmailAddress returns a boolean if a field has been set. +func (o *Recipient) HasEmailAddress() bool { + if o != nil && !IsNil(o.EmailAddress) { + return true + } + + return false +} + +// SetEmailAddress gets a reference to the given EmailAddress and assigns it to the EmailAddress field. +func (o *Recipient) SetEmailAddress(v EmailAddress) { + o.EmailAddress = &v +} + +func (o Recipient) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o Recipient) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.EmailAddress) { + toSerialize["emailAddress"] = o.EmailAddress + } + return toSerialize, nil +} + +type NullableRecipient struct { + value *Recipient + isSet bool +} + +func (v NullableRecipient) Get() *Recipient { + return v.value +} + +func (v *NullableRecipient) Set(val *Recipient) { + v.value = val + v.isSet = true +} + +func (v NullableRecipient) IsSet() bool { + return v.isSet +} + +func (v *NullableRecipient) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRecipient(val *Recipient) *NullableRecipient { + return &NullableRecipient{value: val, isSet: true} +} + +func (v NullableRecipient) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRecipient) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/modules.txt b/vendor/modules.txt index 8f7fb6bde2..1c6f834d18 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1361,7 +1361,7 @@ github.com/open-policy-agent/opa/v1/version # github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 ## explicit; go 1.24.6 github.com/opencloud-eu/icap-client -# github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d +# github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260508132157-0223d2e98d28 ## explicit; go 1.18 github.com/opencloud-eu/libre-graph-api-go # github.com/opencloud-eu/reva/v2 v2.46.4-0.20260625152426-8cff2a7032ec From 6db302387d5801a01c707d42c9298b654d5b6ec0 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sun, 5 Jul 2026 18:21:59 +0200 Subject: [PATCH 2/8] feat(search): add motionPhoto facet Extracts Google Motion Photo metadata (GCamera XMP) via Tika and plumbs the @libre.graph.motionPhoto facet through the index, search results and the graph DriveItem (libre-graph-api#36). --- .../opencloud/messages/search/v0/search.pb.go | 275 ++++++++++++------ .../messages/search/v0/search.pb.web.go | 36 +++ .../services/search/v0/search.swagger.json | 20 ++ .../opencloud/messages/search/v0/search.proto | 7 + services/graph/pkg/service/v0/driveitems.go | 1 + services/search/pkg/bleve/backend.go | 29 +- services/search/pkg/content/content.go | 25 +- services/search/pkg/content/tika.go | 181 +++++++++++- services/search/pkg/content/tika_test.go | 91 ++++++ .../opensearch/internal/convert/opensearch.go | 9 +- services/search/pkg/search/service.go | 1 + 11 files changed, 557 insertions(+), 118 deletions(-) diff --git a/protogen/gen/opencloud/messages/search/v0/search.pb.go b/protogen/gen/opencloud/messages/search/v0/search.pb.go index e5c74946ea..017b5b0d11 100644 --- a/protogen/gen/opencloud/messages/search/v0/search.pb.go +++ b/protogen/gen/opencloud/messages/search/v0/search.pb.go @@ -535,6 +535,69 @@ func (x *Photo) GetTakenDateTime() *timestamppb.Timestamp { return nil } +type MotionPhoto struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Version *int32 `protobuf:"varint,1,opt,name=version,proto3,oneof" json:"version,omitempty"` + PresentationTimestampUs *int64 `protobuf:"varint,2,opt,name=presentationTimestampUs,proto3,oneof" json:"presentationTimestampUs,omitempty"` + VideoSize *int64 `protobuf:"varint,3,opt,name=videoSize,proto3,oneof" json:"videoSize,omitempty"` +} + +func (x *MotionPhoto) Reset() { + *x = MotionPhoto{} + if protoimpl.UnsafeEnabled { + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MotionPhoto) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MotionPhoto) ProtoMessage() {} + +func (x *MotionPhoto) ProtoReflect() protoreflect.Message { + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MotionPhoto.ProtoReflect.Descriptor instead. +func (*MotionPhoto) Descriptor() ([]byte, []int) { + return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{6} +} + +func (x *MotionPhoto) GetVersion() int32 { + if x != nil && x.Version != nil { + return *x.Version + } + return 0 +} + +func (x *MotionPhoto) GetPresentationTimestampUs() int64 { + if x != nil && x.PresentationTimestampUs != nil { + return *x.PresentationTimestampUs + } + return 0 +} + +func (x *MotionPhoto) GetVideoSize() int64 { + if x != nil && x.VideoSize != nil { + return *x.VideoSize + } + return 0 +} + type Entity struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -560,12 +623,13 @@ type Entity struct { Image *Image `protobuf:"bytes,18,opt,name=image,proto3" json:"image,omitempty"` Photo *Photo `protobuf:"bytes,19,opt,name=photo,proto3" json:"photo,omitempty"` Favorites []string `protobuf:"bytes,20,rep,name=favorites,proto3" json:"favorites,omitempty"` + MotionPhoto *MotionPhoto `protobuf:"bytes,21,opt,name=motionPhoto,proto3" json:"motionPhoto,omitempty"` } func (x *Entity) Reset() { *x = Entity{} if protoimpl.UnsafeEnabled { - mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[6] + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -578,7 +642,7 @@ func (x *Entity) String() string { func (*Entity) ProtoMessage() {} func (x *Entity) ProtoReflect() protoreflect.Message { - mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[6] + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -591,7 +655,7 @@ func (x *Entity) ProtoReflect() protoreflect.Message { // Deprecated: Use Entity.ProtoReflect.Descriptor instead. func (*Entity) Descriptor() ([]byte, []int) { - return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{6} + return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{7} } func (x *Entity) GetRef() *Reference { @@ -734,6 +798,13 @@ func (x *Entity) GetFavorites() []string { return nil } +func (x *Entity) GetMotionPhoto() *MotionPhoto { + if x != nil { + return x.MotionPhoto + } + return nil +} + type Match struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -748,7 +819,7 @@ type Match struct { func (x *Match) Reset() { *x = Match{} if protoimpl.UnsafeEnabled { - mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[7] + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -761,7 +832,7 @@ func (x *Match) String() string { func (*Match) ProtoMessage() {} func (x *Match) ProtoReflect() protoreflect.Message { - mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[7] + mi := &file_opencloud_messages_search_v0_search_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -774,7 +845,7 @@ func (x *Match) ProtoReflect() protoreflect.Message { // Deprecated: Use Match.ProtoReflect.Descriptor instead. func (*Match) Descriptor() ([]byte, []int) { - return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{7} + return file_opencloud_messages_search_v0_search_proto_rawDescGZIP(), []int{8} } func (x *Match) GetEntity() *Entity { @@ -908,74 +979,91 @@ var file_opencloud_messages_search_v0_search_proto_rawDesc = []byte{ 0x6c, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x69, 0x73, 0x6f, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x6f, 0x72, 0x69, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x74, 0x61, 0x6b, 0x65, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, - 0x65, 0x22, 0xfa, 0x06, 0x0a, 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x03, - 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x70, 0x65, 0x6e, - 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, - 0x63, 0x65, 0x52, 0x03, 0x72, 0x65, 0x66, 0x12, 0x38, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, - 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x65, 0x74, 0x61, 0x67, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x65, 0x74, 0x61, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, - 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x48, 0x0a, - 0x12, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x69, 0x66, - 0x69, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, - 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, - 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x65, 0x52, 0x6f, 0x6f, - 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x68, 0x61, - 0x72, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x45, 0x0a, 0x09, 0x70, 0x61, - 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, - 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x49, - 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, - 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, - 0x68, 0x74, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x68, 0x69, 0x67, 0x68, 0x6c, - 0x69, 0x67, 0x68, 0x74, 0x73, 0x12, 0x39, 0x0a, 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x18, 0x0f, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, + 0x65, 0x22, 0xc4, 0x01, 0x0a, 0x0b, 0x4d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x68, 0x6f, 0x74, + 0x6f, 0x12, 0x1d, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x05, 0x48, 0x00, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, + 0x12, 0x3d, 0x0a, 0x17, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x55, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x48, 0x01, 0x52, 0x17, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x55, 0x73, 0x88, 0x01, 0x01, 0x12, + 0x21, 0x0a, 0x09, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x53, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x03, 0x48, 0x02, 0x52, 0x09, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x53, 0x69, 0x7a, 0x65, 0x88, + 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x1a, + 0x0a, 0x18, 0x5f, 0x70, 0x72, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x55, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x76, + 0x69, 0x64, 0x65, 0x6f, 0x53, 0x69, 0x7a, 0x65, 0x22, 0xc7, 0x07, 0x0a, 0x06, 0x45, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x03, 0x72, 0x65, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x27, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, + 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x03, 0x72, 0x65, 0x66, 0x12, 0x38, + 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x70, 0x65, + 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, + 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x44, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x65, 0x74, 0x61, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x65, 0x74, 0x61, 0x67, + 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, + 0x73, 0x69, 0x7a, 0x65, 0x12, 0x48, 0x0a, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x6d, 0x6f, 0x64, + 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x6c, 0x61, + 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, + 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x70, + 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x12, 0x0a, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x73, + 0x68, 0x61, 0x72, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x45, 0x0a, 0x09, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0c, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, - 0x2e, 0x76, 0x30, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x6f, 0x52, 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, - 0x12, 0x48, 0x0a, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x10, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, - 0x30, 0x2e, 0x47, 0x65, 0x6f, 0x43, 0x6f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x73, - 0x52, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4e, 0x0a, 0x0e, 0x72, 0x65, - 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, - 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x52, 0x0c, 0x72, 0x65, - 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x05, 0x69, 0x6d, - 0x61, 0x67, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x70, 0x65, 0x6e, + 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x44, 0x52, 0x08, + 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, + 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x1e, 0x0a, 0x0a, + 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x73, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x73, 0x12, 0x39, 0x0a, 0x05, + 0x61, 0x75, 0x64, 0x69, 0x6f, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x70, + 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, + 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x41, 0x75, 0x64, 0x69, 0x6f, + 0x52, 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x12, 0x48, 0x0a, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x05, - 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x39, 0x0a, 0x05, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x18, 0x13, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, - 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, - 0x2e, 0x76, 0x30, 0x2e, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x52, 0x05, 0x70, 0x68, 0x6f, 0x74, 0x6f, - 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x73, 0x18, 0x14, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x09, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x73, 0x22, 0x5b, - 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x3c, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, - 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, - 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x4d, 0x5a, 0x4b, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, - 0x6f, 0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x70, - 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, - 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x30, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x47, 0x65, 0x6f, 0x43, 0x6f, 0x6f, 0x72, + 0x64, 0x69, 0x6e, 0x61, 0x74, 0x65, 0x73, 0x52, 0x08, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x4e, 0x0a, 0x0e, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x69, 0x74, 0x65, 0x6d, + 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x70, 0x65, 0x6e, + 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x44, 0x52, 0x0c, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x49, + 0x64, 0x12, 0x39, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x23, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, + 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x39, 0x0a, 0x05, + 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x70, + 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, + 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x50, 0x68, 0x6f, 0x74, 0x6f, + 0x52, 0x05, 0x70, 0x68, 0x6f, 0x74, 0x6f, 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x61, 0x76, 0x6f, 0x72, + 0x69, 0x74, 0x65, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x66, 0x61, 0x76, 0x6f, + 0x72, 0x69, 0x74, 0x65, 0x73, 0x12, 0x4b, 0x0a, 0x0b, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x50, + 0x68, 0x6f, 0x74, 0x6f, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x70, 0x65, + 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, + 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x4d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, + 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x52, 0x0b, 0x6d, 0x6f, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x68, 0x6f, + 0x74, 0x6f, 0x22, 0x5b, 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x3c, 0x0a, 0x06, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x70, + 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, + 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x45, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, + 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x42, + 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, + 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, + 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, + 0x6e, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x30, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -990,7 +1078,7 @@ func file_opencloud_messages_search_v0_search_proto_rawDescGZIP() []byte { return file_opencloud_messages_search_v0_search_proto_rawDescData } -var file_opencloud_messages_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_opencloud_messages_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_opencloud_messages_search_v0_search_proto_goTypes = []interface{}{ (*ResourceID)(nil), // 0: opencloud.messages.search.v0.ResourceID (*Reference)(nil), // 1: opencloud.messages.search.v0.Reference @@ -998,28 +1086,30 @@ var file_opencloud_messages_search_v0_search_proto_goTypes = []interface{}{ (*Image)(nil), // 3: opencloud.messages.search.v0.Image (*GeoCoordinates)(nil), // 4: opencloud.messages.search.v0.GeoCoordinates (*Photo)(nil), // 5: opencloud.messages.search.v0.Photo - (*Entity)(nil), // 6: opencloud.messages.search.v0.Entity - (*Match)(nil), // 7: opencloud.messages.search.v0.Match - (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp + (*MotionPhoto)(nil), // 6: opencloud.messages.search.v0.MotionPhoto + (*Entity)(nil), // 7: opencloud.messages.search.v0.Entity + (*Match)(nil), // 8: opencloud.messages.search.v0.Match + (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp } var file_opencloud_messages_search_v0_search_proto_depIdxs = []int32{ 0, // 0: opencloud.messages.search.v0.Reference.resource_id:type_name -> opencloud.messages.search.v0.ResourceID - 8, // 1: opencloud.messages.search.v0.Photo.takenDateTime:type_name -> google.protobuf.Timestamp + 9, // 1: opencloud.messages.search.v0.Photo.takenDateTime:type_name -> google.protobuf.Timestamp 1, // 2: opencloud.messages.search.v0.Entity.ref:type_name -> opencloud.messages.search.v0.Reference 0, // 3: opencloud.messages.search.v0.Entity.id:type_name -> opencloud.messages.search.v0.ResourceID - 8, // 4: opencloud.messages.search.v0.Entity.last_modified_time:type_name -> google.protobuf.Timestamp + 9, // 4: opencloud.messages.search.v0.Entity.last_modified_time:type_name -> google.protobuf.Timestamp 0, // 5: opencloud.messages.search.v0.Entity.parent_id:type_name -> opencloud.messages.search.v0.ResourceID 2, // 6: opencloud.messages.search.v0.Entity.audio:type_name -> opencloud.messages.search.v0.Audio 4, // 7: opencloud.messages.search.v0.Entity.location:type_name -> opencloud.messages.search.v0.GeoCoordinates 0, // 8: opencloud.messages.search.v0.Entity.remote_item_id:type_name -> opencloud.messages.search.v0.ResourceID 3, // 9: opencloud.messages.search.v0.Entity.image:type_name -> opencloud.messages.search.v0.Image 5, // 10: opencloud.messages.search.v0.Entity.photo:type_name -> opencloud.messages.search.v0.Photo - 6, // 11: opencloud.messages.search.v0.Match.entity:type_name -> opencloud.messages.search.v0.Entity - 12, // [12:12] is the sub-list for method output_type - 12, // [12:12] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 6, // 11: opencloud.messages.search.v0.Entity.motionPhoto:type_name -> opencloud.messages.search.v0.MotionPhoto + 7, // 12: opencloud.messages.search.v0.Match.entity:type_name -> opencloud.messages.search.v0.Entity + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_opencloud_messages_search_v0_search_proto_init() } @@ -1101,7 +1191,7 @@ func file_opencloud_messages_search_v0_search_proto_init() { } } file_opencloud_messages_search_v0_search_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Entity); i { + switch v := v.(*MotionPhoto); i { case 0: return &v.state case 1: @@ -1113,6 +1203,18 @@ func file_opencloud_messages_search_v0_search_proto_init() { } } file_opencloud_messages_search_v0_search_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Entity); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_opencloud_messages_search_v0_search_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Match); i { case 0: return &v.state @@ -1129,13 +1231,14 @@ func file_opencloud_messages_search_v0_search_proto_init() { file_opencloud_messages_search_v0_search_proto_msgTypes[3].OneofWrappers = []interface{}{} file_opencloud_messages_search_v0_search_proto_msgTypes[4].OneofWrappers = []interface{}{} file_opencloud_messages_search_v0_search_proto_msgTypes[5].OneofWrappers = []interface{}{} + file_opencloud_messages_search_v0_search_proto_msgTypes[6].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_opencloud_messages_search_v0_search_proto_rawDesc, NumEnums: 0, - NumMessages: 8, + NumMessages: 9, NumExtensions: 0, NumServices: 0, }, diff --git a/protogen/gen/opencloud/messages/search/v0/search.pb.web.go b/protogen/gen/opencloud/messages/search/v0/search.pb.web.go index 22c3e08238..c17bd156b9 100644 --- a/protogen/gen/opencloud/messages/search/v0/search.pb.web.go +++ b/protogen/gen/opencloud/messages/search/v0/search.pb.web.go @@ -226,6 +226,42 @@ func (m *Photo) UnmarshalJSON(b []byte) error { var _ json.Unmarshaler = (*Photo)(nil) +// MotionPhotoJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of MotionPhoto. This struct is safe to replace or modify but +// should not be done so concurrently. +var MotionPhotoJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *MotionPhoto) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := MotionPhotoJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*MotionPhoto)(nil) + +// MotionPhotoJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of MotionPhoto. This struct is safe to replace or modify but +// should not be done so concurrently. +var MotionPhotoJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *MotionPhoto) UnmarshalJSON(b []byte) error { + return MotionPhotoJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*MotionPhoto)(nil) + // EntityJSONMarshaler describes the default jsonpb.Marshaler used by all // instances of Entity. This struct is safe to replace or modify but // should not be done so concurrently. diff --git a/protogen/gen/opencloud/services/search/v0/search.swagger.json b/protogen/gen/opencloud/services/search/v0/search.swagger.json index a8d7b9bd8f..a6c1b6f2d0 100644 --- a/protogen/gen/opencloud/services/search/v0/search.swagger.json +++ b/protogen/gen/opencloud/services/search/v0/search.swagger.json @@ -288,6 +288,9 @@ "items": { "type": "string" } + }, + "motionPhoto": { + "$ref": "#/definitions/v0MotionPhoto" } } }, @@ -352,6 +355,23 @@ } } }, + "v0MotionPhoto": { + "type": "object", + "properties": { + "version": { + "type": "integer", + "format": "int32" + }, + "presentationTimestampUs": { + "type": "string", + "format": "int64" + }, + "videoSize": { + "type": "string", + "format": "int64" + } + } + }, "v0Photo": { "type": "object", "properties": { diff --git a/protogen/proto/opencloud/messages/search/v0/search.proto b/protogen/proto/opencloud/messages/search/v0/search.proto index 4633a2cee7..2cab8ce4c6 100644 --- a/protogen/proto/opencloud/messages/search/v0/search.proto +++ b/protogen/proto/opencloud/messages/search/v0/search.proto @@ -59,6 +59,12 @@ message Photo { optional google.protobuf.Timestamp takenDateTime = 9; } +message MotionPhoto { + optional int32 version = 1; + optional int64 presentationTimestampUs = 2; + optional int64 videoSize = 3; +} + message Entity { Reference ref = 1; ResourceID id = 2; @@ -80,6 +86,7 @@ message Entity { Image image = 18; Photo photo = 19; repeated string favorites = 20; + MotionPhoto motionPhoto = 21; } message Match { diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index 1acd7b9215..46b110c3ea 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -456,6 +456,7 @@ func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *sto driveItem.Image = metadataToFacet[libregraph.Image](metadata, "image") driveItem.Location = metadataToFacet[libregraph.GeoCoordinates](metadata, "location") driveItem.Photo = metadataToFacet[libregraph.Photo](metadata, "photo") + driveItem.LibreGraphMotionPhoto = metadataToFacet[libregraph.MotionPhoto](metadata, "motionPhoto") } return driveItem, nil diff --git a/services/search/pkg/bleve/backend.go b/services/search/pkg/bleve/backend.go index 673e61de2c..2c8ae24936 100644 --- a/services/search/pkg/bleve/backend.go +++ b/services/search/pkg/bleve/backend.go @@ -126,20 +126,21 @@ func (b *Backend) Search(_ context.Context, sir *searchService.SearchIndexReques ResourceId: resourceIDtoSearchID(rootID), Path: getFieldValue[string](hit.Fields, "Path"), }, - Id: resourceIDtoSearchID(rID), - Name: getFieldValue[string](hit.Fields, "Name"), - ParentId: resourceIDtoSearchID(pID), - Size: uint64(getFieldValue[float64](hit.Fields, "Size")), - Type: uint64(getFieldValue[float64](hit.Fields, "Type")), - MimeType: getFieldValue[string](hit.Fields, "MimeType"), - Deleted: getFieldValue[bool](hit.Fields, "Deleted"), - Tags: getFieldSliceValue[string](hit.Fields, "Tags"), - Favorites: getFieldSliceValue[string](hit.Fields, "Favorites"), - Highlights: getFragmentValue(hit.Fragments, "Content", 0), - Audio: hitToFacet[searchMessage.Audio](hit.Fields, "audio"), - Image: hitToFacet[searchMessage.Image](hit.Fields, "image"), - Location: hitToFacet[searchMessage.GeoCoordinates](hit.Fields, "location"), - Photo: hitToFacet[searchMessage.Photo](hit.Fields, "photo"), + Id: resourceIDtoSearchID(rID), + Name: getFieldValue[string](hit.Fields, "Name"), + ParentId: resourceIDtoSearchID(pID), + Size: uint64(getFieldValue[float64](hit.Fields, "Size")), + Type: uint64(getFieldValue[float64](hit.Fields, "Type")), + MimeType: getFieldValue[string](hit.Fields, "MimeType"), + Deleted: getFieldValue[bool](hit.Fields, "Deleted"), + Tags: getFieldSliceValue[string](hit.Fields, "Tags"), + Favorites: getFieldSliceValue[string](hit.Fields, "Favorites"), + Highlights: getFragmentValue(hit.Fragments, "Content", 0), + Audio: hitToFacet[searchMessage.Audio](hit.Fields, "audio"), + Image: hitToFacet[searchMessage.Image](hit.Fields, "image"), + Location: hitToFacet[searchMessage.GeoCoordinates](hit.Fields, "location"), + Photo: hitToFacet[searchMessage.Photo](hit.Fields, "photo"), + MotionPhoto: hitToFacet[searchMessage.MotionPhoto](hit.Fields, "motionPhoto"), }, } diff --git a/services/search/pkg/content/content.go b/services/search/pkg/content/content.go index b162ccdcfc..314f87a171 100644 --- a/services/search/pkg/content/content.go +++ b/services/search/pkg/content/content.go @@ -14,18 +14,19 @@ func init() { // Document wraps all resource meta fields, // it is used as a content extraction result. type Document struct { - Title string `json:"Title"` - Name string `json:"Name"` - Content string `json:"Content"` - Size uint64 `json:"Size"` - Mtime string `json:"Mtime"` - MimeType string `json:"MimeType"` - Tags []string `json:"Tags"` - Favorites []string `json:"Favorites"` - Audio *libregraph.Audio `json:"audio,omitempty"` - Image *libregraph.Image `json:"image,omitempty"` - Location *libregraph.GeoCoordinates `json:"location,omitempty"` - Photo *libregraph.Photo `json:"photo,omitempty"` + Title string `json:"Title"` + Name string `json:"Name"` + Content string `json:"Content"` + Size uint64 `json:"Size"` + Mtime string `json:"Mtime"` + MimeType string `json:"MimeType"` + Tags []string `json:"Tags"` + Favorites []string `json:"Favorites"` + Audio *libregraph.Audio `json:"audio,omitempty"` + Image *libregraph.Image `json:"image,omitempty"` + Location *libregraph.GeoCoordinates `json:"location,omitempty"` + Photo *libregraph.Photo `json:"photo,omitempty"` + MotionPhoto *libregraph.MotionPhoto `json:"motionPhoto,omitempty"` } func CleanString(content, langCode string) string { diff --git a/services/search/pkg/content/tika.go b/services/search/pkg/content/tika.go index 8b6ac5e60b..e329f4747a 100644 --- a/services/search/pkg/content/tika.go +++ b/services/search/pkg/content/tika.go @@ -1,8 +1,11 @@ package content import ( + "bytes" "context" + "encoding/xml" "fmt" + "io" "math" "strconv" "strings" @@ -11,8 +14,8 @@ import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/google/go-tika/tika" - "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" libregraph "github.com/opencloud-eu/libre-graph-api-go" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/services/search/pkg/config" @@ -77,7 +80,19 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, } defer data.Close() - metas, err := t.tika.MetaRecursive(ctx, data) + // Motion photos keep their metadata in vendor XMP that stock Tika does not + // expose (see getMotionPhoto). For images, buffer the head so we can read + // that XMP ourselves as a fallback; the rest still streams to Tika. + var xmpHead []byte + stream := io.Reader(data) + if strings.HasPrefix(ri.GetMimeType(), "image/") { + xmpHead = make([]byte, motionPhotoXMPScanLimit) + n, _ := io.ReadFull(data, xmpHead) + xmpHead = xmpHead[:n] + stream = io.MultiReader(bytes.NewReader(xmpHead), data) + } + + metas, err := t.tika.MetaRecursive(ctx, stream) if err != nil { return doc, err } @@ -94,12 +109,18 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, doc.Location = t.getLocation(meta) doc.Image = t.getImage(meta) doc.Photo = t.getPhoto(meta) + doc.MotionPhoto = t.getMotionPhoto(meta) if contentType, err := getFirstValue(meta, "Content-Type"); err == nil && strings.HasPrefix(contentType, "audio/") { doc.Audio = t.getAudio(meta) } } + // Tika did not surface the motion photo XMP: read it from the buffered head. + if doc.MotionPhoto == nil && xmpHead != nil { + doc.MotionPhoto = motionPhotoFromXMP(xmpHead) + } + if langCode, _ := t.tika.LanguageString(ctx, doc.Content); langCode != "" && t.CleanStopWords { doc.Content = CleanString(doc.Content, langCode) } @@ -226,6 +247,162 @@ func (t Tika) getPhoto(meta map[string][]string) *libregraph.Photo { return photo } +// getMotionPhoto reads Google Motion Photo XMP, which Tika exposes under the +// canonical Camera/Container prefixes. It covers both the current MotionPhoto +// scheme and the legacy MicroVideo scheme. videoSize (the embedded video's byte +// length, needed to range-fetch it) is required, so the facet is dropped without it. +func (t Tika) getMotionPhoto(meta map[string][]string) *libregraph.MotionPhoto { + var motionPhoto *libregraph.MotionPhoto + initMotionPhoto := func() { + if motionPhoto == nil { + motionPhoto = libregraph.NewMotionPhoto() + } + } + + if v, ok := firstValue(meta, "Camera:MotionPhotoVersion", "Camera:MicroVideoVersion"); ok { + if i, err := strconv.ParseInt(v, 0, 32); err == nil { + initMotionPhoto() + motionPhoto.SetVersion(int32(i)) + } + } + + if v, ok := firstValue(meta, "Camera:MotionPhotoPresentationTimestampUs", "Camera:MicroVideoPresentationTimestampUs"); ok { + if i, err := strconv.ParseInt(v, 0, 64); err == nil { + initMotionPhoto() + motionPhoto.SetPresentationTimestampUs(i) + } + } + + if size, ok := motionPhotoVideoSize(meta); ok { + initMotionPhoto() + motionPhoto.SetVideoSize(size) + } + + if motionPhoto == nil || !motionPhoto.HasVideoSize() { + return nil + } + return motionPhoto +} + +// motionPhotoVideoSize returns the embedded video's byte length. Legacy files +// carry it as the MicroVideo offset (bytes from EOF to the video start, which +// equals its length); current files expose it as the length of the Container +// item whose semantic is "MotionPhoto". +func motionPhotoVideoSize(meta map[string][]string) (int64, bool) { + if v, ok := firstValue(meta, "Camera:MicroVideoOffset"); ok { + if i, err := strconv.ParseInt(v, 0, 64); err == nil { + return i, true + } + } + for k, vals := range meta { + if !strings.HasSuffix(k, "/Item:Semantic") || len(vals) == 0 || vals[0] != "MotionPhoto" { + continue + } + if v, ok := firstValue(meta, strings.TrimSuffix(k, "/Item:Semantic")+"/Item:Length"); ok { + if i, err := strconv.ParseInt(v, 0, 64); err == nil { + return i, true + } + } + } + return 0, false +} + +const motionPhotoXMPScanLimit = 256 * 1024 + +// motionPhotoFromXMP parses Google Motion Photo metadata directly from a file's +// XMP packet, covering the current MotionPhoto scheme and the legacy MicroVideo +// scheme. It matches on namespace URIs, so the declared prefix does not matter. +// videoSize is required, so the facet is dropped without it. +func motionPhotoFromXMP(data []byte) *libregraph.MotionPhoto { + const cameraNS = "http://ns.google.com/photos/1.0/camera/" + const itemNS = "http://ns.google.com/photos/1.0/container/item/" + + packet := extractXMPPacket(data) + if packet == nil { + return nil + } + + var motionPhoto *libregraph.MotionPhoto + initMotionPhoto := func() { + if motionPhoto == nil { + motionPhoto = libregraph.NewMotionPhoto() + } + } + + decoder := xml.NewDecoder(bytes.NewReader(packet)) + for { + token, err := decoder.Token() + if err != nil { + break + } + element, ok := token.(xml.StartElement) + if !ok { + continue + } + + var itemSemantic, itemLength string + for _, attr := range element.Attr { + switch { + case attr.Name.Space == cameraNS && (attr.Name.Local == "MotionPhotoVersion" || attr.Name.Local == "MicroVideoVersion"): + if i, err := strconv.ParseInt(attr.Value, 10, 32); err == nil { + initMotionPhoto() + motionPhoto.SetVersion(int32(i)) + } + case attr.Name.Space == cameraNS && (attr.Name.Local == "MotionPhotoPresentationTimestampUs" || attr.Name.Local == "MicroVideoPresentationTimestampUs"): + if i, err := strconv.ParseInt(attr.Value, 10, 64); err == nil { + initMotionPhoto() + motionPhoto.SetPresentationTimestampUs(i) + } + case attr.Name.Space == cameraNS && attr.Name.Local == "MicroVideoOffset": + if i, err := strconv.ParseInt(attr.Value, 10, 64); err == nil { + initMotionPhoto() + motionPhoto.SetVideoSize(i) + } + case attr.Name.Space == itemNS && attr.Name.Local == "Semantic": + itemSemantic = attr.Value + case attr.Name.Space == itemNS && attr.Name.Local == "Length": + itemLength = attr.Value + } + } + // current format: the video is the container item whose semantic is MotionPhoto. + if itemSemantic == "MotionPhoto" && itemLength != "" { + if i, err := strconv.ParseInt(itemLength, 10, 64); err == nil { + initMotionPhoto() + motionPhoto.SetVideoSize(i) + } + } + } + + if motionPhoto == nil || !motionPhoto.HasVideoSize() { + return nil + } + return motionPhoto +} + +// extractXMPPacket returns the element from a file's leading bytes, +// or nil when none is present. +func extractXMPPacket(data []byte) []byte { + start := bytes.Index(data, []byte("")) + if end < 0 { + return nil + } + return data[start : start+end+len("")] +} + +// firstValue returns the first metadata value present among keys. +func firstValue(meta map[string][]string, keys ...string) (string, bool) { + for _, k := range keys { + if v, err := getFirstValue(meta, k); err == nil { + return v, true + } + } + return "", false +} + func (t Tika) getAudio(meta map[string][]string) *libregraph.Audio { var audio *libregraph.Audio initAudio := func() { diff --git a/services/search/pkg/content/tika_test.go b/services/search/pkg/content/tika_test.go index b6ee193528..ca37fe2006 100644 --- a/services/search/pkg/content/tika_test.go +++ b/services/search/pkg/content/tika_test.go @@ -216,6 +216,97 @@ var _ = Describe("Tika", func() { Expect(photo.TakenDateTime).To(Equal(libregraph.PtrTime(time.Date(2018, 1, 1, 12, 34, 56, 0, time.UTC)))) }) + It("adds motion photo content from the legacy MicroVideo format", func() { + fullResponse = `[ + { + "Camera:MicroVideoVersion": "1", + "Camera:MicroVideoPresentationTimestampUs": "500000", + "Camera:MicroVideoOffset": "1234567" + } + ]` + doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{ + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Size: 1, + }) + Expect(err).ToNot(HaveOccurred()) + + motionPhoto := doc.MotionPhoto + Expect(motionPhoto).ToNot(BeNil()) + Expect(motionPhoto.Version).To(Equal(libregraph.PtrInt32(1))) + Expect(motionPhoto.PresentationTimestampUs).To(Equal(libregraph.PtrInt64(500000))) + Expect(motionPhoto.VideoSize).To(Equal(libregraph.PtrInt64(1234567))) + }) + + It("adds motion photo content from the current Container format", func() { + // videoSize is the length of the container item whose semantic is MotionPhoto. + fullResponse = `[ + { + "Camera:MotionPhotoVersion": "1", + "Camera:MotionPhotoPresentationTimestampUs": "500000", + "Container:Directory[1]/Item:Semantic": "Primary", + "Container:Directory[2]/Item:Semantic": "MotionPhoto", + "Container:Directory[2]/Item:Length": "122562" + } + ]` + doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{ + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Size: 1, + }) + Expect(err).ToNot(HaveOccurred()) + + motionPhoto := doc.MotionPhoto + Expect(motionPhoto).ToNot(BeNil()) + Expect(motionPhoto.Version).To(Equal(libregraph.PtrInt32(1))) + Expect(motionPhoto.PresentationTimestampUs).To(Equal(libregraph.PtrInt64(500000))) + Expect(motionPhoto.VideoSize).To(Equal(libregraph.PtrInt64(122562))) + }) + + It("skips a motion photo without a video size", func() { + // Without a video size the facet cannot drive the range fetch, so it is not stored. + fullResponse = `[ + { + "Camera:MotionPhotoVersion": "1", + "Camera:MotionPhotoPresentationTimestampUs": "500000" + } + ]` + doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{ + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Size: 1, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(doc.MotionPhoto).To(BeNil()) + }) + + It("reads the motion photo XMP directly when Tika does not expose it", func() { + xmp := `` + + `` + + `` + + `` + + `` + + `` + + retriever := &contentMocks.Retriever{} + retriever.On("Retrieve", mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader(xmp)), nil) + tika.Retriever = retriever + + doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{ + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Size: 1, + MimeType: "image/jpeg", + }) + Expect(err).ToNot(HaveOccurred()) + + motionPhoto := doc.MotionPhoto + Expect(motionPhoto).ToNot(BeNil()) + Expect(motionPhoto.Version).To(Equal(libregraph.PtrInt32(1))) + Expect(motionPhoto.PresentationTimestampUs).To(Equal(libregraph.PtrInt64(500000))) + Expect(motionPhoto.VideoSize).To(Equal(libregraph.PtrInt64(122562))) + }) + It("removes stop words", func() { body = "body to test stop words!!! against almost everyone" language = "en" diff --git a/services/search/pkg/opensearch/internal/convert/opensearch.go b/services/search/pkg/opensearch/internal/convert/opensearch.go index 1eef75ac88..92d6f7853a 100644 --- a/services/search/pkg/opensearch/internal/convert/opensearch.go +++ b/services/search/pkg/opensearch/internal/convert/opensearch.go @@ -79,10 +79,11 @@ func OpenSearchHitToMatch(hit opensearchgoAPI.SearchHit) (*searchMessage.Match, return strings.Join(contentHighlights[:], "; ") }(), - Audio: copyFacet[searchMessage.Audio](resource.Audio), - Image: copyFacet[searchMessage.Image](resource.Image), - Location: copyFacet[searchMessage.GeoCoordinates](resource.Location), - Photo: copyFacet[searchMessage.Photo](resource.Photo), + Audio: copyFacet[searchMessage.Audio](resource.Audio), + Image: copyFacet[searchMessage.Image](resource.Image), + Location: copyFacet[searchMessage.GeoCoordinates](resource.Location), + Photo: copyFacet[searchMessage.Photo](resource.Photo), + MotionPhoto: copyFacet[searchMessage.MotionPhoto](resource.MotionPhoto), }, } diff --git a/services/search/pkg/search/service.go b/services/search/pkg/search/service.go index 2c8221b2a7..ffa453713b 100644 --- a/services/search/pkg/search/service.go +++ b/services/search/pkg/search/service.go @@ -633,6 +633,7 @@ func (s *Service) doUpsertItem(ref *provider.Reference, batch BatchOperator) { facetToMetadata(metadata, doc.Image, "libre.graph.image.") facetToMetadata(metadata, doc.Location, "libre.graph.location.") facetToMetadata(metadata, doc.Photo, "libre.graph.photo.") + facetToMetadata(metadata, doc.MotionPhoto, "libre.graph.motionPhoto.") if len(metadata) == 0 { return } From c869eadab4c42acba534e4c33c53cf563ac020e4 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 7 Jul 2026 01:45:03 +0200 Subject: [PATCH 3/8] chore(reva): expose motionPhoto facet via PROPFIND [vendored demo] Vendored patch to reva/v2 ocdav propfind, mirroring the audio/photo/location/image facets, so the web client (which lists folders via PROPFIND, not Graph) receives . Demonstrator only: to be upstreamed as a proper reva PR. A `go mod vendor` would overwrite this. --- .../services/owncloud/ocdav/propfind/propfind.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind/propfind.go b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind/propfind.go index d86367a2c5..f40f4ec082 100644 --- a/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind/propfind.go +++ b/vendor/github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind/propfind.go @@ -107,6 +107,11 @@ var ( "orientation", "takenDateTime", } + motionPhotoKeys = []string{ + "version", + "presentationTimestampUs", + "videoSize", + } ) type countingReader struct { @@ -887,6 +892,8 @@ func metadataKeys(pf XML) ([]string, []string) { metadataKeys = append(metadataKeys, metadataKeysWithPrefix("libre.graph.image", imageKeys)...) case "http://owncloud.org/ns/photo": metadataKeys = append(metadataKeys, metadataKeysWithPrefix("libre.graph.photo", photoKeys)...) + case "http://owncloud.org/ns/motionPhoto": + metadataKeys = append(metadataKeys, metadataKeysWithPrefix("libre.graph.motionPhoto", motionPhotoKeys)...) default: metadataKeys = append(metadataKeys, key) } @@ -944,7 +951,7 @@ func requiresExplicitFetching(n *xml.Name) bool { } case net.NsOwncloud: switch n.Local { - case "favorite", "share-types", "checksums", "size", "tags", "audio", "location", "image", "photo": + case "favorite", "share-types", "checksums", "size", "tags", "audio", "location", "image", "photo", "motionPhoto": return true default: return false @@ -1292,6 +1299,7 @@ func mdToPropResponse(ctx context.Context, pf *XML, md *provider.ResourceInfo, p appendMetadataProp(k, "oc", "location", "libre.graph.location", locationKeys) appendMetadataProp(k, "oc", "image", "libre.graph.image", imageKeys) appendMetadataProp(k, "oc", "photo", "libre.graph.photo", photoKeys) + appendMetadataProp(k, "oc", "motionPhoto", "libre.graph.motionPhoto", motionPhotoKeys) } if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { @@ -1579,6 +1587,10 @@ func mdToPropResponse(ctx context.Context, pf *XML, md *provider.ResourceInfo, p if k := md.GetArbitraryMetadata().GetMetadata(); k != nil { appendMetadataProp(k, "oc", "photo", "libre.graph.photo", photoKeys) } + case "motionPhoto": + if k := md.GetArbitraryMetadata().GetMetadata(); k != nil { + appendMetadataProp(k, "oc", "motionPhoto", "libre.graph.motionPhoto", motionPhotoKeys) + } case "name": appendToOK(prop.Escaped("oc:name", md.Name)) case "shareid": From 3bed3d314a0a92c0c46909f7c21a9e19b8f7675f Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 7 Jul 2026 01:45:03 +0200 Subject: [PATCH 4/8] feat(search): only expose motionPhoto when the file has the video The facet was derived purely from XMP. Sharing a photo via photos.google.com strips the appended video but keeps the XMP, which produced an unplayable motionPhoto facet. Before exposing it, validate the trailing bytes: a ranged read at fileSize-videoSize must start with an MP4 ftyp box. Adds Retriever.RetrieveRange (cs3 + mock) and gates the facet in Tika.Extract. --- services/search/pkg/content/cs3.go | 72 +++++++++++++++-- .../search/pkg/content/mocks/retriever.go | 80 +++++++++++++++++++ services/search/pkg/content/retriever.go | 4 + services/search/pkg/content/tika.go | 41 ++++++++++ services/search/pkg/content/tika_test.go | 59 +++++++++++++- 5 files changed, 246 insertions(+), 10 deletions(-) diff --git a/services/search/pkg/content/cs3.go b/services/search/pkg/content/cs3.go index e21dcd94ec..647c88299b 100644 --- a/services/search/pkg/content/cs3.go +++ b/services/search/pkg/content/cs3.go @@ -33,26 +33,26 @@ func newCS3Retriever(gatewaySelector pool.Selectable[gateway.GatewayAPIClient], } } -// Retrieve downloads the file from a cs3 service -// The caller MUST make sure to close the returned ReadCloser -func (s cs3) Retrieve(ctx context.Context, rID *provider.ResourceId) (io.ReadCloser, error) { +// initiateDownload asks the gateway for a download endpoint and returns it +// together with the transfer token and the auth token from the context. +func (s cs3) initiateDownload(ctx context.Context, rID *provider.ResourceId) (endpoint, transferToken, authToken string, err error) { at, ok := contextGet(ctx, revactx.TokenHeader) if !ok { - return nil, fmt.Errorf("context without %s", revactx.TokenHeader) + return "", "", "", fmt.Errorf("context without %s", revactx.TokenHeader) } gatewayClient, err := s.gatewaySelector.Next() if err != nil { s.logger.Error().Err(err).Msg("could not get reva gatewayClient") - return nil, err + return "", "", "", err } res, err := gatewayClient.InitiateFileDownload(ctx, &provider.InitiateFileDownloadRequest{Ref: &provider.Reference{ResourceId: rID, Path: "."}}) if err != nil { - return nil, err + return "", "", "", err } if res.Status.Code != rpc.Code_CODE_OK { - return nil, fmt.Errorf("could not load resoure: %s", res.Status.Message) + return "", "", "", fmt.Errorf("could not load resoure: %s", res.Status.Message) } var ep, tt string @@ -66,6 +66,17 @@ func (s cs3) Retrieve(ctx context.Context, rID *provider.ResourceId) (io.ReadClo ep, tt = res.Protocols[0].DownloadEndpoint, res.Protocols[0].Token } + return ep, tt, at, nil +} + +// Retrieve downloads the file from a cs3 service +// The caller MUST make sure to close the returned ReadCloser +func (s cs3) Retrieve(ctx context.Context, rID *provider.ResourceId) (io.ReadCloser, error) { + ep, tt, at, err := s.initiateDownload(ctx, rID) + if err != nil { + return nil, err + } + req, err := http.NewRequest(http.MethodGet, ep, nil) if err != nil { return nil, err @@ -80,8 +91,55 @@ func (s cs3) Retrieve(ctx context.Context, rID *provider.ResourceId) (io.ReadClo } if cres.StatusCode != http.StatusOK { + _ = cres.Body.Close() return nil, fmt.Errorf("could not download resource. Request returned with statuscode %d ", cres.StatusCode) } return cres.Body, nil } + +// RetrieveRange downloads length bytes starting at offset from a cs3 service. +// The caller MUST make sure to close the returned ReadCloser. +// It relies on HTTP range support of the download endpoint. If the endpoint +// ignores the Range header and returns the full file (200 instead of 206), the +// leading offset bytes are discarded so the returned reader is always positioned +// at offset. +func (s cs3) RetrieveRange(ctx context.Context, rID *provider.ResourceId, offset, length int64) (io.ReadCloser, error) { + ep, tt, at, err := s.initiateDownload(ctx, rID) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, ep, nil) + if err != nil { + return nil, err + } + + req.Header.Set(revactx.TokenHeader, at) + req.Header.Set("X-Reva-Transfer", tt) + // A single range keeps the response a plain 206 with a Content-Range header, + // never a multipart/byteranges body. + req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+length-1)) + + cres, err := s.httpClient.Do(req) + if err != nil { + return nil, err + } + + switch cres.StatusCode { + case http.StatusPartialContent: + // Range honored: the body already starts at offset. + return cres.Body, nil + case http.StatusOK: + // Range ignored: the body is the whole file. Skip to offset so the + // caller always reads from the requested position. + if _, err := io.CopyN(io.Discard, cres.Body, offset); err != nil { + _ = cres.Body.Close() + return nil, fmt.Errorf("could not skip to offset %d: %w", offset, err) + } + return cres.Body, nil + default: + _ = cres.Body.Close() + return nil, fmt.Errorf("could not download range. Request returned with statuscode %d ", cres.StatusCode) + } +} diff --git a/services/search/pkg/content/mocks/retriever.go b/services/search/pkg/content/mocks/retriever.go index 5060f071a3..4fb4b5607e 100644 --- a/services/search/pkg/content/mocks/retriever.go +++ b/services/search/pkg/content/mocks/retriever.go @@ -106,3 +106,83 @@ func (_c *Retriever_Retrieve_Call) RunAndReturn(run func(ctx context.Context, rI _c.Call.Return(run) return _c } + +// RetrieveRange provides a mock function for the type Retriever +func (_mock *Retriever) RetrieveRange(ctx context.Context, rID *providerv1beta1.ResourceId, offset int64, length int64) (io.ReadCloser, error) { + ret := _mock.Called(ctx, rID, offset, length) + + if len(ret) == 0 { + panic("no return value specified for RetrieveRange") + } + + var r0 io.ReadCloser + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, int64, int64) (io.ReadCloser, error)); ok { + return returnFunc(ctx, rID, offset, length) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, int64, int64) io.ReadCloser); ok { + r0 = returnFunc(ctx, rID, offset, length) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(io.ReadCloser) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, int64, int64) error); ok { + r1 = returnFunc(ctx, rID, offset, length) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// Retriever_RetrieveRange_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RetrieveRange' +type Retriever_RetrieveRange_Call struct { + *mock.Call +} + +// RetrieveRange is a helper method to define mock.On call +// - ctx context.Context +// - rID *providerv1beta1.ResourceId +// - offset int64 +// - length int64 +func (_e *Retriever_Expecter) RetrieveRange(ctx interface{}, rID interface{}, offset interface{}, length interface{}) *Retriever_RetrieveRange_Call { + return &Retriever_RetrieveRange_Call{Call: _e.mock.On("RetrieveRange", ctx, rID, offset, length)} +} + +func (_c *Retriever_RetrieveRange_Call) Run(run func(ctx context.Context, rID *providerv1beta1.ResourceId, offset int64, length int64)) *Retriever_RetrieveRange_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *providerv1beta1.ResourceId + if args[1] != nil { + arg1 = args[1].(*providerv1beta1.ResourceId) + } + var arg2 int64 + if args[2] != nil { + arg2 = args[2].(int64) + } + var arg3 int64 + if args[3] != nil { + arg3 = args[3].(int64) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *Retriever_RetrieveRange_Call) Return(readCloser io.ReadCloser, err error) *Retriever_RetrieveRange_Call { + _c.Call.Return(readCloser, err) + return _c +} + +func (_c *Retriever_RetrieveRange_Call) RunAndReturn(run func(ctx context.Context, rID *providerv1beta1.ResourceId, offset int64, length int64) (io.ReadCloser, error)) *Retriever_RetrieveRange_Call { + _c.Call.Return(run) + return _c +} diff --git a/services/search/pkg/content/retriever.go b/services/search/pkg/content/retriever.go index bb64584d6f..586e42790f 100644 --- a/services/search/pkg/content/retriever.go +++ b/services/search/pkg/content/retriever.go @@ -12,6 +12,10 @@ import ( // It requests and then returns a resource from the underlying storage. type Retriever interface { Retrieve(ctx context.Context, rID *provider.ResourceId) (io.ReadCloser, error) + // RetrieveRange returns a reader positioned at offset for up to length bytes + // of the resource. Implementations must ensure the reader starts at offset + // even when the storage does not honor HTTP range requests. + RetrieveRange(ctx context.Context, rID *provider.ResourceId, offset, length int64) (io.ReadCloser, error) } func contextGet(ctx context.Context, k string) (string, bool) { diff --git a/services/search/pkg/content/tika.go b/services/search/pkg/content/tika.go index e329f4747a..30729909b8 100644 --- a/services/search/pkg/content/tika.go +++ b/services/search/pkg/content/tika.go @@ -121,6 +121,13 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, doc.MotionPhoto = motionPhotoFromXMP(xmpHead) } + // Only expose the facet if the file really contains the video the XMP claims. + // Sharing a photo via photos.google.com strips the appended video but keeps + // the XMP, which would otherwise surface an unplayable motion photo. + if doc.MotionPhoto != nil && !t.motionPhotoHasVideo(ctx, ri, doc.MotionPhoto.GetVideoSize()) { + doc.MotionPhoto = nil + } + if langCode, _ := t.tika.LanguageString(ctx, doc.Content); langCode != "" && t.CleanStopWords { doc.Content = CleanString(doc.Content, langCode) } @@ -284,6 +291,40 @@ func (t Tika) getMotionPhoto(meta map[string][]string) *libregraph.MotionPhoto { return motionPhoto } +// motionPhotoVideoSignatureLen is the number of trailing bytes we read to confirm +// an actual video is present. Enough to cover the ISO base media (MP4) box size +// and the "ftyp" box type at bytes [4:8]. +const motionPhotoVideoSignatureLen = 12 + +// looksLikeMP4 reports whether buf begins with an ISO base media (MP4/QuickTime) +// "ftyp" box, which Google Motion Photo and legacy MicroVideo clips start with. +func looksLikeMP4(buf []byte) bool { + return len(buf) >= 8 && string(buf[4:8]) == "ftyp" +} + +// motionPhotoHasVideo confirms that the file actually contains the embedded video +// the XMP advertises. A photos.google.com share strips the appended video but +// keeps the XMP, which would otherwise make us expose an unplayable facet. The +// video is appended at the end, so it starts at fileSize-videoSize; we read a few +// bytes there and require an MP4 signature. +func (t Tika) motionPhotoHasVideo(ctx context.Context, ri *provider.ResourceInfo, videoSize int64) bool { + size := int64(ri.GetSize()) + if videoSize <= 0 || videoSize >= size { + return false + } + + rc, err := t.RetrieveRange(ctx, ri.GetId(), size-videoSize, motionPhotoVideoSignatureLen) + if err != nil { + t.logger.Debug().Err(err).Interface("ResourceID", ri.GetId()).Msg("could not read motion photo video header, dropping facet") + return false + } + defer rc.Close() + + buf := make([]byte, motionPhotoVideoSignatureLen) + n, _ := io.ReadFull(rc, buf) + return looksLikeMP4(buf[:n]) +} + // motionPhotoVideoSize returns the embedded video's byte length. Legacy files // carry it as the MicroVideo offset (bytes from EOF to the video start, which // equals its length); current files expose it as the length of the Container diff --git a/services/search/pkg/content/tika_test.go b/services/search/pkg/content/tika_test.go index ca37fe2006..a74d4e24d1 100644 --- a/services/search/pkg/content/tika_test.go +++ b/services/search/pkg/content/tika_test.go @@ -1,6 +1,7 @@ package content_test import ( + "bytes" "context" "fmt" "io" @@ -21,6 +22,10 @@ import ( contentMocks "github.com/opencloud-eu/opencloud/services/search/pkg/content/mocks" ) +// validMP4Header is a minimal ISO base media "ftyp" box, used to stub the tail +// read that confirms a motion photo really contains a video. +var validMP4Header = []byte{0x00, 0x00, 0x00, 0x18, 'f', 't', 'y', 'p', 'm', 'p', '4', '2'} + var _ = Describe("Tika", func() { Describe("extract", func() { var ( @@ -66,6 +71,9 @@ var _ = Describe("Tika", func() { retriever := &contentMocks.Retriever{} retriever.On("Retrieve", mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader(body)), nil) + // By default the trailing bytes look like a real video so the motion + // photo facet survives validation; tests that need otherwise override this. + retriever.On("RetrieveRange", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(bytes.NewReader(validMP4Header)), nil) tika.Retriever = retriever }) @@ -226,7 +234,7 @@ var _ = Describe("Tika", func() { ]` doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{ Type: provider.ResourceType_RESOURCE_TYPE_FILE, - Size: 1, + Size: 2 * 1024 * 1024, }) Expect(err).ToNot(HaveOccurred()) @@ -250,7 +258,7 @@ var _ = Describe("Tika", func() { ]` doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{ Type: provider.ResourceType_RESOURCE_TYPE_FILE, - Size: 1, + Size: 2 * 1024 * 1024, }) Expect(err).ToNot(HaveOccurred()) @@ -291,11 +299,12 @@ var _ = Describe("Tika", func() { retriever := &contentMocks.Retriever{} retriever.On("Retrieve", mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader(xmp)), nil) + retriever.On("RetrieveRange", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(bytes.NewReader(validMP4Header)), nil) tika.Retriever = retriever doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{ Type: provider.ResourceType_RESOURCE_TYPE_FILE, - Size: 1, + Size: 2 * 1024 * 1024, MimeType: "image/jpeg", }) Expect(err).ToNot(HaveOccurred()) @@ -307,6 +316,50 @@ var _ = Describe("Tika", func() { Expect(motionPhoto.VideoSize).To(Equal(libregraph.PtrInt64(122562))) }) + It("drops a motion photo when the file is not larger than the video", func() { + // A photos.google.com share strips the appended video but keeps the XMP, + // leaving a file smaller than the advertised video. Caught without any IO. + fullResponse = `[ + { + "Camera:MotionPhotoVersion": "1", + "Camera:MotionPhotoPresentationTimestampUs": "500000", + "Container:Directory[2]/Item:Semantic": "MotionPhoto", + "Container:Directory[2]/Item:Length": "122562" + } + ]` + doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{ + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Size: 1000, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(doc.MotionPhoto).To(BeNil()) + }) + + It("drops a motion photo when the trailing bytes are not a video", func() { + // videoSize fits the file, but the bytes at the video offset are not an + // MP4 (the video was stripped and the file is a larger plain JPEG). + fullResponse = `[ + { + "Camera:MotionPhotoVersion": "1", + "Camera:MotionPhotoPresentationTimestampUs": "500000", + "Container:Directory[2]/Item:Semantic": "MotionPhoto", + "Container:Directory[2]/Item:Length": "122562" + } + ]` + + retriever := &contentMocks.Retriever{} + retriever.On("Retrieve", mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader(body)), nil) + retriever.On("RetrieveRange", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader("\xff\xd8\xff\xe0not a video")), nil) + tika.Retriever = retriever + + doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{ + Type: provider.ResourceType_RESOURCE_TYPE_FILE, + Size: 2 * 1024 * 1024, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(doc.MotionPhoto).To(BeNil()) + }) + It("removes stop words", func() { body = "body to test stop words!!! against almost everyone" language = "en" From ac056d9b32a9638c40f0aa88323e2f19c0655c91 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 7 Jul 2026 01:56:46 +0200 Subject: [PATCH 5/8] refactor(search): move motionPhoto extraction into its own file Keep tika.go focused on the generic Tika extraction. All Google Motion Photo specifics (XMP parsing, video-size resolution, the ftyp video presence check and helpers) move to motionphoto.go. Extract keeps only the orchestration calls. No behavior change. --- services/search/pkg/content/motionphoto.go | 205 +++++++++++++++++++++ services/search/pkg/content/tika.go | 191 ------------------- 2 files changed, 205 insertions(+), 191 deletions(-) create mode 100644 services/search/pkg/content/motionphoto.go diff --git a/services/search/pkg/content/motionphoto.go b/services/search/pkg/content/motionphoto.go new file mode 100644 index 0000000000..39aa02adb0 --- /dev/null +++ b/services/search/pkg/content/motionphoto.go @@ -0,0 +1,205 @@ +package content + +import ( + "bytes" + "context" + "encoding/xml" + "io" + "strconv" + "strings" + + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + libregraph "github.com/opencloud-eu/libre-graph-api-go" +) + +// motionPhotoXMPScanLimit is how many leading bytes of an image we buffer to look +// for the Google Motion Photo XMP packet when Tika does not surface it. +const motionPhotoXMPScanLimit = 256 * 1024 + +// motionPhotoVideoSignatureLen is the number of trailing bytes we read to confirm +// an actual video is present. Enough to cover the ISO base media (MP4) box size +// and the "ftyp" box type at bytes [4:8]. +const motionPhotoVideoSignatureLen = 12 + +// firstValue returns the first metadata value present among keys. +func firstValue(meta map[string][]string, keys ...string) (string, bool) { + for _, k := range keys { + if v, err := getFirstValue(meta, k); err == nil { + return v, true + } + } + return "", false +} + +// getMotionPhoto reads Google Motion Photo XMP, which Tika exposes under the +// canonical Camera/Container prefixes. It covers both the current MotionPhoto +// scheme and the legacy MicroVideo scheme. videoSize (the embedded video's byte +// length, needed to range-fetch it) is required, so the facet is dropped without it. +func (t Tika) getMotionPhoto(meta map[string][]string) *libregraph.MotionPhoto { + var motionPhoto *libregraph.MotionPhoto + initMotionPhoto := func() { + if motionPhoto == nil { + motionPhoto = libregraph.NewMotionPhoto() + } + } + + if v, ok := firstValue(meta, "Camera:MotionPhotoVersion", "Camera:MicroVideoVersion"); ok { + if i, err := strconv.ParseInt(v, 0, 32); err == nil { + initMotionPhoto() + motionPhoto.SetVersion(int32(i)) + } + } + + if v, ok := firstValue(meta, "Camera:MotionPhotoPresentationTimestampUs", "Camera:MicroVideoPresentationTimestampUs"); ok { + if i, err := strconv.ParseInt(v, 0, 64); err == nil { + initMotionPhoto() + motionPhoto.SetPresentationTimestampUs(i) + } + } + + if size, ok := motionPhotoVideoSize(meta); ok { + initMotionPhoto() + motionPhoto.SetVideoSize(size) + } + + if motionPhoto == nil || !motionPhoto.HasVideoSize() { + return nil + } + return motionPhoto +} + +// motionPhotoVideoSize returns the embedded video's byte length. Legacy files +// carry it as the MicroVideo offset (bytes from EOF to the video start, which +// equals its length); current files expose it as the length of the Container +// item whose semantic is "MotionPhoto". +func motionPhotoVideoSize(meta map[string][]string) (int64, bool) { + if v, ok := firstValue(meta, "Camera:MicroVideoOffset"); ok { + if i, err := strconv.ParseInt(v, 0, 64); err == nil { + return i, true + } + } + for k, vals := range meta { + if !strings.HasSuffix(k, "/Item:Semantic") || len(vals) == 0 || vals[0] != "MotionPhoto" { + continue + } + if v, ok := firstValue(meta, strings.TrimSuffix(k, "/Item:Semantic")+"/Item:Length"); ok { + if i, err := strconv.ParseInt(v, 0, 64); err == nil { + return i, true + } + } + } + return 0, false +} + +// motionPhotoFromXMP parses Google Motion Photo metadata directly from a file's +// XMP packet, covering the current MotionPhoto scheme and the legacy MicroVideo +// scheme. It matches on namespace URIs, so the declared prefix does not matter. +// videoSize is required, so the facet is dropped without it. +func motionPhotoFromXMP(data []byte) *libregraph.MotionPhoto { + const cameraNS = "http://ns.google.com/photos/1.0/camera/" + const itemNS = "http://ns.google.com/photos/1.0/container/item/" + + packet := extractXMPPacket(data) + if packet == nil { + return nil + } + + var motionPhoto *libregraph.MotionPhoto + initMotionPhoto := func() { + if motionPhoto == nil { + motionPhoto = libregraph.NewMotionPhoto() + } + } + + decoder := xml.NewDecoder(bytes.NewReader(packet)) + for { + token, err := decoder.Token() + if err != nil { + break + } + element, ok := token.(xml.StartElement) + if !ok { + continue + } + + var itemSemantic, itemLength string + for _, attr := range element.Attr { + switch { + case attr.Name.Space == cameraNS && (attr.Name.Local == "MotionPhotoVersion" || attr.Name.Local == "MicroVideoVersion"): + if i, err := strconv.ParseInt(attr.Value, 10, 32); err == nil { + initMotionPhoto() + motionPhoto.SetVersion(int32(i)) + } + case attr.Name.Space == cameraNS && (attr.Name.Local == "MotionPhotoPresentationTimestampUs" || attr.Name.Local == "MicroVideoPresentationTimestampUs"): + if i, err := strconv.ParseInt(attr.Value, 10, 64); err == nil { + initMotionPhoto() + motionPhoto.SetPresentationTimestampUs(i) + } + case attr.Name.Space == cameraNS && attr.Name.Local == "MicroVideoOffset": + if i, err := strconv.ParseInt(attr.Value, 10, 64); err == nil { + initMotionPhoto() + motionPhoto.SetVideoSize(i) + } + case attr.Name.Space == itemNS && attr.Name.Local == "Semantic": + itemSemantic = attr.Value + case attr.Name.Space == itemNS && attr.Name.Local == "Length": + itemLength = attr.Value + } + } + // current format: the video is the container item whose semantic is MotionPhoto. + if itemSemantic == "MotionPhoto" && itemLength != "" { + if i, err := strconv.ParseInt(itemLength, 10, 64); err == nil { + initMotionPhoto() + motionPhoto.SetVideoSize(i) + } + } + } + + if motionPhoto == nil || !motionPhoto.HasVideoSize() { + return nil + } + return motionPhoto +} + +// extractXMPPacket returns the element from a file's leading bytes, +// or nil when none is present. +func extractXMPPacket(data []byte) []byte { + start := bytes.Index(data, []byte("")) + if end < 0 { + return nil + } + return data[start : start+end+len("")] +} + +// looksLikeMP4 reports whether buf begins with an ISO base media (MP4/QuickTime) +// "ftyp" box, which Google Motion Photo and legacy MicroVideo clips start with. +func looksLikeMP4(buf []byte) bool { + return len(buf) >= 8 && string(buf[4:8]) == "ftyp" +} + +// motionPhotoHasVideo confirms that the file actually contains the embedded video +// the XMP advertises. A photos.google.com share strips the appended video but +// keeps the XMP, which would otherwise make us expose an unplayable facet. The +// video is appended at the end, so it starts at fileSize-videoSize; we read a few +// bytes there and require an MP4 signature. +func (t Tika) motionPhotoHasVideo(ctx context.Context, ri *provider.ResourceInfo, videoSize int64) bool { + size := int64(ri.GetSize()) + if videoSize <= 0 || videoSize >= size { + return false + } + + rc, err := t.RetrieveRange(ctx, ri.GetId(), size-videoSize, motionPhotoVideoSignatureLen) + if err != nil { + t.logger.Debug().Err(err).Interface("ResourceID", ri.GetId()).Msg("could not read motion photo video header, dropping facet") + return false + } + defer rc.Close() + + buf := make([]byte, motionPhotoVideoSignatureLen) + n, _ := io.ReadFull(rc, buf) + return looksLikeMP4(buf[:n]) +} diff --git a/services/search/pkg/content/tika.go b/services/search/pkg/content/tika.go index 30729909b8..78943b4d37 100644 --- a/services/search/pkg/content/tika.go +++ b/services/search/pkg/content/tika.go @@ -3,7 +3,6 @@ package content import ( "bytes" "context" - "encoding/xml" "fmt" "io" "math" @@ -254,196 +253,6 @@ func (t Tika) getPhoto(meta map[string][]string) *libregraph.Photo { return photo } -// getMotionPhoto reads Google Motion Photo XMP, which Tika exposes under the -// canonical Camera/Container prefixes. It covers both the current MotionPhoto -// scheme and the legacy MicroVideo scheme. videoSize (the embedded video's byte -// length, needed to range-fetch it) is required, so the facet is dropped without it. -func (t Tika) getMotionPhoto(meta map[string][]string) *libregraph.MotionPhoto { - var motionPhoto *libregraph.MotionPhoto - initMotionPhoto := func() { - if motionPhoto == nil { - motionPhoto = libregraph.NewMotionPhoto() - } - } - - if v, ok := firstValue(meta, "Camera:MotionPhotoVersion", "Camera:MicroVideoVersion"); ok { - if i, err := strconv.ParseInt(v, 0, 32); err == nil { - initMotionPhoto() - motionPhoto.SetVersion(int32(i)) - } - } - - if v, ok := firstValue(meta, "Camera:MotionPhotoPresentationTimestampUs", "Camera:MicroVideoPresentationTimestampUs"); ok { - if i, err := strconv.ParseInt(v, 0, 64); err == nil { - initMotionPhoto() - motionPhoto.SetPresentationTimestampUs(i) - } - } - - if size, ok := motionPhotoVideoSize(meta); ok { - initMotionPhoto() - motionPhoto.SetVideoSize(size) - } - - if motionPhoto == nil || !motionPhoto.HasVideoSize() { - return nil - } - return motionPhoto -} - -// motionPhotoVideoSignatureLen is the number of trailing bytes we read to confirm -// an actual video is present. Enough to cover the ISO base media (MP4) box size -// and the "ftyp" box type at bytes [4:8]. -const motionPhotoVideoSignatureLen = 12 - -// looksLikeMP4 reports whether buf begins with an ISO base media (MP4/QuickTime) -// "ftyp" box, which Google Motion Photo and legacy MicroVideo clips start with. -func looksLikeMP4(buf []byte) bool { - return len(buf) >= 8 && string(buf[4:8]) == "ftyp" -} - -// motionPhotoHasVideo confirms that the file actually contains the embedded video -// the XMP advertises. A photos.google.com share strips the appended video but -// keeps the XMP, which would otherwise make us expose an unplayable facet. The -// video is appended at the end, so it starts at fileSize-videoSize; we read a few -// bytes there and require an MP4 signature. -func (t Tika) motionPhotoHasVideo(ctx context.Context, ri *provider.ResourceInfo, videoSize int64) bool { - size := int64(ri.GetSize()) - if videoSize <= 0 || videoSize >= size { - return false - } - - rc, err := t.RetrieveRange(ctx, ri.GetId(), size-videoSize, motionPhotoVideoSignatureLen) - if err != nil { - t.logger.Debug().Err(err).Interface("ResourceID", ri.GetId()).Msg("could not read motion photo video header, dropping facet") - return false - } - defer rc.Close() - - buf := make([]byte, motionPhotoVideoSignatureLen) - n, _ := io.ReadFull(rc, buf) - return looksLikeMP4(buf[:n]) -} - -// motionPhotoVideoSize returns the embedded video's byte length. Legacy files -// carry it as the MicroVideo offset (bytes from EOF to the video start, which -// equals its length); current files expose it as the length of the Container -// item whose semantic is "MotionPhoto". -func motionPhotoVideoSize(meta map[string][]string) (int64, bool) { - if v, ok := firstValue(meta, "Camera:MicroVideoOffset"); ok { - if i, err := strconv.ParseInt(v, 0, 64); err == nil { - return i, true - } - } - for k, vals := range meta { - if !strings.HasSuffix(k, "/Item:Semantic") || len(vals) == 0 || vals[0] != "MotionPhoto" { - continue - } - if v, ok := firstValue(meta, strings.TrimSuffix(k, "/Item:Semantic")+"/Item:Length"); ok { - if i, err := strconv.ParseInt(v, 0, 64); err == nil { - return i, true - } - } - } - return 0, false -} - -const motionPhotoXMPScanLimit = 256 * 1024 - -// motionPhotoFromXMP parses Google Motion Photo metadata directly from a file's -// XMP packet, covering the current MotionPhoto scheme and the legacy MicroVideo -// scheme. It matches on namespace URIs, so the declared prefix does not matter. -// videoSize is required, so the facet is dropped without it. -func motionPhotoFromXMP(data []byte) *libregraph.MotionPhoto { - const cameraNS = "http://ns.google.com/photos/1.0/camera/" - const itemNS = "http://ns.google.com/photos/1.0/container/item/" - - packet := extractXMPPacket(data) - if packet == nil { - return nil - } - - var motionPhoto *libregraph.MotionPhoto - initMotionPhoto := func() { - if motionPhoto == nil { - motionPhoto = libregraph.NewMotionPhoto() - } - } - - decoder := xml.NewDecoder(bytes.NewReader(packet)) - for { - token, err := decoder.Token() - if err != nil { - break - } - element, ok := token.(xml.StartElement) - if !ok { - continue - } - - var itemSemantic, itemLength string - for _, attr := range element.Attr { - switch { - case attr.Name.Space == cameraNS && (attr.Name.Local == "MotionPhotoVersion" || attr.Name.Local == "MicroVideoVersion"): - if i, err := strconv.ParseInt(attr.Value, 10, 32); err == nil { - initMotionPhoto() - motionPhoto.SetVersion(int32(i)) - } - case attr.Name.Space == cameraNS && (attr.Name.Local == "MotionPhotoPresentationTimestampUs" || attr.Name.Local == "MicroVideoPresentationTimestampUs"): - if i, err := strconv.ParseInt(attr.Value, 10, 64); err == nil { - initMotionPhoto() - motionPhoto.SetPresentationTimestampUs(i) - } - case attr.Name.Space == cameraNS && attr.Name.Local == "MicroVideoOffset": - if i, err := strconv.ParseInt(attr.Value, 10, 64); err == nil { - initMotionPhoto() - motionPhoto.SetVideoSize(i) - } - case attr.Name.Space == itemNS && attr.Name.Local == "Semantic": - itemSemantic = attr.Value - case attr.Name.Space == itemNS && attr.Name.Local == "Length": - itemLength = attr.Value - } - } - // current format: the video is the container item whose semantic is MotionPhoto. - if itemSemantic == "MotionPhoto" && itemLength != "" { - if i, err := strconv.ParseInt(itemLength, 10, 64); err == nil { - initMotionPhoto() - motionPhoto.SetVideoSize(i) - } - } - } - - if motionPhoto == nil || !motionPhoto.HasVideoSize() { - return nil - } - return motionPhoto -} - -// extractXMPPacket returns the element from a file's leading bytes, -// or nil when none is present. -func extractXMPPacket(data []byte) []byte { - start := bytes.Index(data, []byte("")) - if end < 0 { - return nil - } - return data[start : start+end+len("")] -} - -// firstValue returns the first metadata value present among keys. -func firstValue(meta map[string][]string, keys ...string) (string, bool) { - for _, k := range keys { - if v, err := getFirstValue(meta, k); err == nil { - return v, true - } - } - return "", false -} - func (t Tika) getAudio(meta map[string][]string) *libregraph.Audio { var audio *libregraph.Audio initAudio := func() { From 33eaaea4999f336838789746ace953f902027e40 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 8 Jul 2026 12:30:27 +0200 Subject: [PATCH 6/8] refactor(search): drop the direct-XMP motion photo fallback The fallback read the Google Motion Photo XMP straight from the buffered file head because stock Tika dropped the vendor namespace. With the patched Tika (TIKA-4773, in 4.0.0-SNAPSHOT) exposing the Camera:/Container: keys, the Tika-key path (getMotionPhoto) covers it, so the fallback, extractXMPPacket and the head buffering are removed. The video-presence validation (motionPhotoHasVideo) is independent and stays. --- services/search/pkg/content/motionphoto.go | 90 ---------------------- services/search/pkg/content/tika.go | 21 +---- services/search/pkg/content/tika_test.go | 31 -------- 3 files changed, 1 insertion(+), 141 deletions(-) diff --git a/services/search/pkg/content/motionphoto.go b/services/search/pkg/content/motionphoto.go index 39aa02adb0..2f1bc34585 100644 --- a/services/search/pkg/content/motionphoto.go +++ b/services/search/pkg/content/motionphoto.go @@ -1,9 +1,7 @@ package content import ( - "bytes" "context" - "encoding/xml" "io" "strconv" "strings" @@ -12,10 +10,6 @@ import ( libregraph "github.com/opencloud-eu/libre-graph-api-go" ) -// motionPhotoXMPScanLimit is how many leading bytes of an image we buffer to look -// for the Google Motion Photo XMP packet when Tika does not surface it. -const motionPhotoXMPScanLimit = 256 * 1024 - // motionPhotoVideoSignatureLen is the number of trailing bytes we read to confirm // an actual video is present. Enough to cover the ISO base media (MP4) box size // and the "ftyp" box type at bytes [4:8]. @@ -91,90 +85,6 @@ func motionPhotoVideoSize(meta map[string][]string) (int64, bool) { return 0, false } -// motionPhotoFromXMP parses Google Motion Photo metadata directly from a file's -// XMP packet, covering the current MotionPhoto scheme and the legacy MicroVideo -// scheme. It matches on namespace URIs, so the declared prefix does not matter. -// videoSize is required, so the facet is dropped without it. -func motionPhotoFromXMP(data []byte) *libregraph.MotionPhoto { - const cameraNS = "http://ns.google.com/photos/1.0/camera/" - const itemNS = "http://ns.google.com/photos/1.0/container/item/" - - packet := extractXMPPacket(data) - if packet == nil { - return nil - } - - var motionPhoto *libregraph.MotionPhoto - initMotionPhoto := func() { - if motionPhoto == nil { - motionPhoto = libregraph.NewMotionPhoto() - } - } - - decoder := xml.NewDecoder(bytes.NewReader(packet)) - for { - token, err := decoder.Token() - if err != nil { - break - } - element, ok := token.(xml.StartElement) - if !ok { - continue - } - - var itemSemantic, itemLength string - for _, attr := range element.Attr { - switch { - case attr.Name.Space == cameraNS && (attr.Name.Local == "MotionPhotoVersion" || attr.Name.Local == "MicroVideoVersion"): - if i, err := strconv.ParseInt(attr.Value, 10, 32); err == nil { - initMotionPhoto() - motionPhoto.SetVersion(int32(i)) - } - case attr.Name.Space == cameraNS && (attr.Name.Local == "MotionPhotoPresentationTimestampUs" || attr.Name.Local == "MicroVideoPresentationTimestampUs"): - if i, err := strconv.ParseInt(attr.Value, 10, 64); err == nil { - initMotionPhoto() - motionPhoto.SetPresentationTimestampUs(i) - } - case attr.Name.Space == cameraNS && attr.Name.Local == "MicroVideoOffset": - if i, err := strconv.ParseInt(attr.Value, 10, 64); err == nil { - initMotionPhoto() - motionPhoto.SetVideoSize(i) - } - case attr.Name.Space == itemNS && attr.Name.Local == "Semantic": - itemSemantic = attr.Value - case attr.Name.Space == itemNS && attr.Name.Local == "Length": - itemLength = attr.Value - } - } - // current format: the video is the container item whose semantic is MotionPhoto. - if itemSemantic == "MotionPhoto" && itemLength != "" { - if i, err := strconv.ParseInt(itemLength, 10, 64); err == nil { - initMotionPhoto() - motionPhoto.SetVideoSize(i) - } - } - } - - if motionPhoto == nil || !motionPhoto.HasVideoSize() { - return nil - } - return motionPhoto -} - -// extractXMPPacket returns the element from a file's leading bytes, -// or nil when none is present. -func extractXMPPacket(data []byte) []byte { - start := bytes.Index(data, []byte("")) - if end < 0 { - return nil - } - return data[start : start+end+len("")] -} - // looksLikeMP4 reports whether buf begins with an ISO base media (MP4/QuickTime) // "ftyp" box, which Google Motion Photo and legacy MicroVideo clips start with. func looksLikeMP4(buf []byte) bool { diff --git a/services/search/pkg/content/tika.go b/services/search/pkg/content/tika.go index 78943b4d37..76ea37236f 100644 --- a/services/search/pkg/content/tika.go +++ b/services/search/pkg/content/tika.go @@ -1,10 +1,8 @@ package content import ( - "bytes" "context" "fmt" - "io" "math" "strconv" "strings" @@ -79,19 +77,7 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, } defer data.Close() - // Motion photos keep their metadata in vendor XMP that stock Tika does not - // expose (see getMotionPhoto). For images, buffer the head so we can read - // that XMP ourselves as a fallback; the rest still streams to Tika. - var xmpHead []byte - stream := io.Reader(data) - if strings.HasPrefix(ri.GetMimeType(), "image/") { - xmpHead = make([]byte, motionPhotoXMPScanLimit) - n, _ := io.ReadFull(data, xmpHead) - xmpHead = xmpHead[:n] - stream = io.MultiReader(bytes.NewReader(xmpHead), data) - } - - metas, err := t.tika.MetaRecursive(ctx, stream) + metas, err := t.tika.MetaRecursive(ctx, data) if err != nil { return doc, err } @@ -115,11 +101,6 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, } } - // Tika did not surface the motion photo XMP: read it from the buffered head. - if doc.MotionPhoto == nil && xmpHead != nil { - doc.MotionPhoto = motionPhotoFromXMP(xmpHead) - } - // Only expose the facet if the file really contains the video the XMP claims. // Sharing a photo via photos.google.com strips the appended video but keeps // the XMP, which would otherwise surface an unplayable motion photo. diff --git a/services/search/pkg/content/tika_test.go b/services/search/pkg/content/tika_test.go index a74d4e24d1..ae35643f31 100644 --- a/services/search/pkg/content/tika_test.go +++ b/services/search/pkg/content/tika_test.go @@ -285,37 +285,6 @@ var _ = Describe("Tika", func() { Expect(doc.MotionPhoto).To(BeNil()) }) - It("reads the motion photo XMP directly when Tika does not expose it", func() { - xmp := `` + - `` + - `` + - `` + - `` + - `` - - retriever := &contentMocks.Retriever{} - retriever.On("Retrieve", mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(strings.NewReader(xmp)), nil) - retriever.On("RetrieveRange", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(io.NopCloser(bytes.NewReader(validMP4Header)), nil) - tika.Retriever = retriever - - doc, err := tika.Extract(context.TODO(), &provider.ResourceInfo{ - Type: provider.ResourceType_RESOURCE_TYPE_FILE, - Size: 2 * 1024 * 1024, - MimeType: "image/jpeg", - }) - Expect(err).ToNot(HaveOccurred()) - - motionPhoto := doc.MotionPhoto - Expect(motionPhoto).ToNot(BeNil()) - Expect(motionPhoto.Version).To(Equal(libregraph.PtrInt32(1))) - Expect(motionPhoto.PresentationTimestampUs).To(Equal(libregraph.PtrInt64(500000))) - Expect(motionPhoto.VideoSize).To(Equal(libregraph.PtrInt64(122562))) - }) - It("drops a motion photo when the file is not larger than the video", func() { // A photos.google.com share strips the appended video but keeps the XMP, // leaving a file smaller than the advertised video. Caught without any IO. From 4c50212b2d43d86809a73a11c2dfc390cad8a8ef Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 8 Jul 2026 13:13:26 +0200 Subject: [PATCH 7/8] refactor(search): split Tika facet extractors into per-facet files Each facet extractor now lives in its own file next to the Tika core: tika_image.go, tika_location.go, tika_photo.go, tika_audio.go and tika_motion_photo.go (renamed from motionphoto.go). tika.go keeps only the Tika type, its constructor and the Extract orchestration. getFirstValue is now variadic (returns the first value present among the given keys), absorbing the motion-photo multi-key lookup, so there is one metadata helper instead of two. --- services/search/pkg/content/extractor.go | 17 +- services/search/pkg/content/tika.go | 200 ------------------ services/search/pkg/content/tika_audio.go | 85 ++++++++ services/search/pkg/content/tika_image.go | 32 +++ services/search/pkg/content/tika_location.go | 36 ++++ .../{motionphoto.go => tika_motion_photo.go} | 18 +- services/search/pkg/content/tika_photo.go | 74 +++++++ 7 files changed, 239 insertions(+), 223 deletions(-) create mode 100644 services/search/pkg/content/tika_audio.go create mode 100644 services/search/pkg/content/tika_image.go create mode 100644 services/search/pkg/content/tika_location.go rename services/search/pkg/content/{motionphoto.go => tika_motion_photo.go} (83%) create mode 100644 services/search/pkg/content/tika_photo.go diff --git a/services/search/pkg/content/extractor.go b/services/search/pkg/content/extractor.go index a91be6cb4e..581bb0c88d 100644 --- a/services/search/pkg/content/extractor.go +++ b/services/search/pkg/content/extractor.go @@ -13,19 +13,18 @@ type Extractor interface { Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, error) } -func getFirstValue(m map[string][]string, key string) (string, error) { +// getFirstValue returns the first metadata value present among keys, trying them +// in order. It errors when the map is nil or none of the keys holds a value. +func getFirstValue(m map[string][]string, keys ...string) (string, error) { if m == nil { return "", errors.New("undefined map") } - v, ok := m[key] - if !ok { - return "", fmt.Errorf("unknown key: %v", key) + for _, key := range keys { + if v, ok := m[key]; ok && len(v) > 0 { + return v[0], nil + } } - if len(m) == 0 { - return "", fmt.Errorf("no values for: %v", key) - } - - return v[0], nil + return "", fmt.Errorf("no value for keys: %v", keys) } diff --git a/services/search/pkg/content/tika.go b/services/search/pkg/content/tika.go index 76ea37236f..0b6055d3b6 100644 --- a/services/search/pkg/content/tika.go +++ b/services/search/pkg/content/tika.go @@ -3,15 +3,11 @@ package content import ( "context" "fmt" - "math" - "strconv" "strings" - "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/google/go-tika/tika" - libregraph "github.com/opencloud-eu/libre-graph-api-go" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" "github.com/opencloud-eu/opencloud/pkg/log" @@ -114,199 +110,3 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, return doc, nil } - -func (t Tika) getImage(meta map[string][]string) *libregraph.Image { - var image *libregraph.Image - initImage := func() { - if image == nil { - image = libregraph.NewImage() - } - } - - if v, err := getFirstValue(meta, "tiff:ImageWidth"); err == nil { - if i, err := strconv.ParseInt(v, 0, 32); err == nil { - initImage() - image.SetWidth(int32(i)) - } - } - - if v, err := getFirstValue(meta, "tiff:ImageLength"); err == nil { - if i, err := strconv.ParseInt(v, 0, 32); err == nil { - initImage() - image.SetHeight(int32(i)) - } - } - - return image -} - -func (t Tika) getLocation(meta map[string][]string) *libregraph.GeoCoordinates { - var location *libregraph.GeoCoordinates - initLocation := func() { - if location == nil { - location = libregraph.NewGeoCoordinates() - } - } - - // TODO: location.Altitute: transform the following data to … feet above sea level. - // "GPS:GPS Altitude": []string{"227.4 metres"}, - // "GPS:GPS Altitude Ref": []string{"Sea level"}, - - if v, err := getFirstValue(meta, "geo:lat"); err == nil { - if i, err := strconv.ParseFloat(v, 64); err == nil { - initLocation() - location.SetLatitude(i) - } - } - - if v, err := getFirstValue(meta, "geo:long"); err == nil { - if i, err := strconv.ParseFloat(v, 64); err == nil { - initLocation() - location.SetLongitude(i) - } - } - - return location -} - -func (t Tika) getPhoto(meta map[string][]string) *libregraph.Photo { - var photo *libregraph.Photo - initPhoto := func() { - if photo == nil { - photo = libregraph.NewPhoto() - } - } - - if v, err := getFirstValue(meta, "tiff:Make"); err == nil { - initPhoto() - photo.SetCameraMake(v) - } - - if v, err := getFirstValue(meta, "tiff:Model"); err == nil { - initPhoto() - photo.SetCameraModel(v) - } - - if v, err := getFirstValue(meta, "exif:FNumber"); err == nil { - if i, err := strconv.ParseFloat(v, 64); err == nil { - initPhoto() - photo.SetFNumber(i) - } - } - - if v, err := getFirstValue(meta, "exif:FocalLength"); err == nil { - if i, err := strconv.ParseFloat(v, 64); err == nil { - initPhoto() - photo.SetFocalLength(i) - } - } - - if v, err := getFirstValue(meta, "Base ISO"); err == nil { - if i, err := strconv.ParseInt(v, 0, 32); err == nil { - initPhoto() - photo.SetIso(int32(i)) - } - } - - if v, err := getFirstValue(meta, "tiff:Orientation"); err == nil { - if i, err := strconv.ParseInt(v, 0, 32); err == nil { - initPhoto() - photo.SetOrientation(int32(i)) - } - } - - if v, err := getFirstValue(meta, "exif:DateTimeOriginal"); err == nil { - layout := "2006-01-02T15:04:05" - if t, err := time.Parse(layout, v); err == nil { - initPhoto() - photo.SetTakenDateTime(t) - } - } - - if v, err := getFirstValue(meta, "exif:ExposureTime"); err == nil { - if i, err := strconv.ParseFloat(v, 64); err == nil { - initPhoto() - photo.SetExposureNumerator(1) - photo.SetExposureDenominator(math.Round(1 / i)) - } - } - - return photo -} - -func (t Tika) getAudio(meta map[string][]string) *libregraph.Audio { - var audio *libregraph.Audio - initAudio := func() { - if audio == nil { - audio = libregraph.NewAudio() - } - } - - if v, err := getFirstValue(meta, "xmpDM:album"); err == nil { - initAudio() - audio.SetAlbum(v) - } - - if v, err := getFirstValue(meta, "xmpDM:albumArtist"); err == nil { - initAudio() - audio.SetAlbumArtist(v) - } - - if v, err := getFirstValue(meta, "xmpDM:artist"); err == nil { - initAudio() - audio.SetArtist(v) - } - - // TODO: audio.Bitrate: not provided by tika - // TODO: audio.Composers: not provided by tika - // TODO: audio.Copyright: not provided by tika for audio files? - - if v, err := getFirstValue(meta, "xmpDM:discNumber"); err == nil { - if i, err := strconv.ParseInt(v, 10, 32); err == nil { - initAudio() - audio.SetDisc(int32(i)) - } - - } - - // TODO: audio.DiscCount: not provided by tika - - if v, err := getFirstValue(meta, "xmpDM:duration"); err == nil { - // Tika emits fractional seconds. - if f, err := strconv.ParseFloat(v, 64); err == nil { - initAudio() - audio.SetDuration(int64(math.Round(f * 1000))) - } - } - - if v, err := getFirstValue(meta, "xmpDM:genre"); err == nil { - initAudio() - audio.SetGenre(v) - } - - // TODO: audio.HasDrm: not provided by tika - // TODO: audio.IsVariableBitrate: not provided by tika - - if v, err := getFirstValue(meta, "dc:title"); err == nil { - initAudio() - audio.SetTitle(v) - } - - if v, err := getFirstValue(meta, "xmpDM:trackNumber"); err == nil { - if i, err := strconv.ParseInt(v, 10, 32); err == nil { - initAudio() - audio.SetTrack(int32(i)) - } - } - - // TODO: audio.TrackCount: not provided by tika - - if v, err := getFirstValue(meta, "xmpDM:releaseDate"); err == nil { - if i, err := strconv.ParseInt(v, 10, 32); err == nil { - initAudio() - audio.SetYear(int32(i)) - } - } - - return audio -} diff --git a/services/search/pkg/content/tika_audio.go b/services/search/pkg/content/tika_audio.go new file mode 100644 index 0000000000..7a89e3e225 --- /dev/null +++ b/services/search/pkg/content/tika_audio.go @@ -0,0 +1,85 @@ +package content + +import ( + "math" + "strconv" + + libregraph "github.com/opencloud-eu/libre-graph-api-go" +) + +func (t Tika) getAudio(meta map[string][]string) *libregraph.Audio { + var audio *libregraph.Audio + initAudio := func() { + if audio == nil { + audio = libregraph.NewAudio() + } + } + + if v, err := getFirstValue(meta, "xmpDM:album"); err == nil { + initAudio() + audio.SetAlbum(v) + } + + if v, err := getFirstValue(meta, "xmpDM:albumArtist"); err == nil { + initAudio() + audio.SetAlbumArtist(v) + } + + if v, err := getFirstValue(meta, "xmpDM:artist"); err == nil { + initAudio() + audio.SetArtist(v) + } + + // TODO: audio.Bitrate: not provided by tika + // TODO: audio.Composers: not provided by tika + // TODO: audio.Copyright: not provided by tika for audio files? + + if v, err := getFirstValue(meta, "xmpDM:discNumber"); err == nil { + if i, err := strconv.ParseInt(v, 10, 32); err == nil { + initAudio() + audio.SetDisc(int32(i)) + } + + } + + // TODO: audio.DiscCount: not provided by tika + + if v, err := getFirstValue(meta, "xmpDM:duration"); err == nil { + // Tika emits fractional seconds. + if f, err := strconv.ParseFloat(v, 64); err == nil { + initAudio() + audio.SetDuration(int64(math.Round(f * 1000))) + } + } + + if v, err := getFirstValue(meta, "xmpDM:genre"); err == nil { + initAudio() + audio.SetGenre(v) + } + + // TODO: audio.HasDrm: not provided by tika + // TODO: audio.IsVariableBitrate: not provided by tika + + if v, err := getFirstValue(meta, "dc:title"); err == nil { + initAudio() + audio.SetTitle(v) + } + + if v, err := getFirstValue(meta, "xmpDM:trackNumber"); err == nil { + if i, err := strconv.ParseInt(v, 10, 32); err == nil { + initAudio() + audio.SetTrack(int32(i)) + } + } + + // TODO: audio.TrackCount: not provided by tika + + if v, err := getFirstValue(meta, "xmpDM:releaseDate"); err == nil { + if i, err := strconv.ParseInt(v, 10, 32); err == nil { + initAudio() + audio.SetYear(int32(i)) + } + } + + return audio +} diff --git a/services/search/pkg/content/tika_image.go b/services/search/pkg/content/tika_image.go new file mode 100644 index 0000000000..b5ce2a4037 --- /dev/null +++ b/services/search/pkg/content/tika_image.go @@ -0,0 +1,32 @@ +package content + +import ( + "strconv" + + libregraph "github.com/opencloud-eu/libre-graph-api-go" +) + +func (t Tika) getImage(meta map[string][]string) *libregraph.Image { + var image *libregraph.Image + initImage := func() { + if image == nil { + image = libregraph.NewImage() + } + } + + if v, err := getFirstValue(meta, "tiff:ImageWidth"); err == nil { + if i, err := strconv.ParseInt(v, 0, 32); err == nil { + initImage() + image.SetWidth(int32(i)) + } + } + + if v, err := getFirstValue(meta, "tiff:ImageLength"); err == nil { + if i, err := strconv.ParseInt(v, 0, 32); err == nil { + initImage() + image.SetHeight(int32(i)) + } + } + + return image +} diff --git a/services/search/pkg/content/tika_location.go b/services/search/pkg/content/tika_location.go new file mode 100644 index 0000000000..2334c53afe --- /dev/null +++ b/services/search/pkg/content/tika_location.go @@ -0,0 +1,36 @@ +package content + +import ( + "strconv" + + libregraph "github.com/opencloud-eu/libre-graph-api-go" +) + +func (t Tika) getLocation(meta map[string][]string) *libregraph.GeoCoordinates { + var location *libregraph.GeoCoordinates + initLocation := func() { + if location == nil { + location = libregraph.NewGeoCoordinates() + } + } + + // TODO: location.Altitute: transform the following data to … feet above sea level. + // "GPS:GPS Altitude": []string{"227.4 metres"}, + // "GPS:GPS Altitude Ref": []string{"Sea level"}, + + if v, err := getFirstValue(meta, "geo:lat"); err == nil { + if i, err := strconv.ParseFloat(v, 64); err == nil { + initLocation() + location.SetLatitude(i) + } + } + + if v, err := getFirstValue(meta, "geo:long"); err == nil { + if i, err := strconv.ParseFloat(v, 64); err == nil { + initLocation() + location.SetLongitude(i) + } + } + + return location +} diff --git a/services/search/pkg/content/motionphoto.go b/services/search/pkg/content/tika_motion_photo.go similarity index 83% rename from services/search/pkg/content/motionphoto.go rename to services/search/pkg/content/tika_motion_photo.go index 2f1bc34585..a913ce0c31 100644 --- a/services/search/pkg/content/motionphoto.go +++ b/services/search/pkg/content/tika_motion_photo.go @@ -15,16 +15,6 @@ import ( // and the "ftyp" box type at bytes [4:8]. const motionPhotoVideoSignatureLen = 12 -// firstValue returns the first metadata value present among keys. -func firstValue(meta map[string][]string, keys ...string) (string, bool) { - for _, k := range keys { - if v, err := getFirstValue(meta, k); err == nil { - return v, true - } - } - return "", false -} - // getMotionPhoto reads Google Motion Photo XMP, which Tika exposes under the // canonical Camera/Container prefixes. It covers both the current MotionPhoto // scheme and the legacy MicroVideo scheme. videoSize (the embedded video's byte @@ -37,14 +27,14 @@ func (t Tika) getMotionPhoto(meta map[string][]string) *libregraph.MotionPhoto { } } - if v, ok := firstValue(meta, "Camera:MotionPhotoVersion", "Camera:MicroVideoVersion"); ok { + if v, err := getFirstValue(meta, "Camera:MotionPhotoVersion", "Camera:MicroVideoVersion"); err == nil { if i, err := strconv.ParseInt(v, 0, 32); err == nil { initMotionPhoto() motionPhoto.SetVersion(int32(i)) } } - if v, ok := firstValue(meta, "Camera:MotionPhotoPresentationTimestampUs", "Camera:MicroVideoPresentationTimestampUs"); ok { + if v, err := getFirstValue(meta, "Camera:MotionPhotoPresentationTimestampUs", "Camera:MicroVideoPresentationTimestampUs"); err == nil { if i, err := strconv.ParseInt(v, 0, 64); err == nil { initMotionPhoto() motionPhoto.SetPresentationTimestampUs(i) @@ -67,7 +57,7 @@ func (t Tika) getMotionPhoto(meta map[string][]string) *libregraph.MotionPhoto { // equals its length); current files expose it as the length of the Container // item whose semantic is "MotionPhoto". func motionPhotoVideoSize(meta map[string][]string) (int64, bool) { - if v, ok := firstValue(meta, "Camera:MicroVideoOffset"); ok { + if v, err := getFirstValue(meta, "Camera:MicroVideoOffset"); err == nil { if i, err := strconv.ParseInt(v, 0, 64); err == nil { return i, true } @@ -76,7 +66,7 @@ func motionPhotoVideoSize(meta map[string][]string) (int64, bool) { if !strings.HasSuffix(k, "/Item:Semantic") || len(vals) == 0 || vals[0] != "MotionPhoto" { continue } - if v, ok := firstValue(meta, strings.TrimSuffix(k, "/Item:Semantic")+"/Item:Length"); ok { + if v, err := getFirstValue(meta, strings.TrimSuffix(k, "/Item:Semantic")+"/Item:Length"); err == nil { if i, err := strconv.ParseInt(v, 0, 64); err == nil { return i, true } diff --git a/services/search/pkg/content/tika_photo.go b/services/search/pkg/content/tika_photo.go new file mode 100644 index 0000000000..a833df3b1d --- /dev/null +++ b/services/search/pkg/content/tika_photo.go @@ -0,0 +1,74 @@ +package content + +import ( + "math" + "strconv" + "time" + + libregraph "github.com/opencloud-eu/libre-graph-api-go" +) + +func (t Tika) getPhoto(meta map[string][]string) *libregraph.Photo { + var photo *libregraph.Photo + initPhoto := func() { + if photo == nil { + photo = libregraph.NewPhoto() + } + } + + if v, err := getFirstValue(meta, "tiff:Make"); err == nil { + initPhoto() + photo.SetCameraMake(v) + } + + if v, err := getFirstValue(meta, "tiff:Model"); err == nil { + initPhoto() + photo.SetCameraModel(v) + } + + if v, err := getFirstValue(meta, "exif:FNumber"); err == nil { + if i, err := strconv.ParseFloat(v, 64); err == nil { + initPhoto() + photo.SetFNumber(i) + } + } + + if v, err := getFirstValue(meta, "exif:FocalLength"); err == nil { + if i, err := strconv.ParseFloat(v, 64); err == nil { + initPhoto() + photo.SetFocalLength(i) + } + } + + if v, err := getFirstValue(meta, "Base ISO"); err == nil { + if i, err := strconv.ParseInt(v, 0, 32); err == nil { + initPhoto() + photo.SetIso(int32(i)) + } + } + + if v, err := getFirstValue(meta, "tiff:Orientation"); err == nil { + if i, err := strconv.ParseInt(v, 0, 32); err == nil { + initPhoto() + photo.SetOrientation(int32(i)) + } + } + + if v, err := getFirstValue(meta, "exif:DateTimeOriginal"); err == nil { + layout := "2006-01-02T15:04:05" + if t, err := time.Parse(layout, v); err == nil { + initPhoto() + photo.SetTakenDateTime(t) + } + } + + if v, err := getFirstValue(meta, "exif:ExposureTime"); err == nil { + if i, err := strconv.ParseFloat(v, 64); err == nil { + initPhoto() + photo.SetExposureNumerator(1) + photo.SetExposureDenominator(math.Round(1 / i)) + } + } + + return photo +} From ded8ec8af0369eb4604dd93585d09a4e85355ce7 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 8 Jul 2026 14:51:04 +0200 Subject: [PATCH 8/8] refactor(search): keep motion photo extraction next to its validation In Extract, getMotionPhoto now runs last in the per-metadata loop so it sits right before the post-loop video-presence check, instead of being separated by the audio handling. Drop the redundant comment on the check (motionPhotoHasVideo and its own doc comment already say what and why). --- services/search/pkg/content/tika.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/services/search/pkg/content/tika.go b/services/search/pkg/content/tika.go index 0b6055d3b6..9094ea4cf6 100644 --- a/services/search/pkg/content/tika.go +++ b/services/search/pkg/content/tika.go @@ -90,16 +90,14 @@ func (t Tika) Extract(ctx context.Context, ri *provider.ResourceInfo) (Document, doc.Location = t.getLocation(meta) doc.Image = t.getImage(meta) doc.Photo = t.getPhoto(meta) - doc.MotionPhoto = t.getMotionPhoto(meta) if contentType, err := getFirstValue(meta, "Content-Type"); err == nil && strings.HasPrefix(contentType, "audio/") { doc.Audio = t.getAudio(meta) } + + doc.MotionPhoto = t.getMotionPhoto(meta) } - // Only expose the facet if the file really contains the video the XMP claims. - // Sharing a photo via photos.google.com strips the appended video but keeps - // the XMP, which would otherwise surface an unplayable motion photo. if doc.MotionPhoto != nil && !t.motionPhotoHasVideo(ctx, ri, doc.MotionPhoto.GetVideoSize()) { doc.MotionPhoto = nil }