Compare commits

...
Author SHA1 Message Date
Dominik Schmidt be8cd461b4 feat(graph): add the driveItem versions API
Implements the MS Graph driveItemVersion operations on files:

- GET  .../items/{id}/versions lists the versions, newest first
- GET  .../versions/{version-id} returns one version, "current" describes the file itself
- GET  .../versions/{version-id}/content redirects to a signed download url for the version
- POST .../versions/{version-id}/restoreVersion makes the version the current content

The operations map onto ListFileVersions and RestoreFileVersion of the
gateway. Version downloads point at the WebDAV meta endpoint, signed the
same way as the driveItem download url. The download url is only added
when requested via $select.

The driveItemVersion models are vendored from the regenerated client of
opencloud-eu/libre-graph-api#73 until that spec change is merged and the
dependency can be bumped.
2026-09-07 13:00:01 +02:00
Dominik Schmidt 5807eea186 feat(graph): expose @microsoft.graph.downloadUrl and /content on driveItems
Populates @microsoft.graph.downloadUrl on file driveItems when requested
via $select and implements GET .../items/{item-id}/content as a 302 to
the same URL: a by-id WebDAV URL signed with OC_URL_SIGNING_SECRET,
verified by the proxy, valid for 30 minutes. Folders answer 404 on
/content and never carry the annotation.

The annotation is available on the driveItem stat, the children and
root children listings and the share jail item endpoint.
2026-09-07 12:59:50 +02:00
Dominik Schmidt 0459149e0c style(graph): gofmt import order 2026-09-07 12:59:08 +02:00
Dominik Schmidt 13da9331f2 refactor(graph): use BaseGraphService.publicBaseURL consistently
drive.WebUrl, driveItem.WebUrl and the public share link WebUrl all
derive from the same config value (graph.spaces.webdav_base), but only
driveItem.WebUrl used the pre-parsed BaseGraphService.publicBaseURL.
The other two re-parsed the config on every call.

Add a webURLForResource method on BaseGraphService for the /f/<id> URLs
(used twice, with a *string return matching the libregraph DriveItem
field shape), and inline g.publicBaseURL for the single /s/<token>
share-link case. Convert cs3ResourceToDriveItem and formatDriveItems
from free functions into BaseGraphService methods so they pick up
logger and publicBaseURL from the receiver. This also aligns them with
the surrounding code: BaseGraphService already exposes ~15 similar
methods, so the two free functions were the odd ones out.

Net: all three WebUrls are now constructed from a single pre-parsed
URL, and the (g.logger, g.publicBaseURL) plumbing at 7 call sites
disappears.
2026-09-07 12:59:08 +02:00
23 changed files with 1653 additions and 49 deletions

No files matched your search

