mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-09 12:19:08 -04:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be8cd461b4 | ||
|
|
5807eea186 | ||
|
|
0459149e0c | ||
|
|
13da9331f2 |
No files matched your search
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
Generated
Vendored
+126
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+312
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user