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.
This commit is contained in:
Dominik Schmidt committed 2026-09-07 13:00:01 +02:00
1 parent 5807eea186
commit be8cd461b4
5 files changed
+1063

No files matched your search

@@ -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)
})
})
})
+12
View File
@@ -106,6 +106,10 @@ type Service interface { //nolint:interfacebloat
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)
@@ -384,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)
})
})
})
})
})
@@ -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)
}