+4
View File
@@ -191,6 +191,10 @@ To specialize `graph` service instances in order to scale them independently, it
* `GRAPH_HTTP_DISABLE`: when set to `true`, the service does not listen on HTTP and only consumes events (defaults to `false`)
* `GRAPH_EVENTS_DISABLE_CONSUMER`: when set to `true`, the service does not consome events and only listens on HTTP (defaults to `false`)
## Download URLs
`GET /drives/{drive-id}/items/{item-id}/content` and the `@microsoft.graph.downloadUrl` annotation (requested via `$select`) hand out WebDAV URLs signed with `OC_URL_SIGNING_SECRET`. The proxy verifies the signature, so the URLs work without an `Authorization` header. They expire after 30 minutes. Without the secret the annotation is omitted and the `content` endpoint answers with an error.
## Metrics
Metrics are disabled by default, and must be enabled using the following environment variables:
@@ -6,6 +6,7 @@ package mocks
import (
"context"
"net/http"
"github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
"github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
@@ -175,3 +176,49 @@ func (_c *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call) RunAndReturn(run
_c.Call.Return(run)
return _c
}
// SetDriveItemsDownloadURL provides a mock function for the type BaseGraphProvider
func (_mock *BaseGraphProvider) SetDriveItemsDownloadURL(r *http.Request, items []libregraph.DriveItem) {
_mock.Called(r, items)
return
}
// BaseGraphProvider_SetDriveItemsDownloadURL_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetDriveItemsDownloadURL'
type BaseGraphProvider_SetDriveItemsDownloadURL_Call struct {
*mock.Call
}
// SetDriveItemsDownloadURL is a helper method to define mock.On call
// - r *http.Request
// - items []libregraph.DriveItem
func (_e *BaseGraphProvider_Expecter) SetDriveItemsDownloadURL(r interface{}, items interface{}) *BaseGraphProvider_SetDriveItemsDownloadURL_Call {
return &BaseGraphProvider_SetDriveItemsDownloadURL_Call{Call: _e.mock.On("SetDriveItemsDownloadURL", r, items)}
}
func (_c *BaseGraphProvider_SetDriveItemsDownloadURL_Call) Run(run func(r *http.Request, items []libregraph.DriveItem)) *BaseGraphProvider_SetDriveItemsDownloadURL_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *http.Request
if args[0] != nil {
arg0 = args[0].(*http.Request)
}
var arg1 []libregraph.DriveItem
if args[1] != nil {
arg1 = args[1].([]libregraph.DriveItem)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *BaseGraphProvider_SetDriveItemsDownloadURL_Call) Return() *BaseGraphProvider_SetDriveItemsDownloadURL_Call {
_c.Call.Return()
return _c
}
func (_c *BaseGraphProvider_SetDriveItemsDownloadURL_Call) RunAndReturn(run func(r *http.Request, items []libregraph.DriveItem)) *BaseGraphProvider_SetDriveItemsDownloadURL_Call {
_c.Run(run)
return _c
}
@@ -404,7 +404,7 @@ func (s DriveItemPermissionsService) ListPermissions(ctx context.Context, itemID
driveItems := make(driveItemsByResourceID, 1)
// we can use the statResponse to build the drive item before fetching the shares
item, err := cs3ResourceToDriveItem(s.logger, s.publicBaseURL, statResponse.GetInfo())
item, err := s.cs3ResourceToDriveItem(statResponse.GetInfo())
if err != nil {
return collectionOfPermissions, err
}
@@ -13,11 +13,11 @@ import (
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
"github.com/opencloud-eu/opencloud/services/graph/pkg/linktype"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
func (s DriveItemPermissionsService) CreateLink(ctx context.Context, driveItemID *storageprovider.ResourceId, createLink libregraph.DriveItemCreateLink) (libregraph.Permission, error) {
@@ -416,6 +416,9 @@ func (api DrivesDriveItemApi) GetDriveItem(w http.ResponseWriter, r *http.Reques
ErrDriveItemConversion.Render(w, r)
return
}
if driveItemPropertySelected(r, _selectDownloadURL) {
api.baseGraphService.SetDriveItemsDownloadURL(r, driveItems)
}
render.Status(r, http.StatusOK)
render.JSON(w, r, driveItems[0])
@@ -980,6 +980,27 @@ var _ = Describe("DrivesDriveItemApi", func() {
jsonData := gjson.Get(w.Body.String(), "error")
Expect(jsonData.Get("code").String() + ": " + jsonData.Get("message").String()).To(Equal(svc.ErrDriveItemConversion.Error()))
})
It("adds the download url when selected via $select", func() {
baseGraphProvider.
EXPECT().
CS3ReceivedSharesToDriveItems(mock.Anything, mock.Anything).
Return([]libregraph.DriveItem{{}}, nil).
Once()
baseGraphProvider.
EXPECT().
SetDriveItemsDownloadURL(mock.Anything, mock.Anything).
Return().
Once()
r = httptest.NewRequest(http.MethodGet, "/?$select=@microsoft.graph.downloadUrl", nil).
WithContext(
context.WithValue(context.Background(), chi.RouteCtxKey, rCTX),
)
drivesDriveItemApi.GetDriveItem(w, r)
Expect(w.Code).To(Equal(http.StatusOK))
})
})
It("successfully returns the share", func() {
+1 -1
View File
@@ -6,9 +6,9 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
)
// ListApplications implements the Service interface.
@@ -6,13 +6,13 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
settingsmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/settings/v0"
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/utils"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
merrors "go-micro.dev/v4/errors"
)
+15 -9
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"time"
@@ -23,6 +24,7 @@ import (
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/share"
"github.com/opencloud-eu/reva/v2/pkg/signedurl"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
@@ -39,6 +41,7 @@ import (
type BaseGraphProvider interface {
CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaboration.ReceivedShare) ([]libregraph.DriveItem, error)
CS3ReceivedOCMSharesToDriveItems(ctx context.Context, receivedOCMShares []*ocm.ReceivedShare) ([]libregraph.DriveItem, error)
SetDriveItemsDownloadURL(r *http.Request, items []libregraph.DriveItem)
}
// BaseGraphService implements a couple of helper functions that are
@@ -50,6 +53,16 @@ type BaseGraphService struct {
config *config.Config
availableRoles []*libregraph.UnifiedRoleDefinition
publicBaseURL *url.URL
downloadSigner signedurl.Signer
}
// webURLForResource returns the public web URL pointing at the given resource
// (e.g. https://cloud.example.com/f/<resource-id>), using the pre-parsed
// publicBaseURL held by the service.
func (g BaseGraphService) webURLForResource(rid *storageprovider.ResourceId) *string {
u := *g.publicBaseURL
u.Path = path.Join(u.Path, "f", storagespace.FormatResourceID(rid))
return libregraph.PtrString(u.String())
}
func (g BaseGraphService) getDriveItem(ctx context.Context, ref *storageprovider.Reference) (*libregraph.DriveItem, error) {
@@ -66,7 +79,7 @@ func (g BaseGraphService) getDriveItem(ctx context.Context, ref *storageprovider
refStr, _ := storagespace.FormatReference(ref)
return nil, fmt.Errorf("could not stat %s: %s", refStr, res.GetStatus().GetMessage())
}
return cs3ResourceToDriveItem(g.logger, g.publicBaseURL, res.GetInfo())
return g.cs3ResourceToDriveItem(res.GetInfo())
}
func (g BaseGraphService) CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaboration.ReceivedShare) ([]libregraph.DriveItem, error) {
@@ -217,14 +230,6 @@ func (g BaseGraphService) cs3SpacePermissionsToLibreGraph(ctx context.Context, s
}
func (g BaseGraphService) libreGraphPermissionFromCS3PublicShare(createdLink *link.PublicShare) (*libregraph.Permission, error) {
webURL, err := url.Parse(g.config.Spaces.WebDavBase)
if err != nil {
g.logger.Error().
Err(err).
Str("url", g.config.Spaces.WebDavBase).
Msg("failed to parse webURL base url")
return nil, err
}
lt, actions := linktype.SharingLinkTypeFromCS3Permissions(createdLink.GetPermissions())
perm := libregraph.NewPermission()
perm.Id = libregraph.PtrString(createdLink.GetId().GetOpaqueId())
@@ -235,6 +240,7 @@ func (g BaseGraphService) libreGraphPermissionFromCS3PublicShare(createdLink *li
LibreGraphQuickLink: libregraph.PtrBool(createdLink.GetQuicklink()),
}
perm.LibreGraphPermissionsActions = actions
webURL := *g.publicBaseURL
webURL.Path = path.Join(webURL.Path, "s", createdLink.GetToken())
perm.Link.SetWebUrl(webURL.String())
@@ -0,0 +1,142 @@
package svc
import (
"errors"
"net/http"
"path"
"time"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
)
const downloadURLTTL = 30 * time.Minute
// ErrDownloadURLSigningNotConfigured is returned when no url signing secret is configured
var ErrDownloadURLSigningNotConfigured = errors.New("download url signing is not configured")
// GetDriveItemContent redirects to a signed download url for a file
func (g Graph) GetDriveItemContent(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
driveID, err := parseIDParam(r, "driveID")
if err != nil {
errorcode.RenderError(w, r, err)
return
}
itemID, err := parseIDParam(r, "itemID")
if err != nil {
errorcode.RenderError(w, r, err)
return
}
if driveID.GetStorageId() != itemID.GetStorageId() || driveID.GetSpaceId() != itemID.GetSpaceId() {
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item does not exist")
return
}
user, ok := revactx.ContextGetUser(ctx)
if !ok {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "user not in context")
return
}
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
stat, err := gatewayClient.Stat(ctx, &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &itemID}})
switch {
case err != nil:
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_OK:
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_NOT_FOUND:
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, stat.GetStatus().GetMessage())
return
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_PERMISSION_DENIED:
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, stat.GetStatus().GetMessage())
return
case stat.GetStatus().GetCode() == cs3rpc.Code_CODE_UNAUTHENTICATED:
errorcode.Unauthenticated.Render(w, r, http.StatusUnauthorized, stat.GetStatus().GetMessage())
return
default:
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, stat.GetStatus().GetMessage())
return
}
if stat.GetInfo().GetType() != storageprovider.ResourceType_RESOURCE_TYPE_FILE {
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item is not a file")
return
}
downloadURL, err := g.signedDownloadURL(&itemID, user.GetId().GetOpaqueId())
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
http.Redirect(w, r, downloadURL, http.StatusFound)
}
// SetDriveItemsDownloadURL adds a signed download url to every file in items when requested via $select
func (g BaseGraphService) SetDriveItemsDownloadURL(r *http.Request, items []libregraph.DriveItem) {
if !g.downloadURLRequested(r) {
return
}
user, ok := revactx.ContextGetUser(r.Context())
if !ok {
return
}
for i := range items {
g.signDriveItemDownloadURL(&items[i], user.GetId().GetOpaqueId())
}
}
func (g BaseGraphService) setDriveItemDownloadURL(r *http.Request, item *libregraph.DriveItem) {
if !g.downloadURLRequested(r) {
return
}
user, ok := revactx.ContextGetUser(r.Context())
if !ok {
return
}
g.signDriveItemDownloadURL(item, user.GetId().GetOpaqueId())
}
func (g BaseGraphService) signDriveItemDownloadURL(item *libregraph.DriveItem, userID string) {
if item.File == nil {
return
}
id, err := storagespace.ParseID(item.GetId())
if err != nil {
g.logger.Debug().Err(err).Str("id", item.GetId()).Msg("could not parse drive item id for the download url")
return
}
u, err := g.signedDownloadURL(&id, userID)
if err != nil {
g.logger.Debug().Err(err).Str("id", item.GetId()).Msg("could not sign the download url")
return
}
item.MicrosoftGraphDownloadUrl = &u
}
func (g BaseGraphService) signedDownloadURL(id *storageprovider.ResourceId, userID string) (string, error) {
if g.downloadSigner == nil {
return "", ErrDownloadURLSigningNotConfigured
}
base, err := g.getWebDavBaseURL()
if err != nil {
return "", err
}
base.Path = path.Join(base.Path, storagespace.FormatResourceID(id))
return g.downloadSigner.Sign(base.String(), userID, downloadURLTTL)
}
func (g BaseGraphService) downloadURLRequested(r *http.Request) bool {
return g.downloadSigner != nil && driveItemPropertySelected(r, _selectDownloadURL)
}
@@ -0,0 +1,180 @@
package svc_test
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"net/url"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/chi/v5"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/mock"
"google.golang.org/grpc"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/signedurl"
cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/graph/mocks"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults"
identitymocks "github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks"
"github.com/opencloud-eu/opencloud/services/graph/pkg/metrics"
service "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0"
)
const urlSigningSecret = "url-signing-secret"
// verifySignedDownloadURL checks the signature of a download url for the given user and returns the url
func verifySignedDownloadURL(signed, userID string) *url.URL {
GinkgoHelper()
verifier, err := signedurl.NewJWTSignedURL(signedurl.WithSecret(urlSigningSecret))
Expect(err).ToNot(HaveOccurred())
subject, err := verifier.Verify(signed)
Expect(err).ToNot(HaveOccurred())
Expect(subject).To(Equal(userID))
u, err := url.Parse(signed)
Expect(err).ToNot(HaveOccurred())
return u
}
var _ = Describe("GetDriveItemContent", func() {
const (
driveID = "storageid$spaceid"
itemID = "storageid$spaceid!nodeid"
)
var (
ctx context.Context
gatewayClient *cs3mocks.GatewayAPIClient
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
eventsPublisher mocks.Publisher
rr *httptest.ResponseRecorder
fileInfo *provider.ResourceInfo
currentUser = &userpb.User{Id: &userpb.UserId{OpaqueId: "user"}}
)
newService := func(signingSecret string) service.Service {
logger := log.NewLogger()
metrics, _ := metrics.New(prometheus.NewRegistry(), &logger, func([]string) (string, string) { return "", "" })
cfg := defaults.FullDefaultConfig()
cfg.Identity.LDAP.CACert = ""
cfg.TokenManager.JWTSecret = "loremipsum"
cfg.Commons = &shared.Commons{URLSigningSecret: signingSecret}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
svc, err := service.NewService(
service.Config(cfg),
service.Metrics(metrics),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(&identitymocks.Backend{}),
)
Expect(err).ToNot(HaveOccurred())
return svc
}
newRequest := func(withUser bool) *http.Request {
r := httptest.NewRequest(http.MethodGet, "/graph/v1beta1/drives/"+driveID+"/items/"+itemID+"/content", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("driveID", driveID)
rctx.URLParams.Add("itemID", itemID)
c := ctx
if withUser {
c = revactx.ContextSetUser(c, currentUser)
}
return r.WithContext(context.WithValue(c, chi.RouteCtxKey, rctx))
}
BeforeEach(func() {
eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
pool.RemoveSelector("GatewaySelector" + "eu.opencloud.api.gateway")
gatewayClient = &cs3mocks.GatewayAPIClient{}
gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient](
"GatewaySelector",
"eu.opencloud.api.gateway",
func(cc grpc.ClientConnInterface) gateway.GatewayAPIClient {
return gatewayClient
},
)
rr = httptest.NewRecorder()
ctx = context.Background()
fileInfo = &provider.ResourceInfo{
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "nodeid"},
}
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{
Status: status.NewOK(ctx),
Info: fileInfo,
}, nil)
})
It("redirects to a signed download url", func() {
newService(urlSigningSecret).GetDriveItemContent(rr, newRequest(true))
Expect(rr.Code).To(Equal(http.StatusFound))
target := verifySignedDownloadURL(rr.Header().Get("Location"), "user")
Expect(target.Path).To(Equal("/dav/spaces/" + itemID))
})
It("returns 404 for a folder", func() {
fileInfo.Type = provider.ResourceType_RESOURCE_TYPE_CONTAINER
newService(urlSigningSecret).GetDriveItemContent(rr, newRequest(true))
Expect(rr.Code).To(Equal(http.StatusNotFound))
})
It("returns 404 for an unknown item", func() {
gatewayClient.ExpectedCalls = nil
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{Status: status.NewNotFound(ctx, "not found")}, nil)
newService(urlSigningSecret).GetDriveItemContent(rr, newRequest(true))
Expect(rr.Code).To(Equal(http.StatusNotFound))
})
It("treats permission denied as not found", func() {
gatewayClient.ExpectedCalls = nil
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{Status: status.NewPermissionDenied(ctx, errors.New("denied"), "denied")}, nil)
newService(urlSigningSecret).GetDriveItemContent(rr, newRequest(true))
Expect(rr.Code).To(Equal(http.StatusNotFound))
})
It("returns 404 when the item belongs to another drive", func() {
r := newRequest(true)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("driveID", "storageid$otherspace")
rctx.URLParams.Add("itemID", itemID)
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
newService(urlSigningSecret).GetDriveItemContent(rr, r)
Expect(rr.Code).To(Equal(http.StatusNotFound))
gatewayClient.AssertNotCalled(GinkgoT(), "Stat", mock.Anything, mock.Anything)
})
It("returns 500 without a user in the context", func() {
newService(urlSigningSecret).GetDriveItemContent(rr, newRequest(false))
Expect(rr.Code).To(Equal(http.StatusInternalServerError))
})
It("returns 500 when url signing is not configured", func() {
newService("").GetDriveItemContent(rr, newRequest(true))
Expect(rr.Code).To(Equal(http.StatusInternalServerError))
})
})
@@ -0,0 +1,294 @@
package svc
import (
"net/http"
"net/url"
"path"
"sort"
"time"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
)
const _versionIDCurrent = "current"
// ListDriveItemVersions lists the versions of a file
func (g Graph) ListDriveItemVersions(w http.ResponseWriter, r *http.Request) {
g.logger.Info().Msg("Calling ListDriveItemVersions")
itemID, ok := g.fileDriveItemFromRequest(w, r)
if !ok {
return
}
versions, ok := g.listDriveItemVersions(w, r, itemID)
if !ok {
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{Value: versions})
}
// GetDriveItemVersion returns a single version of a file
func (g Graph) GetDriveItemVersion(w http.ResponseWriter, r *http.Request) {
g.logger.Info().Msg("Calling GetDriveItemVersion")
itemID, ok := g.fileDriveItemFromRequest(w, r)
if !ok {
return
}
versionID := chi.URLParam(r, "versionID")
if versionID == _versionIDCurrent {
version, ok := g.currentDriveItemVersion(w, r, itemID)
if !ok {
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, version)
return
}
version, ok := g.getDriveItemVersion(w, r, itemID, versionID)
if !ok {
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, version)
}
// GetDriveItemVersionContent redirects to a signed download url for a version
func (g Graph) GetDriveItemVersionContent(w http.ResponseWriter, r *http.Request) {
g.logger.Info().Msg("Calling GetDriveItemVersionContent")
itemID, ok := g.fileDriveItemFromRequest(w, r)
if !ok {
return
}
versionID := chi.URLParam(r, "versionID")
user, ok := revactx.ContextGetUser(r.Context())
if !ok {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "user not in context")
return
}
var downloadURL string
var err error
switch versionID {
case _versionIDCurrent:
downloadURL, err = g.signedDownloadURL(itemID, user.GetId().GetOpaqueId())
default:
if _, ok := g.getDriveItemVersion(w, r, itemID, versionID); !ok {
return
}
downloadURL, err = g.signedVersionDownloadURL(itemID, versionID, user.GetId().GetOpaqueId())
}
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
http.Redirect(w, r, downloadURL, http.StatusFound)
}
// RestoreDriveItemVersion restores a version of a file
func (g Graph) RestoreDriveItemVersion(w http.ResponseWriter, r *http.Request) {
g.logger.Info().Msg("Calling RestoreDriveItemVersion")
itemID, ok := g.fileDriveItemFromRequest(w, r)
if !ok {
return
}
versionID := chi.URLParam(r, "versionID")
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
res, err := gatewayClient.RestoreFileVersion(r.Context(), &storageprovider.RestoreFileVersionRequest{
Ref: &storageprovider.Reference{ResourceId: itemID},
Key: versionID,
})
if err := errorcode.FromCS3Status(res.GetStatus(), err); err != nil {
errorcode.RenderError(w, r, err)
return
}
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
// fileDriveItemFromRequest parses and stats the item, rendering 404 unless it is a file
func (g Graph) fileDriveItemFromRequest(w http.ResponseWriter, r *http.Request) (*storageprovider.ResourceId, bool) {
driveID, err := parseIDParam(r, "driveID")
if err != nil {
errorcode.RenderError(w, r, err)
return nil, false
}
itemID, err := parseIDParam(r, "driveItemID")
if err != nil {
errorcode.RenderError(w, r, err)
return nil, false
}
if driveID.GetStorageId() != itemID.GetStorageId() || driveID.GetSpaceId() != itemID.GetSpaceId() {
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item does not exist")
return nil, false
}
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return nil, false
}
res, err := gatewayClient.Stat(r.Context(), &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: &itemID}})
if !renderStatStatus(w, r, res, err) {
return nil, false
}
if res.GetInfo().GetType() != storageprovider.ResourceType_RESOURCE_TYPE_FILE {
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Item is not a file")
return nil, false
}
return &itemID, true
}
// renderStatStatus renders the error of a failed stat
func renderStatStatus(w http.ResponseWriter, r *http.Request, res *storageprovider.StatResponse, err error) bool {
switch {
case err != nil:
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return false
case res.GetStatus().GetCode() == cs3rpc.Code_CODE_OK:
return true
case res.GetStatus().GetCode() == cs3rpc.Code_CODE_NOT_FOUND:
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.GetStatus().GetMessage())
return false
case res.GetStatus().GetCode() == cs3rpc.Code_CODE_PERMISSION_DENIED:
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.GetStatus().GetMessage())
return false
case res.GetStatus().GetCode() == cs3rpc.Code_CODE_UNAUTHENTICATED:
errorcode.Unauthenticated.Render(w, r, http.StatusUnauthorized, res.GetStatus().GetMessage())
return false
default:
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.GetStatus().GetMessage())
return false
}
}
func (g Graph) listDriveItemVersions(w http.ResponseWriter, r *http.Request, itemID *storageprovider.ResourceId) ([]libregraph.DriveItemVersion, bool) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return nil, false
}
res, err := gatewayClient.ListFileVersions(r.Context(), &storageprovider.ListFileVersionsRequest{
Ref: &storageprovider.Reference{ResourceId: itemID},
})
if err := errorcode.FromCS3Status(res.GetStatus(), err); err != nil {
errorcode.RenderError(w, r, err)
return nil, false
}
fileVersions := res.GetVersions()
// newest first, the storage does not guarantee an order
sort.SliceStable(fileVersions, func(i, j int) bool {
if fileVersions[i].GetMtime() != fileVersions[j].GetMtime() {
return fileVersions[i].GetMtime() > fileVersions[j].GetMtime()
}
return fileVersions[i].GetKey() > fileVersions[j].GetKey()
})
versions := make([]libregraph.DriveItemVersion, 0, len(fileVersions))
for _, fv := range fileVersions {
version := libregraph.NewDriveItemVersion()
version.SetId(fv.GetKey())
version.SetLastModifiedDateTime(time.Unix(int64(fv.GetMtime()), 0).UTC())
version.SetSize(int64(fv.GetSize()))
g.setDriveItemVersionDownloadURL(r, version, itemID, fv.GetKey())
versions = append(versions, *version)
}
return versions, true
}
func (g Graph) getDriveItemVersion(w http.ResponseWriter, r *http.Request, itemID *storageprovider.ResourceId, versionID string) (*libregraph.DriveItemVersion, bool) {
versions, ok := g.listDriveItemVersions(w, r, itemID)
if !ok {
return nil, false
}
for i := range versions {
if versions[i].GetId() == versionID {
return &versions[i], true
}
}
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "Version does not exist")
return nil, false
}
// currentDriveItemVersion describes the file itself as a version
func (g Graph) currentDriveItemVersion(w http.ResponseWriter, r *http.Request, itemID *storageprovider.ResourceId) (*libregraph.DriveItemVersion, bool) {
gatewayClient, err := g.gatewaySelector.Next()
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return nil, false
}
res, err := gatewayClient.Stat(r.Context(), &storageprovider.StatRequest{Ref: &storageprovider.Reference{ResourceId: itemID}})
if !renderStatStatus(w, r, res, err) {
return nil, false
}
version := libregraph.NewDriveItemVersion()
version.SetId(_versionIDCurrent)
version.SetLastModifiedDateTime(cs3TimestampToTime(res.GetInfo().GetMtime()).UTC())
version.SetSize(int64(res.GetInfo().GetSize()))
if user, ok := revactx.ContextGetUser(r.Context()); ok && g.downloadURLRequested(r) {
if u, err := g.signedDownloadURL(itemID, user.GetId().GetOpaqueId()); err == nil {
version.SetMicrosoftGraphDownloadUrl(u)
}
}
return version, true
}
func (g Graph) setDriveItemVersionDownloadURL(r *http.Request, version *libregraph.DriveItemVersion, itemID *storageprovider.ResourceId, key string) {
if !g.downloadURLRequested(r) {
return
}
user, ok := revactx.ContextGetUser(r.Context())
if !ok {
return
}
u, err := g.signedVersionDownloadURL(itemID, key, user.GetId().GetOpaqueId())
if err != nil {
return
}
version.SetMicrosoftGraphDownloadUrl(u)
}
// signedVersionDownloadURL signs a download url for the WebDAV meta endpoint of a version
func (g BaseGraphService) signedVersionDownloadURL(itemID *storageprovider.ResourceId, key, userID string) (string, error) {
if g.downloadSigner == nil {
return "", ErrDownloadURLSigningNotConfigured
}
u, err := g.getWebDavMetaURL()
if err != nil {
return "", err
}
u.Path = path.Join(u.Path, storagespace.FormatResourceID(itemID), "v", key)
return g.downloadSigner.Sign(u.String(), userID, downloadURLTTL)
}
func (g BaseGraphService) getWebDavMetaURL() (*url.URL, error) {
u := *g.publicBaseURL
u.Path = path.Join(u.Path, path.Dir(path.Clean(g.config.Spaces.WebDavPath)), "meta")
return &u, nil
}
@@ -0,0 +1,319 @@
package svc_test
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/chi/v5"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/mock"
"google.golang.org/grpc"
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/signedurl"
"github.com/opencloud-eu/reva/v2/pkg/utils"
cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/pkg/shared"
"github.com/opencloud-eu/opencloud/services/graph/mocks"
"github.com/opencloud-eu/opencloud/services/graph/pkg/config/defaults"
identitymocks "github.com/opencloud-eu/opencloud/services/graph/pkg/identity/mocks"
"github.com/opencloud-eu/opencloud/services/graph/pkg/metrics"
service "github.com/opencloud-eu/opencloud/services/graph/pkg/service/v0"
)
var _ = Describe("DriveItemVersions", func() {
const (
urlSigningSecret = "url-signing-secret"
driveID = "storageid$spaceid"
itemID = "storageid$spaceid!nodeid"
olderKey = "nodeid.REV.2026-09-01T10:00:00.000000000Z"
newerKey = "nodeid.REV.2026-09-05T10:00:00.000000000Z"
)
var (
svc service.Service
ctx context.Context
gatewayClient *cs3mocks.GatewayAPIClient
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
eventsPublisher mocks.Publisher
identityBackend *identitymocks.Backend
rr *httptest.ResponseRecorder
fileInfo *provider.ResourceInfo
older *provider.FileVersion
newer *provider.FileVersion
fileTime = time.Date(2026, 9, 7, 12, 0, 0, 0, time.UTC)
currentUser = &userpb.User{
Id: &userpb.UserId{
OpaqueId: "user",
},
}
)
newRequest := func(method, versionPath, query string) *http.Request {
r := httptest.NewRequest(method, "/graph/v1.0/drives/"+driveID+"/items/"+itemID+"/versions"+versionPath+query, nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("driveID", driveID)
rctx.URLParams.Add("driveItemID", itemID)
return r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
}
newVersionRequest := func(method, versionID, suffix, query string) *http.Request {
r := newRequest(method, "/"+versionID+suffix, query)
chi.RouteContext(r.Context()).URLParams.Add("versionID", versionID)
return r
}
verifiedTarget := func(signed string) *url.URL {
verifier, err := signedurl.NewJWTSignedURL(signedurl.WithSecret(urlSigningSecret))
Expect(err).ToNot(HaveOccurred())
subject, err := verifier.Verify(signed)
Expect(err).ToNot(HaveOccurred())
Expect(subject).To(Equal("user"))
target, err := url.Parse(signed)
Expect(err).ToNot(HaveOccurred())
return target
}
BeforeEach(func() {
eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
pool.RemoveSelector("GatewaySelector" + "eu.opencloud.api.gateway")
gatewayClient = &cs3mocks.GatewayAPIClient{}
gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient](
"GatewaySelector",
"eu.opencloud.api.gateway",
func(cc grpc.ClientConnInterface) gateway.GatewayAPIClient {
return gatewayClient
},
)
logger := log.NewLogger()
identityBackend = &identitymocks.Backend{}
metrics, _ := metrics.New(prometheus.NewRegistry(), &logger, func([]string) (string, string) { return "", "" })
rr = httptest.NewRecorder()
ctx = context.Background()
cfg := defaults.FullDefaultConfig()
cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests
cfg.TokenManager.JWTSecret = "loremipsum"
cfg.Commons = &shared.Commons{
URLSigningSecret: urlSigningSecret,
}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
var err error
svc, err = service.NewService(
service.Config(cfg),
service.Metrics(metrics),
service.WithGatewaySelector(gatewaySelector),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityBackend(identityBackend),
)
Expect(err).ToNot(HaveOccurred())
fileInfo = &provider.ResourceInfo{
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "nodeid"},
Etag: "etag",
Size: 300,
Mtime: utils.TimeToTS(fileTime),
}
older = &provider.FileVersion{Key: olderKey, Size: 100, Mtime: uint64(fileTime.Add(-6 * 24 * time.Hour).Unix())}
newer = &provider.FileVersion{Key: newerKey, Size: 200, Mtime: uint64(fileTime.Add(-2 * 24 * time.Hour).Unix())}
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{
Status: status.NewOK(ctx),
Info: fileInfo,
}, nil)
gatewayClient.On("ListFileVersions", mock.Anything, mock.Anything).Return(&provider.ListFileVersionsResponse{
Status: status.NewOK(ctx),
Versions: []*provider.FileVersion{older, newer},
}, nil)
})
Describe("ListDriveItemVersions", func() {
list := func(r *http.Request) []libregraph.DriveItemVersion {
svc.ListDriveItemVersions(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := libregraph.CollectionOfDriveItemVersions{}
Expect(json.Unmarshal(data, &res)).To(Succeed())
return res.Value
}
It("lists the versions newest first", func() {
versions := list(newRequest(http.MethodGet, "", ""))
Expect(versions).To(HaveLen(2))
Expect(versions[0].GetId()).To(Equal(newerKey))
Expect(versions[0].GetSize()).To(Equal(int64(200)))
Expect(versions[0].GetLastModifiedDateTime()).To(Equal(fileTime.Add(-2 * 24 * time.Hour)))
Expect(versions[0].MicrosoftGraphDownloadUrl).To(BeNil())
Expect(versions[0].LastModifiedBy).To(BeNil())
Expect(versions[1].GetId()).To(Equal(olderKey))
Expect(versions[1].GetSize()).To(Equal(int64(100)))
})
It("returns an empty list for a file without versions", func() {
gatewayClient.ExpectedCalls = nil
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{Status: status.NewOK(ctx), Info: fileInfo}, nil)
gatewayClient.On("ListFileVersions", mock.Anything, mock.Anything).Return(&provider.ListFileVersionsResponse{Status: status.NewOK(ctx)}, nil)
svc.ListDriveItemVersions(rr, newRequest(http.MethodGet, "", ""))
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
Expect(data).To(MatchJSON(`{"value":[]}`))
})
It("adds a signed download url for every version when requested via $select", func() {
versions := list(newRequest(http.MethodGet, "", "?$select=@microsoft.graph.downloadUrl"))
Expect(versions).To(HaveLen(2))
target := verifiedTarget(versions[0].GetMicrosoftGraphDownloadUrl())
Expect(target.Path).To(Equal("/dav/meta/" + itemID + "/v/" + newerKey))
Expect(verifiedTarget(versions[1].GetMicrosoftGraphDownloadUrl()).Path).To(Equal("/dav/meta/" + itemID + "/v/" + olderKey))
})
It("returns 404 for a folder", func() {
fileInfo.Type = provider.ResourceType_RESOURCE_TYPE_CONTAINER
svc.ListDriveItemVersions(rr, newRequest(http.MethodGet, "", ""))
Expect(rr.Code).To(Equal(http.StatusNotFound))
gatewayClient.AssertNotCalled(GinkgoT(), "ListFileVersions", mock.Anything, mock.Anything)
})
It("returns 404 for an unknown item", func() {
gatewayClient.ExpectedCalls = nil
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(&provider.StatResponse{Status: status.NewNotFound(ctx, "not found")}, nil)
svc.ListDriveItemVersions(rr, newRequest(http.MethodGet, "", ""))
Expect(rr.Code).To(Equal(http.StatusNotFound))
})
It("returns 404 when the item belongs to another drive", func() {
r := newRequest(http.MethodGet, "", "")
rctx := chi.NewRouteContext()
rctx.URLParams.Add("driveID", "storageid$otherspace")
rctx.URLParams.Add("driveItemID", itemID)
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
svc.ListDriveItemVersions(rr, r)
Expect(rr.Code).To(Equal(http.StatusNotFound))
gatewayClient.AssertNotCalled(GinkgoT(), "Stat", mock.Anything, mock.Anything)
})
})
Describe("GetDriveItemVersion", func() {
get := func(r *http.Request) libregraph.DriveItemVersion {
svc.GetDriveItemVersion(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
version := libregraph.DriveItemVersion{}
Expect(json.Unmarshal(data, &version)).To(Succeed())
return version
}
It("returns the requested version", func() {
version := get(newVersionRequest(http.MethodGet, olderKey, "", ""))
Expect(version.GetId()).To(Equal(olderKey))
Expect(version.GetSize()).To(Equal(int64(100)))
Expect(version.MicrosoftGraphDownloadUrl).To(BeNil())
})
It("adds a signed download url when requested via $select", func() {
version := get(newVersionRequest(http.MethodGet, olderKey, "", "?$select=@microsoft.graph.downloadUrl"))
Expect(verifiedTarget(version.GetMicrosoftGraphDownloadUrl()).Path).To(Equal("/dav/meta/" + itemID + "/v/" + olderKey))
})
It("describes the file itself as the current version", func() {
version := get(newVersionRequest(http.MethodGet, "current", "", "?$select=@microsoft.graph.downloadUrl"))
Expect(version.GetId()).To(Equal("current"))
Expect(version.GetSize()).To(Equal(int64(300)))
Expect(version.GetLastModifiedDateTime()).To(Equal(fileTime))
Expect(verifiedTarget(version.GetMicrosoftGraphDownloadUrl()).Path).To(Equal("/dav/spaces/" + itemID))
gatewayClient.AssertNotCalled(GinkgoT(), "ListFileVersions", mock.Anything, mock.Anything)
})
It("returns 404 for an unknown version", func() {
svc.GetDriveItemVersion(rr, newVersionRequest(http.MethodGet, "nodeid.REV.unknown", "", ""))
Expect(rr.Code).To(Equal(http.StatusNotFound))
})
})
Describe("GetDriveItemVersionContent", func() {
It("redirects to a signed download url for the version", func() {
svc.GetDriveItemVersionContent(rr, newVersionRequest(http.MethodGet, olderKey, "/content", ""))
Expect(rr.Code).To(Equal(http.StatusFound))
Expect(verifiedTarget(rr.Header().Get("Location")).Path).To(Equal("/dav/meta/" + itemID + "/v/" + olderKey))
})
It("redirects to the file itself for the current version", func() {
svc.GetDriveItemVersionContent(rr, newVersionRequest(http.MethodGet, "current", "/content", ""))
Expect(rr.Code).To(Equal(http.StatusFound))
Expect(verifiedTarget(rr.Header().Get("Location")).Path).To(Equal("/dav/spaces/" + itemID))
})
It("returns 404 for an unknown version", func() {
svc.GetDriveItemVersionContent(rr, newVersionRequest(http.MethodGet, "nodeid.REV.unknown", "/content", ""))
Expect(rr.Code).To(Equal(http.StatusNotFound))
})
})
Describe("RestoreDriveItemVersion", func() {
It("restores the version and answers 204", func() {
gatewayClient.On("RestoreFileVersion", mock.Anything, mock.MatchedBy(func(req *provider.RestoreFileVersionRequest) bool {
return req.GetKey() == olderKey && req.GetRef().GetResourceId().GetOpaqueId() == "nodeid"
})).Return(&provider.RestoreFileVersionResponse{Status: status.NewOK(ctx)}, nil)
svc.RestoreDriveItemVersion(rr, newVersionRequest(http.MethodPost, olderKey, "/restoreVersion", ""))
Expect(rr.Code).To(Equal(http.StatusNoContent))
})
It("returns 423 when the file is locked", func() {
gatewayClient.On("RestoreFileVersion", mock.Anything, mock.Anything).Return(&provider.RestoreFileVersionResponse{Status: status.NewLocked(ctx, "locked")}, nil)
svc.RestoreDriveItemVersion(rr, newVersionRequest(http.MethodPost, olderKey, "/restoreVersion", ""))
Expect(rr.Code).To(Equal(http.StatusLocked))
})
It("returns 404 for an unknown version", func() {
gatewayClient.On("RestoreFileVersion", mock.Anything, mock.Anything).Return(&provider.RestoreFileVersionResponse{Status: status.NewNotFound(ctx, "not found")}, nil)
svc.RestoreDriveItemVersion(rr, newVersionRequest(http.MethodPost, "nodeid.REV.unknown", "/restoreVersion", ""))
Expect(rr.Code).To(Equal(http.StatusNotFound))
})
It("returns 404 for a folder", func() {
fileInfo.Type = provider.ResourceType_RESOURCE_TYPE_CONTAINER
svc.RestoreDriveItemVersion(rr, newVersionRequest(http.MethodPost, olderKey, "/restoreVersion", ""))
Expect(rr.Code).To(Equal(http.StatusNotFound))
gatewayClient.AssertNotCalled(GinkgoT(), "RestoreFileVersion", mock.Anything, mock.Anything)
})
})
})
+13 -14
View File
@@ -29,7 +29,6 @@ import (
"github.com/opencloud-eu/reva/v2/pkg/tags"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
"github.com/opencloud-eu/opencloud/services/graph/pkg/unifiedrole"
"github.com/opencloud-eu/opencloud/services/search/pkg/mapping"
@@ -39,6 +38,7 @@ import (
const (
_selectAllowedValues = "@libre.graph.permissions.actions.allowedValues"
_selectShareTypes = "@libre.graph.shareTypes"
_selectDownloadURL = "@microsoft.graph.downloadUrl"
)
// without it the provider leaves the share-types opaque empty
@@ -250,7 +250,7 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
return
}
files, err := formatDriveItems(g.logger, g.publicBaseURL, lRes.GetInfos())
files, err := g.formatDriveItems(lRes.GetInfos())
if err != nil {
g.logger.Error().Err(err).Msg("error encoding response as json")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
@@ -267,6 +267,7 @@ func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
if driveItemPropertySelected(r, _selectShareTypes) {
g.addShareTypes(ctx, files, lRes.GetInfos())
}
g.SetDriveItemsDownloadURL(r, files)
render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{Value: files})
@@ -326,12 +327,11 @@ func (g Graph) GetDriveItem(w http.ResponseWriter, r *http.Request) {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.GetStatus().GetMessage())
return
}
driveItem, err := cs3ResourceToDriveItem(g.logger, g.publicBaseURL, res.GetInfo())
driveItem, err := g.cs3ResourceToDriveItem(res.GetInfo())
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
if driveItemPropertySelected(r, _selectAllowedValues) {
driveItem.LibreGraphPermissionsActionsAllowedValues = unifiedrole.CS3ResourcePermissionsToLibregraphActions(res.GetInfo().GetPermissionSet())
}
@@ -349,6 +349,7 @@ func (g Graph) GetDriveItem(w http.ResponseWriter, r *http.Request) {
infos := []*storageprovider.ResourceInfo{res.GetInfo()}
driveItem.LibreGraphShareTypes = shareTypesOf(res.GetInfo(), g.listLinkShares(ctx, infos))
}
g.setDriveItemDownloadURL(r, driveItem)
if driveItemRelationExpanded(r, _expandThumbnails) {
setDriveItemThumbnails(driveItem, res.GetInfo(), g.config.Commons.OpenCloudURL)
@@ -430,7 +431,7 @@ func (g Graph) listDriveItemChildren(w http.ResponseWriter, r *http.Request, dri
return nil, false
}
files, err := formatDriveItems(g.logger, g.publicBaseURL, res.GetInfos())
files, err := g.formatDriveItems(res.GetInfos())
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return nil, false
@@ -439,6 +440,7 @@ func (g Graph) listDriveItemChildren(w http.ResponseWriter, r *http.Request, dri
if driveItemPropertySelected(r, _selectShareTypes) {
g.addShareTypes(r.Context(), files, res.GetInfos())
}
g.SetDriveItemsDownloadURL(r, files)
g.setDriveItemsThumbnails(r, files, res.GetInfos())
@@ -483,10 +485,10 @@ func (g Graph) getRemoteItem(ctx context.Context, root *storageprovider.Resource
return item, nil
}
func formatDriveItems(logger *log.Logger, publicBaseURL *url.URL, mds []*storageprovider.ResourceInfo) ([]libregraph.DriveItem, error) {
func (g BaseGraphService) formatDriveItems(mds []*storageprovider.ResourceInfo) ([]libregraph.DriveItem, error) {
responses := make([]libregraph.DriveItem, 0, len(mds))
for i := range mds {
res, err := cs3ResourceToDriveItem(logger, publicBaseURL, mds[i])
res, err := g.cs3ResourceToDriveItem(mds[i])
if err != nil {
return nil, err
}
@@ -500,19 +502,16 @@ func cs3TimestampToTime(t *types.Timestamp) time.Time {
return time.Unix(int64(t.GetSeconds()), int64(t.GetNanos()))
}
func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) {
func (g BaseGraphService) cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) {
size := new(int64)
*size = int64(res.GetSize()) // TODO lurking overflow: make size of libregraph drive item use uint64
driveItem := &libregraph.DriveItem{
Id: libregraph.PtrString(storagespace.FormatResourceID(res.GetId())),
Size: size,
Id: libregraph.PtrString(storagespace.FormatResourceID(res.GetId())),
Size: size,
WebUrl: g.webURLForResource(res.GetId()),
}
webURL := *publicBaseURL
webURL.Path = path.Join(webURL.Path, "f", storagespace.FormatResourceID(res.GetId()))
driveItem.WebUrl = libregraph.PtrString(webURL.String())
if name := path.Base(res.GetPath()); name != "" {
driveItem.Name = &name
}
@@ -90,7 +90,9 @@ var _ = Describe("Driveitems", func() {
cfg = defaults.FullDefaultConfig()
cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests
cfg.TokenManager.JWTSecret = "loremipsum"
cfg.Commons = &shared.Commons{}
cfg.Commons = &shared.Commons{
URLSigningSecret: urlSigningSecret,
}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
var err error
@@ -277,6 +279,70 @@ var _ = Describe("Driveitems", func() {
unifiedrole.DriveItemContentRead,
))
})
It("adds @microsoft.graph.downloadUrl when selected via $select", func() {
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*provider.StorageSpace{{Owner: currentUser, Root: &provider.ResourceId{}}},
}, nil)
gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
Status: status.NewOK(ctx),
Infos: []*provider.ResourceInfo{
{
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"},
Etag: "etag",
Mtime: utils.TimeToTS(time.Now()),
},
},
}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drive/root/children?$select=@microsoft.graph.downloadUrl", nil)
r = r.WithContext(revactx.ContextSetUser(ctx, currentUser))
svc.GetRootDriveChildren(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := itemsList{}
Expect(json.Unmarshal(data, &res)).To(Succeed())
Expect(res.Value).To(HaveLen(1))
target := verifySignedDownloadURL(res.Value[0].GetMicrosoftGraphDownloadUrl(), "user")
Expect(target.Path).To(Equal("/dav/spaces/storageid$spaceid!opaqueid"))
})
It("honours @microsoft.graph.downloadUrl in a combined $select", func() {
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*provider.StorageSpace{{Owner: currentUser, Root: &provider.ResourceId{}}},
}, nil)
gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
Status: status.NewOK(ctx),
Infos: []*provider.ResourceInfo{
{
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"},
Etag: "etag",
Mtime: utils.TimeToTS(time.Now()),
PermissionSet: &provider.ResourcePermissions{GetPath: true, InitiateFileDownload: true},
},
},
}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drive/root/children?$select=@libre.graph.permissions.actions.allowedValues,@microsoft.graph.downloadUrl", nil)
r = r.WithContext(revactx.ContextSetUser(ctx, currentUser))
svc.GetRootDriveChildren(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := itemsList{}
Expect(json.Unmarshal(data, &res)).To(Succeed())
Expect(res.Value).To(HaveLen(1))
Expect(res.Value[0].MicrosoftGraphDownloadUrl).ToNot(BeNil())
Expect(res.Value[0].GetLibreGraphPermissionsActionsAllowedValues()).To(ConsistOf(
unifiedrole.DriveItemPathRead,
unifiedrole.DriveItemContentRead,
))
})
})
Describe("GetDriveItem", func() {
@@ -389,6 +455,21 @@ var _ = Describe("Driveitems", func() {
Expect(item.Children[0].Thumbnails).To(HaveLen(1))
})
})
It("adds @microsoft.graph.downloadUrl to a file when selected via $select", func() {
folderInfo.Type = provider.ResourceType_RESOURCE_TYPE_FILE
Expect(getItem(newRequest("")).MicrosoftGraphDownloadUrl).To(BeNil())
rr = httptest.NewRecorder()
item := getItem(newRequest("?$select=@microsoft.graph.downloadUrl"))
target := verifySignedDownloadURL(item.GetMicrosoftGraphDownloadUrl(), "user")
Expect(target.Path).To(Equal("/dav/spaces/storageid$spaceid!nodeid"))
})
It("omits @microsoft.graph.downloadUrl for a folder when selected via $select", func() {
Expect(getItem(newRequest("?$select=@microsoft.graph.downloadUrl")).MicrosoftGraphDownloadUrl).To(BeNil())
})
})
Describe("GetDriveItem $expand=children error", func() {
@@ -510,6 +591,7 @@ var _ = Describe("Driveitems", func() {
res := assertItemsList(1)
Expect(res.Value[0].Audio).To(BeNil())
Expect(res.Value[0].Location).To(BeNil())
Expect(res.Value[0].MicrosoftGraphDownloadUrl).To(BeNil())
Expect(res.Value[0].LibreGraphMeFollowing).To(BeNil())
Expect(res.Value[0].LibreGraphTags).To(BeNil())
Expect(res.Value[0].PendingOperations).To(BeNil())
@@ -750,6 +832,60 @@ var _ = Describe("Driveitems", func() {
Expect(res.Value[0].GetLibreGraphMeFollowing()).To(BeFalse())
})
It("adds @microsoft.graph.downloadUrl to files when selected via $select", func() {
gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
Status: status.NewOK(ctx),
Infos: []*provider.ResourceInfo{
{
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"},
Etag: "etag",
Mtime: utils.TimeToTS(mtime),
},
},
}, nil)
r = httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives/storageid$spaceid/items/storageid$spaceid!nodeid/children?$select=@microsoft.graph.downloadUrl", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("driveID", "storageid$spaceid")
rctx.URLParams.Add("driveItemID", "storageid$spaceid!nodeid")
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
res := assertItemsList(1)
target := verifySignedDownloadURL(res.Value[0].GetMicrosoftGraphDownloadUrl(), "user")
Expect(target.Path).To(Equal("/dav/spaces/storageid$spaceid!opaqueid"))
})
It("omits @microsoft.graph.downloadUrl for folders when selected via $select", func() {
gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
Status: status.NewOK(ctx),
Infos: []*provider.ResourceInfo{
{
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "opaqueid"},
Etag: "etag",
Mtime: utils.TimeToTS(mtime),
},
{
Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER,
Id: &provider.ResourceId{StorageId: "storageid", SpaceId: "spaceid", OpaqueId: "folderid"},
Etag: "etag",
Mtime: utils.TimeToTS(mtime),
},
},
}, nil)
r = httptest.NewRequest(http.MethodGet, "/graph/v1.0/drives/storageid$spaceid/items/storageid$spaceid!nodeid/children?$select=@microsoft.graph.downloadUrl", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("driveID", "storageid$spaceid")
rctx.URLParams.Add("driveItemID", "storageid$spaceid!nodeid")
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
res := assertItemsList(2)
Expect(res.Value[0].MicrosoftGraphDownloadUrl).ToNot(BeNil())
Expect(res.Value[1].MicrosoftGraphDownloadUrl).To(BeNil())
})
It("returns the audio facet if metadata is available", func() {
gatewayClient.On("ListContainer", mock.Anything, mock.Anything).Return(&provider.ListContainerResponse{
Status: status.NewOK(ctx),
@@ -26,7 +26,8 @@ func TestCS3ResourceToDriveItemPopulatesWebUrl(t *testing.T) {
base, err := url.Parse("https://example.com")
require.NoError(t, err)
item, err := cs3ResourceToDriveItem(&logger, base, res)
g := BaseGraphService{logger: &logger, publicBaseURL: base}
item, err := g.cs3ResourceToDriveItem(res)
require.NoError(t, err)
require.NotNil(t, item.WebUrl)
assert.Equal(t, "https://example.com/f/storage-1$space-1%21item-1", *item.WebUrl)
@@ -36,7 +37,8 @@ func TestCS3ResourceToDriveItemPopulatesWebUrl(t *testing.T) {
base, err := url.Parse("https://example.com/cloud")
require.NoError(t, err)
item, err := cs3ResourceToDriveItem(&logger, base, res)
g := BaseGraphService{logger: &logger, publicBaseURL: base}
item, err := g.cs3ResourceToDriveItem(res)
require.NoError(t, err)
require.NotNil(t, item.WebUrl)
assert.Equal(t, "https://example.com/cloud/f/storage-1$space-1%21item-1", *item.WebUrl)
+1 -11
View File
@@ -862,17 +862,7 @@ func (g Graph) cs3StorageSpaceToDrive(ctx context.Context, baseURL *url.URL, spa
drive.Root.WebDavUrl = libregraph.PtrString(webDavURL.String())
}
webURL, err := url.Parse(g.config.Spaces.WebDavBase)
if err != nil {
logger.Error().
Err(err).
Str("url", g.config.Spaces.WebDavBase).
Msg("failed to parse webURL base url")
return nil, err
}
webURL.Path = path.Join(webURL.Path, "f", storagespace.FormatResourceID(spaceRid))
drive.WebUrl = libregraph.PtrString(webURL.String())
drive.WebUrl = g.webURLForResource(spaceRid)
if space.Owner != nil && space.Owner.Id != nil {
drive.Owner = &libregraph.IdentitySet{
+1 -1
View File
@@ -98,7 +98,7 @@ func (g Graph) FollowDriveItem(w http.ResponseWriter, r *http.Request) {
}
}
driveItem, err := cs3ResourceToDriveItem(g.logger, g.publicBaseURL, statRes.GetInfo())
driveItem, err := g.cs3ResourceToDriveItem(statRes.GetInfo())
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
+3 -6
View File
@@ -96,13 +96,10 @@ func (g Graph) publishEvent(ctx context.Context, ev any) {
}
}
func (g Graph) getWebDavBaseURL() (*url.URL, error) {
webDavBaseURL, err := url.Parse(g.config.Spaces.WebDavBase)
if err != nil {
return nil, err
}
func (g BaseGraphService) getWebDavBaseURL() (*url.URL, error) {
webDavBaseURL := *g.publicBaseURL
webDavBaseURL.Path = path.Join(webDavBaseURL.Path, g.config.Spaces.WebDavPath)
return webDavBaseURL, nil
return &webDavBaseURL, nil
}
// ListResponse is used for proper marshalling of Graph list responses
+26
View File
@@ -14,6 +14,7 @@ import (
"github.com/riandyrn/otelchi"
microstore "go-micro.dev/v4/store"
"github.com/opencloud-eu/reva/v2/pkg/signedurl"
"github.com/opencloud-eu/reva/v2/pkg/store"
"github.com/opencloud-eu/opencloud/pkg/roles"
@@ -104,6 +105,11 @@ type Service interface { //nolint:interfacebloat
GetRootDriveChildren(w http.ResponseWriter, r *http.Request)
GetDriveItem(w http.ResponseWriter, r *http.Request)
GetDriveItemChildren(w http.ResponseWriter, r *http.Request)
GetDriveItemContent(w http.ResponseWriter, r *http.Request)
ListDriveItemVersions(w http.ResponseWriter, r *http.Request)
GetDriveItemVersion(w http.ResponseWriter, r *http.Request)
GetDriveItemVersionContent(w http.ResponseWriter, r *http.Request)
RestoreDriveItemVersion(w http.ResponseWriter, r *http.Request)
CreateUploadSession(w http.ResponseWriter, r *http.Request)
@@ -146,6 +152,16 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
return Graph{}, fmt.Errorf("could not parse graph.spaces.webdav_base: %w", err)
}
var downloadSigner signedurl.Signer
if options.Config.Commons != nil && options.Config.Commons.URLSigningSecret != "" {
downloadSigner, err = signedurl.NewJWTSignedURL(signedurl.WithSecret(options.Config.Commons.URLSigningSecret))
if err != nil {
return Graph{}, fmt.Errorf("could not create download url signer: %w", err)
}
} else {
options.Logger.Warn().Msg("no url signing secret configured, driveItem download urls are disabled")
}
baseGraphService := BaseGraphService{
logger: &options.Logger,
identityCache: identityCache,
@@ -153,6 +169,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
config: options.Config,
availableRoles: unifiedrole.GetRoles(unifiedrole.RoleFilterIDs(options.Config.UnifiedRoles.AvailableRoles...)),
publicBaseURL: publicBaseURL,
downloadSigner: downloadSigner,
}
drivesDriveItemService, err := NewDrivesDriveItemService(options.Logger, options.GatewaySelector)
@@ -271,6 +288,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
r.Get("/", drivesDriveItemApi.GetDriveItem)
r.Patch("/", drivesDriveItemApi.UpdateDriveItem)
r.Delete("/", drivesDriveItemApi.DeleteDriveItem)
r.Get("/content", svc.GetDriveItemContent)
r.Post("/invite", driveItemPermissionsApi.Invite)
r.Post("/createLink", driveItemPermissionsApi.CreateLink)
r.Route("/permissions", func(r chi.Router) {
@@ -370,6 +388,14 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx
r.Get("/", svc.GetDriveItem)
r.Get("/children", svc.GetDriveItemChildren)
r.Post("/createUploadSession", svc.CreateUploadSession)
r.Route("/versions", func(r chi.Router) {
r.Get("/", svc.ListDriveItemVersions)
r.Route("/{versionID}", func(r chi.Router) {
r.Get("/", svc.GetDriveItemVersion)
r.Get("/content", svc.GetDriveItemVersionContent)
r.Post("/restoreVersion", svc.RestoreDriveItemVersion)
})
})
})
})
})
+1 -1
View File
@@ -7,13 +7,13 @@ import (
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/render"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
revaCtx "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/tags"
libregraph "github.com/opencloud-eu/libre-graph-api-go"
"go-micro.dev/v4/metadata"
)
@@ -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 CollectionOfDriveItemVersions type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &CollectionOfDriveItemVersions{}
// CollectionOfDriveItemVersions struct for CollectionOfDriveItemVersions
type CollectionOfDriveItemVersions struct {
Value []DriveItemVersion `json:"value,omitempty"`
}
// NewCollectionOfDriveItemVersions instantiates a new CollectionOfDriveItemVersions 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 NewCollectionOfDriveItemVersions() *CollectionOfDriveItemVersions {
this := CollectionOfDriveItemVersions{}
return &this
}
// NewCollectionOfDriveItemVersionsWithDefaults instantiates a new CollectionOfDriveItemVersions 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 NewCollectionOfDriveItemVersionsWithDefaults() *CollectionOfDriveItemVersions {
this := CollectionOfDriveItemVersions{}
return &this
}
// GetValue returns the Value field value if set, zero value otherwise.
func (o *CollectionOfDriveItemVersions) GetValue() []DriveItemVersion {
if o == nil || IsNil(o.Value) {
var ret []DriveItemVersion
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 *CollectionOfDriveItemVersions) GetValueOk() ([]DriveItemVersion, 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 *CollectionOfDriveItemVersions) HasValue() bool {
if o != nil && !IsNil(o.Value) {
return true
}
return false
}
// SetValue gets a reference to the given []DriveItemVersion and assigns it to the Value field.
func (o *CollectionOfDriveItemVersions) SetValue(v []DriveItemVersion) {
o.Value = v
}
func (o CollectionOfDriveItemVersions) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o CollectionOfDriveItemVersions) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
if !IsNil(o.Value) {
toSerialize["value"] = o.Value
}
return toSerialize, nil
}
type NullableCollectionOfDriveItemVersions struct {
value *CollectionOfDriveItemVersions
isSet bool
}
func (v NullableCollectionOfDriveItemVersions) Get() *CollectionOfDriveItemVersions {
return v.value
}
func (v *NullableCollectionOfDriveItemVersions) Set(val *CollectionOfDriveItemVersions) {
v.value = val
v.isSet = true
}
func (v NullableCollectionOfDriveItemVersions) IsSet() bool {
return v.isSet
}
func (v *NullableCollectionOfDriveItemVersions) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableCollectionOfDriveItemVersions(val *CollectionOfDriveItemVersions) *NullableCollectionOfDriveItemVersions {
return &NullableCollectionOfDriveItemVersions{value: val, isSet: true}
}
func (v NullableCollectionOfDriveItemVersions) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableCollectionOfDriveItemVersions) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}
@@ -0,0 +1,312 @@
/*
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"
"time"
)
// checks if the DriveItemVersion type satisfies the MappedNullable interface at compile time
var _ MappedNullable = &DriveItemVersion{}
// DriveItemVersion Represents a specific version of a driveItem. Read-only. Modeled on the MS Graph driveItemVersion resource (https://learn.microsoft.com/en-us/graph/api/resources/driveitemversion). The `publication` facet is not supported, OpenCloud has no checkout / publish workflow.
type DriveItemVersion struct {
// The ID of the version. Read-only.
Id *string `json:"id,omitempty"`
LastModifiedBy *IdentitySet `json:"lastModifiedBy,omitempty"`
// Date and time the version was last modified. Read-only.
LastModifiedDateTime *time.Time `json:"lastModifiedDateTime,omitempty" validate:"regexp=^[0-9]{4,}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]([.][0-9]{1,12})?([Zz]|[+-][0-9][0-9]:[0-9][0-9])$"`
// Size of the version content in bytes. Read-only.
Size *int64 `json:"size,omitempty"`
// The content stream of this version. Use the `/content` endpoint of the version to download it.
Content *string `json:"content,omitempty"`
// A pre-authenticated URL that can be used to download the content of this version without providing an Authorization header. The URL is short-lived and cannot be cached. This annotation is only populated when explicitly requested via `$select`, matching the behaviour of the annotation on the driveItem.
MicrosoftGraphDownloadUrl *string `json:"@microsoft.graph.downloadUrl,omitempty"`
}
// NewDriveItemVersion instantiates a new DriveItemVersion 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 NewDriveItemVersion() *DriveItemVersion {
this := DriveItemVersion{}
return &this
}
// NewDriveItemVersionWithDefaults instantiates a new DriveItemVersion 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 NewDriveItemVersionWithDefaults() *DriveItemVersion {
this := DriveItemVersion{}
return &this
}
// GetId returns the Id field value if set, zero value otherwise.
func (o *DriveItemVersion) GetId() string {
if o == nil || IsNil(o.Id) {
var ret string
return ret
}
return *o.Id
}
// GetIdOk returns a tuple with the Id field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *DriveItemVersion) GetIdOk() (*string, bool) {
if o == nil || IsNil(o.Id) {
return nil, false
}
return o.Id, true
}
// HasId returns a boolean if a field has been set.
func (o *DriveItemVersion) HasId() bool {
if o != nil && !IsNil(o.Id) {
return true
}
return false
}
// SetId gets a reference to the given string and assigns it to the Id field.
func (o *DriveItemVersion) SetId(v string) {
o.Id = &v
}
// GetLastModifiedBy returns the LastModifiedBy field value if set, zero value otherwise.
func (o *DriveItemVersion) GetLastModifiedBy() IdentitySet {
if o == nil || IsNil(o.LastModifiedBy) {
var ret IdentitySet
return ret
}
return *o.LastModifiedBy
}
// GetLastModifiedByOk returns a tuple with the LastModifiedBy field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *DriveItemVersion) GetLastModifiedByOk() (*IdentitySet, bool) {
if o == nil || IsNil(o.LastModifiedBy) {
return nil, false
}
return o.LastModifiedBy, true
}
// HasLastModifiedBy returns a boolean if a field has been set.
func (o *DriveItemVersion) HasLastModifiedBy() bool {
if o != nil && !IsNil(o.LastModifiedBy) {
return true
}
return false
}
// SetLastModifiedBy gets a reference to the given IdentitySet and assigns it to the LastModifiedBy field.
func (o *DriveItemVersion) SetLastModifiedBy(v IdentitySet) {
o.LastModifiedBy = &v
}
// GetLastModifiedDateTime returns the LastModifiedDateTime field value if set, zero value otherwise.
func (o *DriveItemVersion) GetLastModifiedDateTime() time.Time {
if o == nil || IsNil(o.LastModifiedDateTime) {
var ret time.Time
return ret
}
return *o.LastModifiedDateTime
}
// GetLastModifiedDateTimeOk returns a tuple with the LastModifiedDateTime field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *DriveItemVersion) GetLastModifiedDateTimeOk() (*time.Time, bool) {
if o == nil || IsNil(o.LastModifiedDateTime) {
return nil, false
}
return o.LastModifiedDateTime, true
}
// HasLastModifiedDateTime returns a boolean if a field has been set.
func (o *DriveItemVersion) HasLastModifiedDateTime() bool {
if o != nil && !IsNil(o.LastModifiedDateTime) {
return true
}
return false
}
// SetLastModifiedDateTime gets a reference to the given time.Time and assigns it to the LastModifiedDateTime field.
func (o *DriveItemVersion) SetLastModifiedDateTime(v time.Time) {
o.LastModifiedDateTime = &v
}
// GetSize returns the Size field value if set, zero value otherwise.
func (o *DriveItemVersion) GetSize() int64 {
if o == nil || IsNil(o.Size) {
var ret int64
return ret
}
return *o.Size
}
// GetSizeOk returns a tuple with the Size field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *DriveItemVersion) GetSizeOk() (*int64, bool) {
if o == nil || IsNil(o.Size) {
return nil, false
}
return o.Size, true
}
// HasSize returns a boolean if a field has been set.
func (o *DriveItemVersion) HasSize() bool {
if o != nil && !IsNil(o.Size) {
return true
}
return false
}
// SetSize gets a reference to the given int64 and assigns it to the Size field.
func (o *DriveItemVersion) SetSize(v int64) {
o.Size = &v
}
// GetContent returns the Content field value if set, zero value otherwise.
func (o *DriveItemVersion) GetContent() string {
if o == nil || IsNil(o.Content) {
var ret string
return ret
}
return *o.Content
}
// GetContentOk returns a tuple with the Content field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *DriveItemVersion) GetContentOk() (*string, bool) {
if o == nil || IsNil(o.Content) {
return nil, false
}
return o.Content, true
}
// HasContent returns a boolean if a field has been set.
func (o *DriveItemVersion) HasContent() bool {
if o != nil && !IsNil(o.Content) {
return true
}
return false
}
// SetContent gets a reference to the given string and assigns it to the Content field.
func (o *DriveItemVersion) SetContent(v string) {
o.Content = &v
}
// GetMicrosoftGraphDownloadUrl returns the MicrosoftGraphDownloadUrl field value if set, zero value otherwise.
func (o *DriveItemVersion) 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 *DriveItemVersion) 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 *DriveItemVersion) 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 *DriveItemVersion) SetMicrosoftGraphDownloadUrl(v string) {
o.MicrosoftGraphDownloadUrl = &v
}
func (o DriveItemVersion) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
return []byte{}, err
}
return json.Marshal(toSerialize)
}
func (o DriveItemVersion) ToMap() (map[string]interface{}, error) {
toSerialize := map[string]interface{}{}
if !IsNil(o.Id) {
toSerialize["id"] = o.Id
}
if !IsNil(o.LastModifiedBy) {
toSerialize["lastModifiedBy"] = o.LastModifiedBy
}
if !IsNil(o.LastModifiedDateTime) {
toSerialize["lastModifiedDateTime"] = o.LastModifiedDateTime
}
if !IsNil(o.Size) {
toSerialize["size"] = o.Size
}
if !IsNil(o.Content) {
toSerialize["content"] = o.Content
}
if !IsNil(o.MicrosoftGraphDownloadUrl) {
toSerialize["@microsoft.graph.downloadUrl"] = o.MicrosoftGraphDownloadUrl
}
return toSerialize, nil
}
type NullableDriveItemVersion struct {
value *DriveItemVersion
isSet bool
}
func (v NullableDriveItemVersion) Get() *DriveItemVersion {
return v.value
}
func (v *NullableDriveItemVersion) Set(val *DriveItemVersion) {
v.value = val
v.isSet = true
}
func (v NullableDriveItemVersion) IsSet() bool {
return v.isSet
}
func (v *NullableDriveItemVersion) Unset() {
v.value = nil
v.isSet = false
}
func NewNullableDriveItemVersion(val *DriveItemVersion) *NullableDriveItemVersion {
return &NullableDriveItemVersion{value: val, isSet: true}
}
func (v NullableDriveItemVersion) MarshalJSON() ([]byte, error) {
return json.Marshal(v.value)
}
func (v *NullableDriveItemVersion) UnmarshalJSON(src []byte) error {
v.isSet = true
return json.Unmarshal(src, &v.value)
}