Merge pull request #2962 from owncloud/graph-reads-space-yaml

Read additional graph drive prop from space.yaml
This commit is contained in:
Jörn Friedrich Dreyer
2022-01-19 11:34:57 +01:00
committed by GitHub
23 changed files with 1405 additions and 160 deletions
+3 -1
View File
@@ -24,7 +24,9 @@ docs-generate: config-docs-generate
include ../.make/generate.mk
.PHONY: ci-go-generate
ci-go-generate: # CI runs ci-node-generate automatically before this target
ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this target
$(MOCKERY) --dir pkg/service/v0 --case underscore --name GatewayClient
$(MOCKERY) --dir pkg/service/v0 --case underscore --name HTTPClient
.PHONY: ci-node-generate
ci-node-generate:
+289
View File
@@ -0,0 +1,289 @@
// Code generated by mockery v2.9.4. DO NOT EDIT.
package mocks
import (
context "context"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
grpc "google.golang.org/grpc"
mock "github.com/stretchr/testify/mock"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
// GatewayClient is an autogenerated mock type for the GatewayClient type
type GatewayClient struct {
mock.Mock
}
// CreateStorageSpace provides a mock function with given fields: ctx, in, opts
func (_m *GatewayClient) CreateStorageSpace(ctx context.Context, in *providerv1beta1.CreateStorageSpaceRequest, opts ...grpc.CallOption) (*providerv1beta1.CreateStorageSpaceResponse, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, in)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *providerv1beta1.CreateStorageSpaceResponse
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.CreateStorageSpaceRequest, ...grpc.CallOption) *providerv1beta1.CreateStorageSpaceResponse); ok {
r0 = rf(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*providerv1beta1.CreateStorageSpaceResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.CreateStorageSpaceRequest, ...grpc.CallOption) error); ok {
r1 = rf(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteStorageSpace provides a mock function with given fields: ctx, in, opts
func (_m *GatewayClient) DeleteStorageSpace(ctx context.Context, in *providerv1beta1.DeleteStorageSpaceRequest, opts ...grpc.CallOption) (*providerv1beta1.DeleteStorageSpaceResponse, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, in)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *providerv1beta1.DeleteStorageSpaceResponse
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.DeleteStorageSpaceRequest, ...grpc.CallOption) *providerv1beta1.DeleteStorageSpaceResponse); ok {
r0 = rf(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*providerv1beta1.DeleteStorageSpaceResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.DeleteStorageSpaceRequest, ...grpc.CallOption) error); ok {
r1 = rf(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetHome provides a mock function with given fields: ctx, in, opts
func (_m *GatewayClient) GetHome(ctx context.Context, in *providerv1beta1.GetHomeRequest, opts ...grpc.CallOption) (*providerv1beta1.GetHomeResponse, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, in)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *providerv1beta1.GetHomeResponse
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.GetHomeRequest, ...grpc.CallOption) *providerv1beta1.GetHomeResponse); ok {
r0 = rf(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*providerv1beta1.GetHomeResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.GetHomeRequest, ...grpc.CallOption) error); ok {
r1 = rf(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetQuota provides a mock function with given fields: ctx, in, opts
func (_m *GatewayClient) GetQuota(ctx context.Context, in *gatewayv1beta1.GetQuotaRequest, opts ...grpc.CallOption) (*providerv1beta1.GetQuotaResponse, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, in)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *providerv1beta1.GetQuotaResponse
if rf, ok := ret.Get(0).(func(context.Context, *gatewayv1beta1.GetQuotaRequest, ...grpc.CallOption) *providerv1beta1.GetQuotaResponse); ok {
r0 = rf(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*providerv1beta1.GetQuotaResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *gatewayv1beta1.GetQuotaRequest, ...grpc.CallOption) error); ok {
r1 = rf(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// InitiateFileDownload provides a mock function with given fields: ctx, in, opts
func (_m *GatewayClient) InitiateFileDownload(ctx context.Context, in *providerv1beta1.InitiateFileDownloadRequest, opts ...grpc.CallOption) (*gatewayv1beta1.InitiateFileDownloadResponse, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, in)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *gatewayv1beta1.InitiateFileDownloadResponse
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.InitiateFileDownloadRequest, ...grpc.CallOption) *gatewayv1beta1.InitiateFileDownloadResponse); ok {
r0 = rf(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*gatewayv1beta1.InitiateFileDownloadResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.InitiateFileDownloadRequest, ...grpc.CallOption) error); ok {
r1 = rf(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ListContainer provides a mock function with given fields: ctx, in, opts
func (_m *GatewayClient) ListContainer(ctx context.Context, in *providerv1beta1.ListContainerRequest, opts ...grpc.CallOption) (*providerv1beta1.ListContainerResponse, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, in)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *providerv1beta1.ListContainerResponse
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ListContainerRequest, ...grpc.CallOption) *providerv1beta1.ListContainerResponse); ok {
r0 = rf(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*providerv1beta1.ListContainerResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ListContainerRequest, ...grpc.CallOption) error); ok {
r1 = rf(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ListStorageSpaces provides a mock function with given fields: ctx, in, opts
func (_m *GatewayClient) ListStorageSpaces(ctx context.Context, in *providerv1beta1.ListStorageSpacesRequest, opts ...grpc.CallOption) (*providerv1beta1.ListStorageSpacesResponse, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, in)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *providerv1beta1.ListStorageSpacesResponse
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ListStorageSpacesRequest, ...grpc.CallOption) *providerv1beta1.ListStorageSpacesResponse); ok {
r0 = rf(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*providerv1beta1.ListStorageSpacesResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ListStorageSpacesRequest, ...grpc.CallOption) error); ok {
r1 = rf(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Stat provides a mock function with given fields: ctx, in, opts
func (_m *GatewayClient) Stat(ctx context.Context, in *providerv1beta1.StatRequest, opts ...grpc.CallOption) (*providerv1beta1.StatResponse, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, in)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *providerv1beta1.StatResponse
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.StatRequest, ...grpc.CallOption) *providerv1beta1.StatResponse); ok {
r0 = rf(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*providerv1beta1.StatResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.StatRequest, ...grpc.CallOption) error); ok {
r1 = rf(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateStorageSpace provides a mock function with given fields: ctx, in, opts
func (_m *GatewayClient) UpdateStorageSpace(ctx context.Context, in *providerv1beta1.UpdateStorageSpaceRequest, opts ...grpc.CallOption) (*providerv1beta1.UpdateStorageSpaceResponse, error) {
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []interface{}
_ca = append(_ca, ctx, in)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
var r0 *providerv1beta1.UpdateStorageSpaceResponse
if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.UpdateStorageSpaceRequest, ...grpc.CallOption) *providerv1beta1.UpdateStorageSpaceResponse); ok {
r0 = rf(ctx, in, opts...)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*providerv1beta1.UpdateStorageSpaceResponse)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.UpdateStorageSpaceRequest, ...grpc.CallOption) error); ok {
r1 = rf(ctx, in, opts...)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
+37
View File
@@ -0,0 +1,37 @@
// Code generated by mockery v2.9.4. DO NOT EDIT.
package mocks
import (
http "net/http"
mock "github.com/stretchr/testify/mock"
)
// HTTPClient is an autogenerated mock type for the HTTPClient type
type HTTPClient struct {
mock.Mock
}
// Do provides a mock function with given fields: req
func (_m *HTTPClient) Do(req *http.Request) (*http.Response, error) {
ret := _m.Called(req)
var r0 *http.Response
if rf, ok := ret.Get(0).(func(*http.Request) *http.Response); ok {
r0 = rf(req)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*http.Response)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*http.Request) error); ok {
r1 = rf(req)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
+5 -3
View File
@@ -28,9 +28,11 @@ type Config struct {
}
type Spaces struct {
WebDavBase string `ocisConfig:"webdav_base" env:"OCIS_URL;GRAPH_SPACES_WEBDAV_BASE"`
WebDavPath string `ocisConfig:"webdav_path" env:"GRAPH_SPACES_WEBDAV_PATH"`
DefaultQuota string `ocisConfig:"default_quota" env:"GRAPH_SPACES_DEFAULT_QUOTA"`
WebDavBase string `ocisConfig:"webdav_base" env:"OCIS_URL;GRAPH_SPACES_WEBDAV_BASE"`
WebDavPath string `ocisConfig:"webdav_path" env:"GRAPH_SPACES_WEBDAV_PATH"`
DefaultQuota string `ocisConfig:"default_quota" env:"GRAPH_SPACES_DEFAULT_QUOTA"`
Insecure bool `ocisConfig:"insecure" env:"OCIS_INSECURE;GRAPH_SPACES_INSECURE"`
ExtendedSpacePropertiesCacheTTL int `ocisConfig:"extended_space_properties_cache_ttl" env:"GRAPH_SPACES_EXTENDED_SPACE_PROPERTIES_CACHE_TTL"`
}
type LDAP struct {
+1
View File
@@ -24,6 +24,7 @@ func DefaultConfig() *Config {
WebDavBase: "https://localhost:9200",
WebDavPath: "/dav/spaces/",
DefaultQuota: "1000000000",
Insecure: false,
},
Identity: Identity{
Backend: "cs3",
+144
View File
@@ -0,0 +1,144 @@
package svc
import (
"context"
"fmt"
"net/http"
"path"
"time"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/pkg/utils"
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/graph/pkg/service/v0/errorcode"
)
// GetRootDriveChildren implements the Service interface.
func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
g.logger.Info().Msg("Calling GetRootDriveChildren")
ctx := r.Context()
client := g.GetGatewayClient()
res, err := client.GetHome(ctx, &storageprovider.GetHomeRequest{})
switch {
case err != nil:
g.logger.Error().Err(err).Msg("error sending get home grpc request")
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
return
case res.Status.Code != cs3rpc.Code_CODE_OK:
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message)
return
}
g.logger.Error().Err(err).Msg("error sending get home grpc request")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
return
}
lRes, err := client.ListContainer(ctx, &storageprovider.ListContainerRequest{
Ref: &storageprovider.Reference{
Path: res.Path,
},
})
switch {
case err != nil:
g.logger.Error().Err(err).Msg("error sending list container grpc request")
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
return
case res.Status.Code != cs3rpc.Code_CODE_OK:
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message)
return
}
if res.Status.Code == cs3rpc.Code_CODE_PERMISSION_DENIED {
// TODO check if we should return 404 to not disclose existing items
errorcode.AccessDenied.Render(w, r, http.StatusForbidden, res.Status.Message)
return
}
g.logger.Error().Err(err).Msg("error sending list container grpc request")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
return
}
files, err := formatDriveItems(lRes.Infos)
if err != nil {
g.logger.Error().Err(err).Msg("error encoding response as json")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, &listResponse{Value: files})
}
func (g Graph) getDriveItem(ctx context.Context, root *storageprovider.ResourceId, relativePath string) (*libregraph.DriveItem, error) {
client := g.GetGatewayClient()
ref := &storageprovider.Reference{
ResourceId: root,
// the path is always relative to the root of the drive, not the location of the .config/ocis/space.yaml file
Path: utils.MakeRelativePath(relativePath),
}
res, err := client.Stat(ctx, &storageprovider.StatRequest{Ref: ref})
if err != nil {
return nil, err
}
if res.Status.Code != cs3rpc.Code_CODE_OK {
return nil, fmt.Errorf("could not stat %s: %s", ref, res.Status.Message)
}
return cs3ResourceToDriveItem(res.Info)
}
func formatDriveItems(mds []*storageprovider.ResourceInfo) ([]*libregraph.DriveItem, error) {
responses := make([]*libregraph.DriveItem, 0, len(mds))
for i := range mds {
res, err := cs3ResourceToDriveItem(mds[i])
if err != nil {
return nil, err
}
responses = append(responses, res)
}
return responses, nil
}
func cs3TimestampToTime(t *types.Timestamp) time.Time {
return time.Unix(int64(t.Seconds), int64(t.Nanos))
}
func cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) {
size := new(int64)
*size = int64(res.Size) // TODO lurking overflow: make size of libregraph drive item use uint64
driveItem := &libregraph.DriveItem{
Id: &res.Id.OpaqueId,
Size: size,
}
if name := path.Base(res.Path); name != "" {
driveItem.Name = &name
}
if res.Etag != "" {
driveItem.ETag = &res.Etag
}
if res.Mtime != nil {
lastModified := cs3TimestampToTime(res.Mtime)
driveItem.LastModifiedDateTime = &lastModified
}
if res.Type == storageprovider.ResourceType_RESOURCE_TYPE_FILE && res.MimeType != "" {
// We cannot use a libregraph.File here because the openapi codegenerator autodetects 'File' as a go type ...
driveItem.File = &libregraph.OpenGraphFile{
MimeType: &res.MimeType,
}
}
if res.Type == storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER {
driveItem.Folder = &libregraph.Folder{}
}
return driveItem, nil
}
+197 -137
View File
@@ -7,7 +7,7 @@ import (
"math"
"net/http"
"net/url"
"path"
"path/filepath"
"strconv"
"strings"
"time"
@@ -19,13 +19,16 @@ import (
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
ctxpkg "github.com/cs3org/reva/pkg/ctx"
"github.com/cs3org/reva/pkg/rhttp"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/graph/pkg/service/v0/net"
"github.com/owncloud/ocis/ocis-pkg/service/grpc"
sproto "github.com/owncloud/ocis/settings/pkg/proto/v0"
settingsSvc "github.com/owncloud/ocis/settings/pkg/service/v0"
"gopkg.in/yaml.v2"
merrors "go-micro.dev/v4/errors"
)
@@ -43,12 +46,7 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) {
g.logger.Info().Msg("Calling GetDrives")
ctx := r.Context()
client, err := g.GetClient()
if err != nil {
g.logger.Err(err).Msg("error getting grpc client")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
client := g.GetGatewayClient()
permissions := make(map[string]struct{}, 1)
s := sproto.NewPermissionService("com.owncloud.api.settings", grpc.DefaultClient)
@@ -115,70 +113,6 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, &listResponse{Value: files})
}
// GetRootDriveChildren implements the Service interface.
func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) {
g.logger.Info().Msg("Calling GetRootDriveChildren")
ctx := r.Context()
client, err := g.GetClient()
if err != nil {
g.logger.Error().Err(err).Msg("could not get client")
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
res, err := client.GetHome(ctx, &storageprovider.GetHomeRequest{})
switch {
case err != nil:
g.logger.Error().Err(err).Msg("error sending get home grpc request")
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
return
case res.Status.Code != cs3rpc.Code_CODE_OK:
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message)
return
}
g.logger.Error().Err(err).Msg("error sending get home grpc request")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
return
}
lRes, err := client.ListContainer(ctx, &storageprovider.ListContainerRequest{
Ref: &storageprovider.Reference{
Path: res.Path,
},
})
switch {
case err != nil:
g.logger.Error().Err(err).Msg("error sending list container grpc request")
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
return
case res.Status.Code != cs3rpc.Code_CODE_OK:
if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND {
errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message)
return
}
if res.Status.Code == cs3rpc.Code_CODE_PERMISSION_DENIED {
// TODO check if we should return 404 to not disclose existing items
errorcode.AccessDenied.Render(w, r, http.StatusForbidden, res.Status.Message)
return
}
g.logger.Error().Err(err).Msg("error sending list container grpc request")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message)
return
}
files, err := formatDriveItems(lRes.Infos)
if err != nil {
g.logger.Error().Err(err).Msg("error encoding response as json")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, &listResponse{Value: files})
}
// CreateDrive creates a storage drive (space).
func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
us, ok := ctxpkg.ContextGetUser(r.Context())
@@ -198,11 +132,7 @@ func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) {
return
}
client, err := g.GetClient()
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
client := g.GetGatewayClient()
drive := libregraph.Drive{}
if err := json.NewDecoder(r.Body).Decode(&drive); err != nil {
errorcode.GeneralException.Render(w, r, http.StatusBadRequest, "invalid schema definition")
@@ -293,11 +223,7 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
return
}
client, err := g.GetClient()
if err != nil {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
client := g.GetGatewayClient()
updateSpaceRequest := &storageprovider.UpdateStorageSpaceRequest{
// Prepare the object to apply the diff from. The properties on StorageSpace will overwrite
@@ -365,40 +291,46 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, updatedDrive)
}
func cs3TimestampToTime(t *types.Timestamp) time.Time {
return time.Unix(int64(t.Seconds), int64(t.Nanos))
}
func cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) {
size := new(int64)
*size = int64(res.Size) // uint64 -> int :boom:
name := path.Base(res.Path)
driveItem := &libregraph.DriveItem{
Id: &res.Id.OpaqueId,
Name: &name,
ETag: &res.Etag,
Size: size,
}
if res.Mtime != nil {
lastModified := cs3TimestampToTime(res.Mtime)
driveItem.LastModifiedDateTime = &lastModified
}
if res.Type == storageprovider.ResourceType_RESOURCE_TYPE_FILE {
driveItem.File = &libregraph.OpenGraphFile{ // FIXME We cannot use libregraph.File here because the openapi codegenerator autodetects 'File' as a go type ...
MimeType: &res.MimeType,
func (g Graph) formatDrives(ctx context.Context, baseURL *url.URL, mds []*storageprovider.StorageSpace) ([]*libregraph.Drive, error) {
responses := make([]*libregraph.Drive, 0, len(mds))
for _, space := range mds {
res, err := cs3StorageSpaceToDrive(baseURL, space)
if err != nil {
return nil, err
}
}
if res.Type == storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER {
driveItem.Folder = &libregraph.Folder{}
}
return driveItem, nil
}
spaceProperties, err := g.getExtendedSpaceProperties(ctx, space)
if err != nil {
g.logger.Error().Err(err).Interface("space", space).Msg("error reading extendedSpaceProperties")
continue
}
if err == nil {
if spaceProperties.Description != "" {
res.Description = &spaceProperties.Description
}
if len(spaceProperties.Special) > 0 {
s := make([]libregraph.DriveItem, 0, len(spaceProperties.Special))
for name, relativePath := range spaceProperties.Special {
sdi, err := g.getDriveItem(ctx, space.Root, relativePath)
if err != nil {
// TODO cach not found response
g.logger.Debug().Err(err).Interface("space", space).Interface("path", relativePath).Msg("error fetching drive item")
continue
}
n := name // copy the name to a dedicated variable
sdi.SpecialFolder = &libregraph.SpecialFolder{
Name: &n,
}
webdavURL := baseURL.String() + filepath.Join(space.Id.OpaqueId, relativePath)
sdi.WebDavUrl = &webdavURL
func formatDriveItems(mds []*storageprovider.ResourceInfo) ([]*libregraph.DriveItem, error) {
responses := make([]*libregraph.DriveItem, 0, len(mds))
for i := range mds {
res, err := cs3ResourceToDriveItem(mds[i])
// TODO cache until ./.config/ocis/space.yaml file changes
s = append(s, *sdi)
}
res.Special = &s
}
}
// TODO this overwrites the quota that might already have been mapped in cs3StorageSpaceToDrive above ... move this into the cs3StorageSpaceToDrive method?
res.Quota, err = g.getDriveQuota(ctx, space)
if err != nil {
return nil, err
}
@@ -463,29 +395,8 @@ func cs3StorageSpaceToDrive(baseURL *url.URL, space *storageprovider.StorageSpac
return drive, nil
}
func (g Graph) formatDrives(ctx context.Context, baseURL *url.URL, mds []*storageprovider.StorageSpace) ([]*libregraph.Drive, error) {
responses := make([]*libregraph.Drive, 0, len(mds))
for i := range mds {
res, err := cs3StorageSpaceToDrive(baseURL, mds[i])
if err != nil {
return nil, err
}
res.Quota, err = g.getDriveQuota(ctx, mds[i])
if err != nil {
return nil, err
}
responses = append(responses, res)
}
return responses, nil
}
func (g Graph) getDriveQuota(ctx context.Context, space *storageprovider.StorageSpace) (*libregraph.Quota, error) {
client, err := g.GetClient()
if err != nil {
g.logger.Error().Err(err).Msg("error creating grpc client")
return nil, err
}
client := g.GetGatewayClient()
req := &gateway.GetQuotaRequest{
Ref: &storageprovider.Reference{
@@ -499,13 +410,13 @@ func (g Graph) getDriveQuota(ctx context.Context, space *storageprovider.Storage
res, err := client.GetQuota(ctx, req)
switch {
case err != nil:
g.logger.Error().Err(err).Msg("error sending get quota grpc request")
g.logger.Error().Err(err).Msg("could not call GetQuota")
return nil, nil
case res.Status.Code == cs3rpc.Code_CODE_UNIMPLEMENTED:
// TODO well duh
return nil, nil
case res.Status.Code != cs3rpc.Code_CODE_OK:
g.logger.Error().Err(err).Msg("error sending sending get quota grpc request")
g.logger.Error().Err(err).Msg("error sending get quota grpc request")
return nil, err
}
@@ -524,6 +435,155 @@ func (g Graph) getDriveQuota(ctx context.Context, space *storageprovider.Storage
return &qta, nil
}
// ExtendedSpaceProperties are stored in a file
type ExtendedSpaceProperties struct {
Version string `yaml:"version" json:"version"`
Description string `yaml:"description" json:"description"`
// map of {name} -> {relative path to resource}, eg:
// readme -> readme.md
// image -> .config/ocis/space.png
Special map[string]string `yaml:"special" json:"special"`
}
// generates a space root stat cache key used to detect changes in a space
func spaceRootStatKey(id *storageprovider.ResourceId) string {
if id == nil || id.StorageId == "" || id.OpaqueId == "" {
return ""
}
return "sid:" + id.StorageId + "!oid:" + id.OpaqueId
}
type spacePropertiesEntry struct {
spaceProperties ExtendedSpaceProperties
rootMtime *types.Timestamp
}
func (g Graph) getExtendedSpaceProperties(ctx context.Context, space *storageprovider.StorageSpace) (*ExtendedSpaceProperties, error) {
// if the root is older or equal to our cache we can reuse the cached extended spaces properties
if syc, err := g.spacePropertiesCache.Get(spaceRootStatKey(space.Root)); err == nil {
if spe, ok := syc.(spacePropertiesEntry); ok {
if spe.rootMtime != nil && space.Mtime != nil {
if spe.rootMtime.Seconds > space.Mtime.Seconds { // second precision is good enough
return &spe.spaceProperties, nil
}
}
}
}
client := g.GetGatewayClient()
dlReq := &storageprovider.InitiateFileDownloadRequest{
Ref: &storageprovider.Reference{
ResourceId: &storageprovider.ResourceId{
StorageId: space.Root.StorageId,
OpaqueId: space.Root.OpaqueId,
},
Path: "./.config/ocis/space.yaml",
// TODO what if a public share should have a readme and an image?
// should we just default to a ./Readme.md and ./folder.png/jpg?
// what existing conventions could we use? .desktop file? .env file?
// how should users set a README fo public link file shares? They only point to a file, not a folder that could contain a readme and image
// should weo reuse the readme and image of the space that contains the file shared via link?
},
}
//ctx = metadata.AppendToOutgoingContext(ctx, headers.IfModifiedSince, "TODO grpc has no official cache headers")
// FIXME how can clients retrieve a file just by id?
// The drive Item does currently not have a relative path ...
// so clients would have to make a request by id ... but webdav cannot do that ...
// TODO initiate file download only if the etag does not match
rsp, err := client.InitiateFileDownload(ctx, dlReq)
if err != nil {
return nil, err
}
switch rsp.Status.Code {
case cs3rpc.Code_CODE_OK:
// continue
case cs3rpc.Code_CODE_NOT_FOUND:
// cache an empty instance
spacePropertiesEntry := spacePropertiesEntry{
spaceProperties: ExtendedSpaceProperties{},
rootMtime: space.Mtime,
}
if err := g.spacePropertiesCache.SetWithTTL(spaceRootStatKey(space.Root), spacePropertiesEntry, time.Second*time.Duration(g.config.Spaces.ExtendedSpacePropertiesCacheTTL)); err != nil {
g.logger.Error().Err(err).Msg("could not cache extended space properties")
}
return &spacePropertiesEntry.spaceProperties, nil
default:
return nil, fmt.Errorf("could not initiate download of %s: %s", dlReq.Ref.Path, rsp.Status.Message)
}
var ep, tk string
for _, p := range rsp.Protocols {
if p.Protocol == "spaces" {
ep, tk = p.DownloadEndpoint, p.Token
}
}
if ep == "" {
return nil, fmt.Errorf("space does not support the spaces download protocol")
}
httpReq, err := rhttp.NewRequest(ctx, http.MethodGet, ep, nil)
if err != nil {
return nil, err
}
httpReq.Header.Set(net.HeaderTokenTransport, tk)
httpClient := g.GetHTTPClient()
resp, err := httpClient.Do(httpReq) // nolint:bodyclose
if err != nil {
return nil, err
}
switch resp.StatusCode {
case http.StatusOK:
// continue
case http.StatusNotFound:
// cache an empty instance
spacePropertiesEntry := spacePropertiesEntry{
spaceProperties: ExtendedSpaceProperties{},
rootMtime: space.Mtime,
}
if err := g.spacePropertiesCache.SetWithTTL(spaceRootStatKey(space.Root), spacePropertiesEntry, time.Second*time.Duration(g.config.Spaces.ExtendedSpacePropertiesCacheTTL)); err != nil {
g.logger.Error().Err(err).Msg("could not cache extended space properties")
}
return &spacePropertiesEntry.spaceProperties, nil
default:
return nil, fmt.Errorf("could not get the .space.yaml. Request returned with statuscode %d ", resp.StatusCode)
}
spaceProperties := ExtendedSpaceProperties{}
if err := yaml.NewDecoder(resp.Body).Decode(&spaceProperties); err != nil {
g.logger.Debug().Err(err).Msg("invalid space yaml, ignoring")
// cache an empty instance
// TODO insert an 'invalid yaml' item? how can we return an error to the user?
spacePropertiesEntry := spacePropertiesEntry{
spaceProperties: ExtendedSpaceProperties{},
rootMtime: space.Mtime,
}
if err := g.spacePropertiesCache.SetWithTTL(spaceRootStatKey(space.Root), spacePropertiesEntry, time.Second*time.Duration(g.config.Spaces.ExtendedSpacePropertiesCacheTTL)); err != nil {
g.logger.Error().Err(err).Msg("could not cache extended space properties")
}
return &spacePropertiesEntry.spaceProperties, nil
}
// cache properties
spacePropertiesEntry := spacePropertiesEntry{
spaceProperties: spaceProperties,
rootMtime: space.Mtime,
}
if err := g.spacePropertiesCache.SetWithTTL(spaceRootStatKey(space.Root), spacePropertiesEntry, time.Second*time.Duration(g.config.Spaces.ExtendedSpacePropertiesCacheTTL)); err != nil {
g.logger.Error().Err(err).Msg("could not cache extended space properties")
}
return &spaceProperties, nil
}
func calculateQuotaState(total int64, used int64) (state string) {
percent := (float64(used) / float64(total)) * 100
+60 -7
View File
@@ -1,22 +1,70 @@
package svc
import (
"context"
"net/http"
"github.com/ReneKroon/ttlcache/v2"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/owncloud/ocis/graph/pkg/config"
"github.com/owncloud/ocis/graph/pkg/identity"
"github.com/owncloud/ocis/ocis-pkg/log"
"google.golang.org/grpc"
)
//go:generate make generate
// GatewayClient is the subset of the gateway.GatewayAPIClient that is being used to interact with the gateway
type GatewayClient interface {
//gateway.GatewayAPIClient
// Returns the home path for the given authenticated user.
// When a user has access to multiple storage providers, one of them is the home.
GetHome(ctx context.Context, in *provider.GetHomeRequest, opts ...grpc.CallOption) (*provider.GetHomeResponse, error)
// Returns a list of resource information
// for the provided reference.
// MUST return CODE_NOT_FOUND if the reference does not exists.
ListContainer(ctx context.Context, in *provider.ListContainerRequest, opts ...grpc.CallOption) (*provider.ListContainerResponse, error)
// Returns the resource information at the provided reference.
// MUST return CODE_NOT_FOUND if the reference does not exist.
Stat(ctx context.Context, in *provider.StatRequest, opts ...grpc.CallOption) (*provider.StatResponse, error)
// Initiates the download of a file using an
// out-of-band data transfer mechanism.
InitiateFileDownload(ctx context.Context, in *provider.InitiateFileDownloadRequest, opts ...grpc.CallOption) (*gateway.InitiateFileDownloadResponse, error)
// Creates a storage space.
CreateStorageSpace(ctx context.Context, in *provider.CreateStorageSpaceRequest, opts ...grpc.CallOption) (*provider.CreateStorageSpaceResponse, error)
// Lists storage spaces.
ListStorageSpaces(ctx context.Context, in *provider.ListStorageSpacesRequest, opts ...grpc.CallOption) (*provider.ListStorageSpacesResponse, error)
// Updates a storage space.
UpdateStorageSpace(ctx context.Context, in *provider.UpdateStorageSpaceRequest, opts ...grpc.CallOption) (*provider.UpdateStorageSpaceResponse, error)
// Deletes a storage space.
DeleteStorageSpace(ctx context.Context, in *provider.DeleteStorageSpaceRequest, opts ...grpc.CallOption) (*provider.DeleteStorageSpaceResponse, error)
// Returns the quota available under the provided
// reference.
// MUST return CODE_NOT_FOUND if the reference does not exist
// MUST return CODE_RESOURCE_EXHAUSTED on exceeded quota limits.
GetQuota(ctx context.Context, in *gateway.GetQuotaRequest, opts ...grpc.CallOption) (*provider.GetQuotaResponse, error)
}
// HTTPClient is the subset of the http.Client that is being used to interact with the download gateway
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// GetGatewayServiceClientFunc is a callback used to pass in a mock during testing
type GetGatewayServiceClientFunc func() (GatewayClient, error)
// Graph defines implements the business logic for Service.
type Graph struct {
config *config.Config
mux *chi.Mux
logger *log.Logger
identityBackend identity.Backend
config *config.Config
mux *chi.Mux
logger *log.Logger
identityBackend identity.Backend
gatewayClient GatewayClient
httpClient HTTPClient
spacePropertiesCache *ttlcache.Cache
}
// ServeHTTP implements the Service interface.
@@ -25,8 +73,13 @@ func (g Graph) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
// GetClient returns a gateway client to talk to reva
func (g Graph) GetClient() (gateway.GatewayAPIClient, error) {
return pool.GetGatewayServiceClient(g.config.Reva.Address)
func (g Graph) GetGatewayClient() GatewayClient {
return g.gatewayClient
}
// GetClient returns a gateway client to talk to reva
func (g Graph) GetHTTPClient() HTTPClient {
return g.httpClient
}
type listResponse struct {
+13
View File
@@ -0,0 +1,13 @@
package svc_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func TestGraph(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Graph Suite")
}
+233
View File
@@ -0,0 +1,233 @@
package svc_test
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/pkg/rgrpc/status"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/owncloud/ocis/graph/mocks"
"github.com/owncloud/ocis/graph/pkg/config"
service "github.com/owncloud/ocis/graph/pkg/service/v0"
"github.com/stretchr/testify/mock"
"google.golang.org/grpc"
)
var _ = Describe("Graph", func() {
var (
svc service.Service
gatewayClient *mocks.GatewayClient
httpClient *mocks.HTTPClient
ctx context.Context
)
JustBeforeEach(func() {
ctx = context.Background()
gatewayClient = &mocks.GatewayClient{}
httpClient = &mocks.HTTPClient{}
svc = service.NewService(
service.Config(config.DefaultConfig()),
service.WithGatewayClient(gatewayClient),
service.WithHTTPClient(httpClient),
)
})
Describe("NewService", func() {
It("returns a service", func() {
Expect(svc).ToNot(BeNil())
})
})
Describe("drive", func() {
It("can list an empty list of spaces", func() {
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*provider.StorageSpace{},
}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives", nil)
rr := httptest.NewRecorder()
svc.GetDrives(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
})
It("can list a space without owner", func() {
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*provider.StorageSpace{
{
Id: &provider.StorageSpaceId{OpaqueId: "aspaceid"},
SpaceType: "aspacetype",
Root: &provider.ResourceId{
StorageId: "aspaceid",
OpaqueId: "anopaqueid",
},
Name: "aspacename",
},
},
}, nil)
gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Return(&gateway.InitiateFileDownloadResponse{
Status: status.NewNotFound(ctx, "not found"),
}, nil)
gatewayClient.On("GetQuota", mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{
Status: status.NewUnimplemented(ctx, fmt.Errorf("not supported"), "not supported"),
}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives", nil)
rr := httptest.NewRecorder()
svc.GetDrives(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
body, _ := io.ReadAll(rr.Body)
Expect(body).To(MatchJSON(`
{
"value":[
{
"driveType":"aspacetype",
"id":"aspaceid!anopaqueid",
"name":"aspacename",
"root":{
"id":"aspaceid!anopaqueid",
"webDavUrl":"https://localhost:9200/dav/spaces/aspaceid!anopaqueid"
}
}
]
}
`))
})
It("can list a space with extended properties from a space.yaml", func() {
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*provider.StorageSpace{
{
Id: &provider.StorageSpaceId{OpaqueId: "aspaceid"},
SpaceType: "aspacetype",
Root: &provider.ResourceId{
StorageId: "aspaceid",
OpaqueId: "anopaqueid",
},
Name: "aspacename",
},
},
}, nil)
gatewayClient.On("InitiateFileDownload", mock.Anything, mock.Anything).Return(&gateway.InitiateFileDownloadResponse{
Status: status.NewOK(ctx),
Protocols: []*gateway.FileDownloadProtocol{
{
Protocol: "spaces",
DownloadEndpoint: "ignored",
},
},
}, nil)
// mock space.yaml
httpClient.On("Do", mock.Anything, mock.Anything).Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`---
version: "1.0"
description: read from yaml
special:
readme: readme2.md
image: .img/space.png
`)),
}, nil)
gatewayClient.On("GetQuota", mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{
Status: status.NewUnimplemented(ctx, fmt.Errorf("not supported"), "not supported"),
}, nil)
gatewayClient.On("Stat", mock.Anything, mock.Anything).Return(
func(_ context.Context, req *provider.StatRequest, _ ...grpc.CallOption) *provider.StatResponse {
switch req.Ref.GetPath() {
case "./readme2.md":
return &provider.StatResponse{
Status: status.NewOK(ctx),
Info: &provider.ResourceInfo{
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
Path: "readme2.md",
Id: &provider.ResourceId{
StorageId: "aspaceid",
OpaqueId: "readmeid",
},
PermissionSet: &provider.ResourcePermissions{
Stat: true,
},
Size: 10,
},
}
case "./.img/space.png":
return &provider.StatResponse{
Status: status.NewOK(ctx),
Info: &provider.ResourceInfo{
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
Path: "space.png",
Id: &provider.ResourceId{
StorageId: "aspaceid",
OpaqueId: "imageid",
},
PermissionSet: &provider.ResourcePermissions{
Stat: true,
},
Size: 20,
},
}
default:
return &provider.StatResponse{
Status: status.NewNotFound(ctx, "not found"),
}
}
},
nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/drives", nil)
rr := httptest.NewRecorder()
svc.GetDrives(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
body, _ := io.ReadAll(rr.Body)
Expect(body).To(MatchJSON(`
{
"value":[
{
"driveType":"aspacetype",
"id":"aspaceid!anopaqueid",
"name":"aspacename",
"description":"read from yaml",
"root":{
"id":"aspaceid!anopaqueid",
"webDavUrl":"https://localhost:9200/dav/spaces/aspaceid!anopaqueid"
},
"special": [
{
"id": "readmeid",
"name": "readme2.md",
"size": 10,
"specialFolder": {
"name": "readme"
},
"webDavUrl": "https://localhost:9200/dav/spaces/aspaceid/readme2.md"
},
{
"id": "imageid",
"name": "space.png",
"size": 20,
"specialFolder": {
"name": "image"
},
"webDavUrl": "https://localhost:9200/dav/spaces/aspaceid/.img/space.png"
}
]
}
]
}
`))
})
})
})
+5
View File
@@ -53,3 +53,8 @@ func (i instrument) DeleteUser(w http.ResponseWriter, r *http.Request) {
func (i instrument) PatchUser(w http.ResponseWriter, r *http.Request) {
i.next.PatchUser(w, r)
}
// GetDrives implements the Service interface.
func (i instrument) GetDrives(w http.ResponseWriter, r *http.Request) {
i.next.GetDrives(w, r)
}
+5
View File
@@ -53,3 +53,8 @@ func (l logging) DeleteUser(w http.ResponseWriter, r *http.Request) {
func (l logging) PatchUser(w http.ResponseWriter, r *http.Request) {
l.next.PatchUser(w, r)
}
// GetDrives implements the Service interface.
func (l logging) GetDrives(w http.ResponseWriter, r *http.Request) {
l.next.GetDrives(w, r)
}
+9
View File
@@ -0,0 +1,9 @@
package net
const (
// "github.com/cs3org/reva/internal/http/services/datagateway" is internal so we redeclare it here
// HeaderTokenTransport holds the header key for the reva transfer token
HeaderTokenTransport = "X-Reva-Transfer"
// HeaderIfModifiedSince is used to mimic/pass on caching headers when using grpc
HeaderIfModifiedSince = "If-Modified-Since"
)
+19 -3
View File
@@ -12,9 +12,11 @@ type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Config *config.Config
Middleware []func(http.Handler) http.Handler
Logger log.Logger
Config *config.Config
Middleware []func(http.Handler) http.Handler
GatewayClient GatewayClient
HTTPClient HTTPClient
}
// newOptions initializes the available default options.
@@ -48,3 +50,17 @@ func Middleware(val ...func(http.Handler) http.Handler) Option {
o.Middleware = val
}
}
// WithGatewayClient provides a function to set the gateway client option.
func WithGatewayClient(val GatewayClient) Option {
return func(o *Options) {
o.GatewayClient = val
}
}
// WithHTTPClient provides a function to set the http client option.
func WithHTTPClient(val HTTPClient) Option {
return func(o *Options) {
o.HTTPClient = val
}
}
+28 -4
View File
@@ -1,8 +1,11 @@
package svc
import (
"crypto/tls"
"net/http"
"github.com/ReneKroon/ttlcache/v2"
"github.com/cs3org/reva/pkg/rgrpc/todo/pool"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
@@ -21,6 +24,8 @@ type Service interface {
PostUser(http.ResponseWriter, *http.Request)
DeleteUser(http.ResponseWriter, *http.Request)
PatchUser(http.ResponseWriter, *http.Request)
GetDrives(w http.ResponseWriter, r *http.Request)
}
// NewService returns a service implementation for Service.
@@ -54,10 +59,29 @@ func NewService(opts ...Option) Service {
}
svc := Graph{
config: options.Config,
mux: m,
logger: &options.Logger,
identityBackend: backend,
config: options.Config,
mux: m,
logger: &options.Logger,
identityBackend: backend,
spacePropertiesCache: ttlcache.NewCache(),
}
if options.GatewayClient == nil {
var err error
svc.gatewayClient, err = pool.GetGatewayServiceClient(options.Config.Reva.Address)
if err != nil {
options.Logger.Error().Err(err).Msg("Could not get gateway client")
return nil
}
} else {
svc.gatewayClient = options.GatewayClient
}
if options.HTTPClient == nil {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: options.Config.Spaces.Insecure, //nolint:gosec
}
svc.httpClient = &http.Client{}
} else {
svc.httpClient = options.HTTPClient
}
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
+5
View File
@@ -49,3 +49,8 @@ func (t tracing) DeleteUser(w http.ResponseWriter, r *http.Request) {
func (t tracing) PatchUser(w http.ResponseWriter, r *http.Request) {
t.next.PatchUser(w, r)
}
// GetDrives implements the Service interface.
func (t tracing) GetDrives(w http.ResponseWriter, r *http.Request) {
t.next.GetDrives(w, r)
}