From 756bdced1d229ae0644ed60d91538325e9b7647a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Thu, 12 Aug 2021 12:52:59 +0200 Subject: [PATCH] [spaces 2/2] /dav/spaces endpoint (#1803) Co-authored-by: David Christofas --- .drone.star | 58 +++++ changelog/unreleased/introduce-dav-spaces.md | 7 + go.sum | 3 - .../grpc/services/gateway/storageprovider.go | 113 ++++++--- .../publicstorageprovider.go | 3 +- .../storageprovider/storageprovider.go | 75 +++--- .../services/dataprovider/dataprovider.go | 1 + internal/http/services/owncloud/ocdav/copy.go | 225 +++++++++++++++++- internal/http/services/owncloud/ocdav/dav.go | 11 + .../http/services/owncloud/ocdav/delete.go | 24 ++ internal/http/services/owncloud/ocdav/get.go | 22 ++ internal/http/services/owncloud/ocdav/head.go | 22 ++ .../http/services/owncloud/ocdav/mkcol.go | 26 +- internal/http/services/owncloud/ocdav/move.go | 49 +++- .../http/services/owncloud/ocdav/ocdav.go | 2 +- .../http/services/owncloud/ocdav/propfind.go | 45 ++++ .../http/services/owncloud/ocdav/proppatch.go | 64 +++++ .../services/owncloud/ocdav/publicfile.go | 2 +- internal/http/services/owncloud/ocdav/put.go | 27 +++ .../http/services/owncloud/ocdav/spaces.go | 132 ++++++++++ internal/http/services/owncloud/ocdav/tus.go | 28 +++ pkg/auth/scope/publicshare.go | 2 +- pkg/auth/scope/resourceinfo.go | 13 +- pkg/rhttp/datatx/manager/loader/loader.go | 1 + pkg/rhttp/datatx/manager/simple/simple.go | 2 +- pkg/rhttp/datatx/manager/spaces/spaces.go | 124 ++++++++++ pkg/rhttp/datatx/manager/tus/tus.go | 2 +- pkg/rhttp/datatx/utils/download/download.go | 24 +- pkg/storage/fs/owncloud/owncloud.go | 14 +- pkg/storage/fs/owncloudsql/owncloudsql.go | 17 +- pkg/storage/fs/s3/s3.go | 16 +- pkg/storage/storage.go | 2 +- .../utils/decomposedfs/decomposedfs.go | 45 +++- .../decomposedfs_concurrency_test.go | 5 +- pkg/storage/utils/decomposedfs/grants.go | 2 +- pkg/storage/utils/decomposedfs/lookup.go | 24 +- pkg/storage/utils/decomposedfs/node/node.go | 27 +-- .../utils/decomposedfs/node/node_test.go | 6 +- .../utils/decomposedfs/node/permissions.go | 5 +- .../utils/decomposedfs/testhelpers/helpers.go | 9 +- .../utils/decomposedfs/tree/tree_test.go | 4 +- pkg/storage/utils/decomposedfs/upload.go | 2 +- pkg/storage/utils/eosfs/eosfs.go | 7 +- pkg/storage/utils/localfs/localfs.go | 9 +- pkg/utils/utils.go | 20 ++ 45 files changed, 1168 insertions(+), 153 deletions(-) create mode 100644 changelog/unreleased/introduce-dav-spaces.md create mode 100644 internal/http/services/owncloud/ocdav/spaces.go create mode 100644 pkg/rhttp/datatx/manager/spaces/spaces.go diff --git a/.drone.star b/.drone.star index f296b9333f..c11e9fac7e 100644 --- a/.drone.star +++ b/.drone.star @@ -102,6 +102,7 @@ def main(ctx): release(), litmusOcisOldWebdav(), litmusOcisNewWebdav(), + litmusOcisSpacesDav(), ] + ocisIntegrationTests(6) + s3ngIntegrationTests(12) @@ -517,6 +518,63 @@ def litmusOcisNewWebdav(): ], } +def litmusOcisSpacesDav(): + return { + "kind": "pipeline", + "type": "docker", + "name": "litmus-owncloud-spaces-dav", + "platform": { + "os": "linux", + "arch": "amd64", + }, + "trigger": { + "event": { + "include": [ + "pull_request", + "tag", + ], + }, + }, + "steps": [ + makeStep("build-ci"), + { + "name": "revad-services", + "image": "registry.cern.ch/docker.io/library/golang:1.16", + "detach": True, + "commands": [ + "cd /drone/src/tests/oc-integration-tests/drone/", + "/drone/src/cmd/revad/revad -c frontend.toml &", + "/drone/src/cmd/revad/revad -c gateway.toml &", + "/drone/src/cmd/revad/revad -c storage-home-ocis.toml &", + "/drone/src/cmd/revad/revad -c storage-oc-ocis.toml &", + "/drone/src/cmd/revad/revad -c users.toml", + ] + }, + { + "name": "sleep-for-revad-start", + "image": "registry.cern.ch/docker.io/library/golang:1.16", + "commands":[ + "sleep 5", + ], + }, + { + "name": "litmus-owncloud-spaces-dav", + "image": "registry.cern.ch/docker.io/owncloud/litmus:latest", + "environment": { + "LITMUS_USERNAME": "einstein", + "LITMUS_PASSWORD": "relativity", + "TESTS": "basic http copymove props", + }, + "commands": [ + # The spaceid is randomly generated during the first login so we need this hack to construct the correct url. + "curl -s -k -u einstein:relativity -I http://revad-services:20080/remote.php/dav/files/einstein", + "export LITMUS_URL=http://revad-services:20080/remote.php/dav/spaces/123e4567-e89b-12d3-a456-426655440000!$(ls /drone/src/tmp/reva/data/spaces/personal/)", + "/usr/local/bin/litmus-wrapper", + ] + }, + ], + } + def ocisIntegrationTests(parallelRuns, skipExceptParts = []): pipelines = [] debugPartsEnabled = (len(skipExceptParts) != 0) diff --git a/changelog/unreleased/introduce-dav-spaces.md b/changelog/unreleased/introduce-dav-spaces.md new file mode 100644 index 0000000000..0dade9a49d --- /dev/null +++ b/changelog/unreleased/introduce-dav-spaces.md @@ -0,0 +1,7 @@ +Enhancement: Introduce new webdav spaces endpoint + +Clients can now use a new webdav endpoint `/dav/spaces//relative/path/to/file` to directly access storage spaces. + +The `` can be retrieved using the ListStorageSpaces CS3 api call. + +https://github.com/cs3org/reva/pull/1803 diff --git a/go.sum b/go.sum index cf760b4ea4..2a46e292ff 100644 --- a/go.sum +++ b/go.sum @@ -279,11 +279,8 @@ github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39 github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-plugin v1.4.2 h1:yFvG3ufXXpqiMiZx9HLcaK3XbIqQ1WJFR/F1a2CuVw0= github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index d13c1d9d61..1464954fe6 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -136,16 +136,16 @@ func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSp if id != nil { // query that specific storage provider - parts := strings.SplitN(id.OpaqueId, "!", 2) - if len(parts) != 2 { + storageid, opaqeid, err := utils.SplitStorageSpaceID(id.OpaqueId) + if err != nil { return &provider.ListStorageSpacesResponse{ Status: status.NewInvalidArg(ctx, "space id must be separated by !"), }, nil } res, err := c.GetStorageProviders(ctx, ®istry.GetStorageProvidersRequest{ Ref: &provider.Reference{ResourceId: &provider.ResourceId{ - StorageId: parts[0], // FIXME REFERENCE the StorageSpaceId is a storageid + an opaqueid - OpaqueId: parts[1], + StorageId: storageid, + OpaqueId: opaqeid, }}, }) if err != nil { @@ -267,15 +267,15 @@ func (s *svc) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorag func (s *svc) DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) (*provider.DeleteStorageSpaceResponse, error) { log := appctx.GetLogger(ctx) // TODO: needs to be fixed - parts := strings.SplitN(req.Id.OpaqueId, "!", 2) - if len(parts) != 2 { + storageid, opaqeid, err := utils.SplitStorageSpaceID(req.Id.OpaqueId) + if err != nil { return &provider.DeleteStorageSpaceResponse{ Status: status.NewInvalidArg(ctx, "space id must be separated by !"), }, nil } c, err := s.find(ctx, &provider.Reference{ResourceId: &provider.ResourceId{ - StorageId: parts[0], // FIXME REFERENCE the StorageSpaceId is a storageid + a opaqueid - OpaqueId: parts[1], + StorageId: storageid, + OpaqueId: opaqeid, }}) if err != nil { return &provider.DeleteStorageSpaceResponse{ @@ -307,6 +307,11 @@ func (s *svc) getHome(_ context.Context) string { func (s *svc) InitiateFileDownload(ctx context.Context, req *provider.InitiateFileDownloadRequest) (*gateway.InitiateFileDownloadResponse, error) { log := appctx.GetLogger(ctx) + + if utils.IsRelativeReference(req.Ref) { + return s.initiateFileDownload(ctx, req) + } + p, st := s.getPath(ctx, req.Ref) if st.Code != rpc.Code_CODE_OK { return &gateway.InitiateFileDownloadResponse{ @@ -517,6 +522,9 @@ func (s *svc) initiateFileDownload(ctx context.Context, req *provider.InitiateFi func (s *svc) InitiateFileUpload(ctx context.Context, req *provider.InitiateFileUploadRequest) (*gateway.InitiateFileUploadResponse, error) { log := appctx.GetLogger(ctx) + if utils.IsRelativeReference(req.Ref) { + return s.initiateFileUpload(ctx, req) + } p, st := s.getPath(ctx, req.Ref) if st.Code != rpc.Code_CODE_OK { return &gateway.InitiateFileUploadResponse{ @@ -739,6 +747,11 @@ func (s *svc) GetPath(ctx context.Context, req *provider.GetPathRequest) (*provi func (s *svc) CreateContainer(ctx context.Context, req *provider.CreateContainerRequest) (*provider.CreateContainerResponse, error) { log := appctx.GetLogger(ctx) + + if utils.IsRelativeReference(req.Ref) { + return s.createContainer(ctx, req) + } + p, st := s.getPath(ctx, req.Ref) if st.Code != rpc.Code_CODE_OK { return &provider.CreateContainerResponse{ @@ -1248,14 +1261,18 @@ func (s *svc) stat(ctx context.Context, req *provider.StatRequest) (*provider.St } resPath := req.Ref.GetPath() - if len(providers) == 1 && (resPath == "" || strings.HasPrefix(resPath, providers[0].ProviderPath)) { + if len(providers) == 1 && (utils.IsRelativeReference(req.Ref) || resPath == "" || strings.HasPrefix(resPath, providers[0].ProviderPath)) { c, err := s.getStorageProviderClient(ctx, providers[0]) if err != nil { return &provider.StatResponse{ Status: status.NewInternal(ctx, err, "error connecting to storage provider="+providers[0].Address), }, nil } - return c.Stat(ctx, req) + rsp, err := c.Stat(ctx, req) + if err != nil || rsp.Status.Code != rpc.Code_CODE_OK { + return rsp, err + } + return rsp, nil } return s.statAcrossProviders(ctx, req, providers) @@ -1308,14 +1325,18 @@ func (s *svc) statOnProvider(ctx context.Context, req *provider.StatRequest, res return } - resPath := path.Clean(req.Ref.GetPath()) - newPath := req.Ref.GetPath() - if resPath != "" && !strings.HasPrefix(resPath, p.ProviderPath) { - newPath = p.ProviderPath + if utils.IsAbsoluteReference(req.Ref) { + resPath := path.Clean(req.Ref.GetPath()) + newPath := req.Ref.GetPath() + if resPath != "." && !strings.HasPrefix(resPath, p.ProviderPath) { + newPath = p.ProviderPath + } + req.Ref = &provider.Reference{Path: newPath} } - r, err := c.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{Path: newPath}}) + + r, err := c.Stat(ctx, req) if err != nil { - *e = errors.Wrap(err, fmt.Sprintf("gateway: error calling Stat %s on %+v", newPath, p)) + *e = errors.Wrap(err, fmt.Sprintf("gateway: error calling Stat %s on %+v", req.Ref, p)) return } if res == nil { @@ -1325,6 +1346,11 @@ func (s *svc) statOnProvider(ctx context.Context, req *provider.StatRequest, res } func (s *svc) Stat(ctx context.Context, req *provider.StatRequest) (*provider.StatResponse, error) { + + if utils.IsRelativeReference(req.Ref) { + return s.stat(ctx, req) + } + p, st := s.getPath(ctx, req.Ref, req.ArbitraryMetadataKeys...) if st.Code != rpc.Code_CODE_OK { return &provider.StatResponse{ @@ -1639,6 +1665,7 @@ func (s *svc) listContainer(ctx context.Context, req *provider.ListContainerRequ for _, inf := range infoFromProviders[i] { if indirects[i] { p := inf.Path + // TODO do we need to trim prefix here for relative references? nestedInfos[p] = append(nestedInfos[p], inf) } else { infos = append(infos, inf) @@ -1673,31 +1700,34 @@ func (s *svc) listContainerOnProvider(ctx context.Context, req *provider.ListCon return } - resPath := path.Clean(req.Ref.GetPath()) - if resPath != "" && !strings.HasPrefix(resPath, p.ProviderPath) { - // The path which we're supposed to list encompasses this provider - // so just return the first child and mark it as indirect - rel, err := filepath.Rel(resPath, p.ProviderPath) - if err != nil { - *e = err - return - } - parts := strings.Split(rel, "/") - p := path.Join(resPath, parts[0]) - *ind = true - *res = []*provider.ResourceInfo{ - { - Id: &provider.ResourceId{ - StorageId: "/", - OpaqueId: uuid.New().String(), + if utils.IsAbsoluteReference(req.Ref) { + resPath := path.Clean(req.Ref.GetPath()) + if resPath != "" && !strings.HasPrefix(resPath, p.ProviderPath) { + // The path which we're supposed to list encompasses this provider + // so just return the first child and mark it as indirect + rel, err := filepath.Rel(resPath, p.ProviderPath) + if err != nil { + *e = err + return + } + parts := strings.Split(rel, "/") + p := path.Join(resPath, parts[0]) + *ind = true + *res = []*provider.ResourceInfo{ + { + Id: &provider.ResourceId{ + StorageId: "/", + OpaqueId: uuid.New().String(), + }, + Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, + Path: p, + Size: 0, }, - Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, - Path: p, - Size: 0, - }, + } + return } - return } + r, err := c.ListContainer(ctx, req) if err != nil { *e = errors.Wrap(err, "gateway: error calling ListContainer") @@ -1708,6 +1738,11 @@ func (s *svc) listContainerOnProvider(ctx context.Context, req *provider.ListCon func (s *svc) ListContainer(ctx context.Context, req *provider.ListContainerRequest) (*provider.ListContainerResponse, error) { log := appctx.GetLogger(ctx) + + if utils.IsRelativeReference(req.Ref) { + return s.listContainer(ctx, req) + } + p, st := s.getPath(ctx, req.Ref, req.ArbitraryMetadataKeys...) if st.Code != rpc.Code_CODE_OK { return &provider.ListContainerResponse{ @@ -1896,7 +1931,7 @@ func (s *svc) getPath(ctx context.Context, ref *provider.Reference, keys ...stri return res.Info.Path, res.Status } - if ref.Path != "" { + if utils.IsAbsolutePathReference(ref) { return ref.Path, &rpc.Status{Code: rpc.Code_CODE_OK} } return "", &rpc.Status{Code: rpc.Code_CODE_INTERNAL} diff --git a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go index 62540b81cc..32bf2f485a 100644 --- a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go +++ b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go @@ -34,6 +34,7 @@ import ( "github.com/cs3org/reva/pkg/rgrpc" "github.com/cs3org/reva/pkg/rgrpc/status" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/pkg/utils" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "go.opencensus.io/trace" @@ -577,7 +578,7 @@ func (s *service) unwrap(ctx context.Context, ref *provider.Reference) (token st return "", "", errtypes.BadRequest("need absolute path ref: got " + ref.String()) } - if ref.GetPath() == "" { + if !utils.IsAbsolutePathReference(ref) { // abort, no valid id nor path return "", "", errtypes.BadRequest("invalid ref: " + ref.String()) } diff --git a/internal/grpc/services/storageprovider/storageprovider.go b/internal/grpc/services/storageprovider/storageprovider.go index 5ae6b1a681..87d280a07a 100644 --- a/internal/grpc/services/storageprovider/storageprovider.go +++ b/internal/grpc/services/storageprovider/storageprovider.go @@ -42,6 +42,7 @@ import ( "github.com/cs3org/reva/pkg/rhttp/router" "github.com/cs3org/reva/pkg/storage" "github.com/cs3org/reva/pkg/storage/fs/registry" + "github.com/cs3org/reva/pkg/utils" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "go.opencensus.io/trace" @@ -268,29 +269,32 @@ func (s *service) InitiateFileDownload(ctx context.Context, req *provider.Initia // or ownclouds://data-server.example.org/home/docs/myfile.txt log := appctx.GetLogger(ctx) u := *s.dataServerURL - newRef, err := s.unwrap(ctx, req.Ref) - if err != nil { - return &provider.InitiateFileDownloadResponse{ - Status: status.NewInternal(ctx, err, "error unwrapping path"), - }, nil + log.Info().Str("data-server", u.String()).Interface("ref", req.Ref).Msg("file download") + + protocol := &provider.FileDownloadProtocol{Expose: s.conf.ExposeDataServer} + + if utils.IsRelativeReference(req.Ref) { + protocol.Protocol = "spaces" + u.Path = path.Join(u.Path, "spaces", req.Ref.ResourceId.StorageId+"!"+req.Ref.ResourceId.OpaqueId, req.Ref.Path) + } else { + newRef, err := s.unwrap(ctx, req.Ref) + if err != nil { + return &provider.InitiateFileDownloadResponse{ + Status: status.NewInternal(ctx, err, "error unwrapping path"), + }, nil + } + // Currently, we only support the simple protocol for GET requests + // Once we have multiple protocols, this would be moved to the fs layer + protocol.Protocol = "simple" + u.Path = path.Join(u.Path, "simple", newRef.GetPath()) } - // Currently, we only support the simple protocol for GET requests - // Once we have multiple protocols, this would be moved to the fs layer - u.Path = path.Join(u.Path, "simple", newRef.GetPath()) + protocol.DownloadEndpoint = u.String() - log.Info().Str("data-server", u.String()).Str("fn", req.Ref.GetPath()).Msg("file download") - res := &provider.InitiateFileDownloadResponse{ - Protocols: []*provider.FileDownloadProtocol{ - { - Protocol: "simple", - DownloadEndpoint: u.String(), - Expose: s.conf.ExposeDataServer, - }, - }, - Status: status.NewOK(ctx), - } - return res, nil + return &provider.InitiateFileDownloadResponse{ + Protocols: []*provider.FileDownloadProtocol{protocol}, + Status: status.NewOK(ctx), + }, nil } func (s *service) InitiateFileUpload(ctx context.Context, req *provider.InitiateFileUploadRequest) (*provider.InitiateFileUploadResponse, error) { @@ -337,7 +341,7 @@ func (s *service) InitiateFileUpload(ctx context.Context, req *provider.Initiate st = status.NewNotFound(ctx, "path not found when initiating upload") case errtypes.IsBadRequest, errtypes.IsChecksumMismatch: st = status.NewInvalidArg(ctx, err.Error()) - // TODO TUS uses a custom ChecksumMismatch 460 http status which is in an unnasigned range in + // TODO TUS uses a custom ChecksumMismatch 460 http status which is in an unassigned range in // https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml // maybe 409 conflict is good enough // someone is proposing `419 Checksum Error`, see https://stackoverflow.com/a/35665694 @@ -497,8 +501,7 @@ func (s *service) CreateContainer(ctx context.Context, req *provider.CreateConta Status: status.NewInternal(ctx, err, "error unwrapping path"), }, nil } - - if err := s.storage.CreateDir(ctx, newRef.GetPath()); err != nil { + if err := s.storage.CreateDir(ctx, newRef); err != nil { var st *rpc.Status switch err.(type) { case errtypes.IsNotFound: @@ -630,7 +633,7 @@ func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provide }, nil } - if err := s.wrap(ctx, md); err != nil { + if err := s.wrap(ctx, md, utils.IsAbsoluteReference(req.Ref)); err != nil { return &provider.StatResponse{ Status: status.NewInternal(ctx, err, "error wrapping path"), }, nil @@ -679,8 +682,9 @@ func (s *service) ListContainerStream(req *provider.ListContainerStreamRequest, return nil } + prefixMountpoint := utils.IsAbsoluteReference(req.Ref) for _, md := range mds { - if err := s.wrap(ctx, md); err != nil { + if err := s.wrap(ctx, md, prefixMountpoint); err != nil { res := &provider.ListContainerStreamResponse{ Status: status.NewInternal(ctx, err, "error wrapping path"), } @@ -728,8 +732,9 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer } var infos = make([]*provider.ResourceInfo, 0, len(mds)) + prefixMountpoint := utils.IsAbsoluteReference(req.Ref) for _, md := range mds { - if err := s.wrap(ctx, md); err != nil { + if err := s.wrap(ctx, md, prefixMountpoint); err != nil { return &provider.ListContainerResponse{ Status: status.NewInternal(ctx, err, "error wrapping path"), }, nil @@ -908,7 +913,7 @@ func (s *service) RestoreRecycleItem(ctx context.Context, req *provider.RestoreR func (s *service) PurgeRecycle(ctx context.Context, req *provider.PurgeRecycleRequest) (*provider.PurgeRecycleResponse, error) { // if a key was sent as opaque id purge only that item - if req.GetRef().GetResourceId() != nil && req.GetRef().GetResourceId().OpaqueId != "" { + if req.GetRef() != nil && req.GetRef().GetResourceId() != nil && req.GetRef().GetResourceId().OpaqueId != "" { if err := s.storage.PurgeRecycleItem(ctx, req.GetRef().GetResourceId().OpaqueId, req.GetRef().Path); err != nil { var st *rpc.Status switch err.(type) { @@ -1201,15 +1206,20 @@ func getFS(c *config) (storage.FS, error) { } func (s *service) unwrap(ctx context.Context, ref *provider.Reference) (*provider.Reference, error) { + // all references with an id can be passed on to the driver + // there are two cases: + // 1. absolute id references (resource_id is set, path is empty) + // 2. relative references (resource_id is set, path starts with a `.`) if ref.GetResourceId() != nil { return ref, nil } - if ref.GetPath() == "" { - // abort, no valid id nor path + if !strings.HasPrefix(ref.GetPath(), "/") { + // abort, absolute path references must start with a `/` return nil, errtypes.BadRequest("ref is invalid: " + ref.String()) } + // TODO move mount path trimming to the gateway fn := ref.GetPath() fsfn, err := s.trimMountPrefix(fn) if err != nil { @@ -1228,12 +1238,15 @@ func (s *service) trimMountPrefix(fn string) (string, error) { return "", errtypes.BadRequest(fmt.Sprintf("path=%q does not belong to this storage provider mount path=%q", fn, s.mountPath)) } -func (s *service) wrap(ctx context.Context, ri *provider.ResourceInfo) error { +func (s *service) wrap(ctx context.Context, ri *provider.ResourceInfo, prefixMountpoint bool) error { if ri.Id.StorageId == "" { // For wrapper drivers, the storage ID might already be set. In that case, skip setting it ri.Id.StorageId = s.mountID } - ri.Path = path.Join(s.mountPath, ri.Path) + if prefixMountpoint { + // TODO move mount path prefixing to the gateway + ri.Path = path.Join(s.mountPath, ri.Path) + } return nil } diff --git a/internal/http/services/dataprovider/dataprovider.go b/internal/http/services/dataprovider/dataprovider.go index a87a32081e..de4a7a785a 100644 --- a/internal/http/services/dataprovider/dataprovider.go +++ b/internal/http/services/dataprovider/dataprovider.go @@ -103,6 +103,7 @@ func getDataTXs(c *config, fs storage.FS) (map[string]http.Handler, error) { } if len(c.DataTXs) == 0 { c.DataTXs["simple"] = make(map[string]interface{}) + c.DataTXs["spaces"] = make(map[string]interface{}) c.DataTXs["tus"] = make(map[string]interface{}) } diff --git a/internal/http/services/owncloud/ocdav/copy.go b/internal/http/services/owncloud/ocdav/copy.go index 93a3851178..8860da2e8f 100644 --- a/internal/http/services/owncloud/ocdav/copy.go +++ b/internal/http/services/owncloud/ocdav/copy.go @@ -33,6 +33,8 @@ import ( "github.com/cs3org/reva/internal/http/services/datagateway" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/rhttp" + "github.com/cs3org/reva/pkg/rhttp/router" + "github.com/cs3org/reva/pkg/utils" "github.com/rs/zerolog" "go.opencensus.io/trace" ) @@ -48,7 +50,7 @@ type intermediateDirRefFunc func() (*provider.Reference, *rpc.Status, error) func (s *svc) handlePathCopy(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() - ctx, span := trace.StartSpan(ctx, "head") + ctx, span := trace.StartSpan(ctx, "copy") defer span.End() src := path.Join(ns, r.URL.Path) @@ -256,6 +258,227 @@ func (s *svc) executePathCopy(ctx context.Context, client gateway.GatewayAPIClie return nil } +func (s *svc) handleSpacesCopy(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_copy") + defer span.End() + + dst, err := extractDestination(r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Str("destination", dst).Logger() + + // retrieve a specific storage space + srcRef, status, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) + return + } + + dstSpaceID, dstRelPath := router.ShiftPath(dst) + + // retrieve a specific storage space + dstRef, status, err := s.lookUpStorageSpaceReference(ctx, dstSpaceID, dstRelPath) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) + return + } + + intermediateDirRefFunc := func() (*provider.Reference, *rpc.Status, error) { + intermediateDir := path.Dir(dstRelPath) + return s.lookUpStorageSpaceReference(ctx, dstSpaceID, intermediateDir) + } + + cp := s.prepareCopy(ctx, w, r, srcRef, dstRef, intermediateDirRefFunc, &sublog) + if cp == nil { + return + } + client, err := s.getClient() + if err != nil { + sublog.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return + } + + err = s.executeSpacesCopy(ctx, w, client, cp) + if err != nil { + sublog.Error().Err(err).Str("depth", cp.depth).Msg("error descending directory") + w.WriteHeader(http.StatusInternalServerError) + } + w.WriteHeader(cp.successCode) +} + +func (s *svc) executeSpacesCopy(ctx context.Context, w http.ResponseWriter, client gateway.GatewayAPIClient, cp *copy) error { + log := appctx.GetLogger(ctx) + log.Debug().Interface("src", cp.sourceInfo).Interface("dst", cp.destination).Msg("descending") + + if cp.sourceInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + // create dir + createReq := &provider.CreateContainerRequest{ + Ref: cp.destination, + } + createRes, err := client.CreateContainer(ctx, createReq) + if err != nil { + log.Error().Err(err).Msg("error performing create container grpc request") + return err + } + if createRes.Status.Code != rpc.Code_CODE_OK { + if createRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { + w.WriteHeader(http.StatusForbidden) + // TODO path could be empty or relative... + m := fmt.Sprintf("Permission denied to create %v", createReq.Ref.Path) + b, err := Marshal(exception{ + code: SabredavPermissionDenied, + message: m, + }) + HandleWebdavError(log, w, b, err) + } + return nil + } + + // TODO: also copy properties: https://tools.ietf.org/html/rfc4918#section-9.8.2 + + if cp.depth != "infinity" { + return nil + } + + // descend for children + listReq := &provider.ListContainerRequest{Ref: &provider.Reference{ResourceId: cp.sourceInfo.Id, Path: "."}} + res, err := client.ListContainer(ctx, listReq) + if err != nil { + return err + } + if res.Status.Code != rpc.Code_CODE_OK { + w.WriteHeader(http.StatusInternalServerError) + return nil + } + + for i := range res.Infos { + childRef := &provider.Reference{ + ResourceId: cp.destination.ResourceId, + Path: utils.MakeRelativePath(path.Join(cp.destination.Path, res.Infos[i].Path)), + } + err := s.executeSpacesCopy(ctx, w, client, ©{sourceInfo: res.Infos[i], destination: childRef, depth: cp.depth, successCode: cp.successCode}) + if err != nil { + return err + } + } + } else { + // copy file + // 1. get download url + dReq := &provider.InitiateFileDownloadRequest{Ref: &provider.Reference{ResourceId: cp.sourceInfo.Id, Path: "."}} + dRes, err := client.InitiateFileDownload(ctx, dReq) + if err != nil { + return err + } + + if dRes.Status.Code != rpc.Code_CODE_OK { + return fmt.Errorf("status code %d", dRes.Status.Code) + } + + var downloadEP, downloadToken string + for _, p := range dRes.Protocols { + if p.Protocol == "spaces" { + downloadEP, downloadToken = p.DownloadEndpoint, p.Token + } + } + // 2. get upload url + uReq := &provider.InitiateFileUploadRequest{ + Ref: cp.destination, + Opaque: &typespb.Opaque{ + Map: map[string]*typespb.OpaqueEntry{ + HeaderUploadLength: { + Decoder: "plain", + // TODO: handle case where size is not known in advance + Value: []byte(strconv.FormatUint(cp.sourceInfo.GetSize(), 10)), + }, + }, + }, + } + + uRes, err := client.InitiateFileUpload(ctx, uReq) + if err != nil { + return err + } + + if uRes.Status.Code != rpc.Code_CODE_OK { + if uRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { + w.WriteHeader(http.StatusForbidden) + // TODO path can be empty or relative + m := fmt.Sprintf("Permissions denied to create %v", uReq.Ref.Path) + b, err := Marshal(exception{ + code: SabredavPermissionDenied, + message: m, + }) + HandleWebdavError(log, w, b, err) + return nil + } + HandleErrorStatus(log, w, uRes.Status) + return nil + } + + var uploadEP, uploadToken string + for _, p := range uRes.Protocols { + if p.Protocol == "simple" { + uploadEP, uploadToken = p.UploadEndpoint, p.Token + } + } + + // 3. do download + httpDownloadReq, err := rhttp.NewRequest(ctx, http.MethodGet, downloadEP, nil) + if err != nil { + return err + } + if downloadToken != "" { + httpDownloadReq.Header.Set(datagateway.TokenTransportHeader, downloadToken) + } + + httpDownloadRes, err := s.client.Do(httpDownloadReq) + if err != nil { + return err + } + defer httpDownloadRes.Body.Close() + if httpDownloadRes.StatusCode != http.StatusOK { + return fmt.Errorf("status code %d", httpDownloadRes.StatusCode) + } + + // 4. do upload + + if cp.sourceInfo.GetSize() > 0 { + httpUploadReq, err := rhttp.NewRequest(ctx, http.MethodPut, uploadEP, httpDownloadRes.Body) + if err != nil { + return err + } + httpUploadReq.Header.Set(datagateway.TokenTransportHeader, uploadToken) + + httpUploadRes, err := s.client.Do(httpUploadReq) + if err != nil { + return err + } + defer httpUploadRes.Body.Close() + if httpUploadRes.StatusCode != http.StatusOK { + return err + } + } + } + return nil +} + func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Request, srcRef, dstRef *provider.Reference, intermediateDirRef intermediateDirRefFunc, log *zerolog.Logger) *copy { overwrite, err := extractOverwrite(w, r) if err != nil { diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index e53c438eee..137423013e 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -45,6 +45,7 @@ type DavHandler struct { FilesHomeHandler *WebDavHandler MetaHandler *MetaHandler TrashbinHandler *TrashbinHandler + SpacesHandler *SpacesHandler PublicFolderHandler *WebDavHandler PublicFileHandler *PublicFileHandler } @@ -68,6 +69,11 @@ func (h *DavHandler) init(c *Config) error { } h.TrashbinHandler = new(TrashbinHandler) + h.SpacesHandler = new(SpacesHandler) + if err := h.SpacesHandler.init(c); err != nil { + return err + } + h.PublicFolderHandler = new(WebDavHandler) if err := h.PublicFolderHandler.init("public", true); err != nil { // jail public file requests to /public/ prefix return err @@ -161,6 +167,11 @@ func (h *DavHandler) Handler(s *svc) http.Handler { ctx := context.WithValue(ctx, ctxKeyBaseURI, base) r = r.WithContext(ctx) h.TrashbinHandler.Handler(s).ServeHTTP(w, r) + case "spaces": + base := path.Join(ctx.Value(ctxKeyBaseURI).(string), "spaces") + ctx := context.WithValue(ctx, ctxKeyBaseURI, base) + r = r.WithContext(ctx) + h.SpacesHandler.Handler(s).ServeHTTP(w, r) case "public-files": base := path.Join(ctx.Value(ctxKeyBaseURI).(string), "public-files") ctx = context.WithValue(ctx, ctxKeyBaseURI, base) diff --git a/internal/http/services/owncloud/ocdav/delete.go b/internal/http/services/owncloud/ocdav/delete.go index 8b221a10c0..69bba949ac 100644 --- a/internal/http/services/owncloud/ocdav/delete.go +++ b/internal/http/services/owncloud/ocdav/delete.go @@ -60,6 +60,7 @@ func (s *svc) handleDelete(ctx context.Context, w http.ResponseWriter, r *http.R } else if res.Status.Code != rpc.Code_CODE_OK { if res.Status.Code == rpc.Code_CODE_NOT_FOUND { w.WriteHeader(http.StatusNotFound) + // TODO path might be empty or relative... m := fmt.Sprintf("Resource %v not found", ref.Path) b, err := Marshal(exception{ code: SabredavNotFound, @@ -69,6 +70,7 @@ func (s *svc) handleDelete(ctx context.Context, w http.ResponseWriter, r *http.R } if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { w.WriteHeader(http.StatusForbidden) + // TODO path might be empty or relative... m := fmt.Sprintf("Permission denied to delete %v", ref.Path) b, err := Marshal(exception{ code: SabredavPermissionDenied, @@ -89,3 +91,25 @@ func (s *svc) handleDelete(ctx context.Context, w http.ResponseWriter, r *http.R } w.WriteHeader(http.StatusNoContent) } + +func (s *svc) handleSpacesDelete(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_delete") + defer span.End() + + sublog := appctx.GetLogger(ctx).With().Logger() + // retrieve a specific storage space + ref, rpcStatus, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if rpcStatus.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, rpcStatus) + return + } + + s.handleDelete(ctx, w, r, ref, sublog) +} diff --git a/internal/http/services/owncloud/ocdav/get.go b/internal/http/services/owncloud/ocdav/get.go index 8674ed62a5..ba5837db90 100644 --- a/internal/http/services/owncloud/ocdav/get.go +++ b/internal/http/services/owncloud/ocdav/get.go @@ -159,3 +159,25 @@ func (s *svc) handleGet(ctx context.Context, w http.ResponseWriter, r *http.Requ } // TODO we need to send the If-Match etag in the GET to the datagateway to prevent race conditions between stating and reading the file } + +func (s *svc) handleSpacesGet(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_get") + defer span.End() + + sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Str("handler", "get").Logger() + + // retrieve a specific storage space + ref, rpcStatus, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if rpcStatus.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, rpcStatus) + return + } + s.handleGet(ctx, w, r, ref, "spaces", sublog) +} diff --git a/internal/http/services/owncloud/ocdav/head.go b/internal/http/services/owncloud/ocdav/head.go index 6755d5e15b..f74c7d5bc0 100644 --- a/internal/http/services/owncloud/ocdav/head.go +++ b/internal/http/services/owncloud/ocdav/head.go @@ -87,3 +87,25 @@ func (s *svc) handleHead(ctx context.Context, w http.ResponseWriter, r *http.Req } w.WriteHeader(http.StatusOK) } + +func (s *svc) handleSpacesHead(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_head") + defer span.End() + + sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Logger() + + spaceRef, status, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) + return + } + + s.handleHead(ctx, w, r, spaceRef, sublog) +} diff --git a/internal/http/services/owncloud/ocdav/mkcol.go b/internal/http/services/owncloud/ocdav/mkcol.go index 5f49e0d6ac..c8707fbc32 100644 --- a/internal/http/services/owncloud/ocdav/mkcol.go +++ b/internal/http/services/owncloud/ocdav/mkcol.go @@ -50,6 +50,29 @@ func (s *svc) handlePathMkcol(w http.ResponseWriter, r *http.Request, ns string) s.handleMkcol(ctx, w, r, ref, sublog) } +func (s *svc) handleSpacesMkCol(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_mkcol") + defer span.End() + + sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Str("handler", "mkcol").Logger() + + ref, rpcStatus, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if rpcStatus.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, rpcStatus) + return + } + + s.handleMkcol(ctx, w, r, ref, sublog) + +} + func (s *svc) handleMkcol(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) { if r.Body != http.NoBody { w.WriteHeader(http.StatusUnsupportedMediaType) @@ -63,7 +86,7 @@ func (s *svc) handleMkcol(ctx context.Context, w http.ResponseWriter, r *http.Re return } - // check fn exists + // check if ref exists statReq := &provider.StatRequest{Ref: ref} statRes, err := client.Stat(ctx, statReq) if err != nil { @@ -101,6 +124,7 @@ func (s *svc) handleMkcol(ctx context.Context, w http.ResponseWriter, r *http.Re w.WriteHeader(http.StatusConflict) case rpc.Code_CODE_PERMISSION_DENIED: w.WriteHeader(http.StatusForbidden) + // TODO path could be empty or relative... m := fmt.Sprintf("Permission denied to create %v", ref.Path) b, err := Marshal(exception{ code: SabredavPermissionDenied, diff --git a/internal/http/services/owncloud/ocdav/move.go b/internal/http/services/owncloud/ocdav/move.go index e93b5c4144..b1e7bc5139 100644 --- a/internal/http/services/owncloud/ocdav/move.go +++ b/internal/http/services/owncloud/ocdav/move.go @@ -28,6 +28,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/rhttp/router" "github.com/rs/zerolog" "go.opencensus.io/trace" ) @@ -38,7 +39,6 @@ func (s *svc) handlePathMove(w http.ResponseWriter, r *http.Request, ns string) defer span.End() srcPath := path.Join(ns, r.URL.Path) - dstPath, err := extractDestination(r) if err != nil { w.WriteHeader(http.StatusBadRequest) @@ -66,6 +66,53 @@ func (s *svc) handlePathMove(w http.ResponseWriter, r *http.Request, ns string) s.handleMove(ctx, w, r, src, dst, intermediateDirRefFunc, sublog) } +func (s *svc) handleSpacesMove(w http.ResponseWriter, r *http.Request, srcSpaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_move") + defer span.End() + + dst, err := extractDestination(r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + sublog := appctx.GetLogger(ctx).With().Str("spaceid", srcSpaceID).Str("path", r.URL.Path).Logger() + // retrieve a specific storage space + srcRef, status, err := s.lookUpStorageSpaceReference(ctx, srcSpaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) + return + } + + dstSpaceID, dstRelPath := router.ShiftPath(dst) + + // retrieve a specific storage space + dstRef, status, err := s.lookUpStorageSpaceReference(ctx, dstSpaceID, dstRelPath) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) + return + } + + intermediateDirRefFunc := func() (*provider.Reference, *rpc.Status, error) { + intermediateDir := path.Dir(dstRelPath) + return s.lookUpStorageSpaceReference(ctx, dstSpaceID, intermediateDir) + } + s.handleMove(ctx, w, r, srcRef, dstRef, intermediateDirRefFunc, sublog) +} + func (s *svc) handleMove(ctx context.Context, w http.ResponseWriter, r *http.Request, src, dst *provider.Reference, intermediateDirRef intermediateDirRefFunc, log zerolog.Logger) { overwrite := r.Header.Get(HeaderOverwrite) log.Debug().Str("overwrite", overwrite).Msg("move") diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index b429ebb467..2de1fbbec6 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -327,7 +327,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str return result + str[lastIndex:] } -var hrefre = regexp.MustCompile(`([^A-Za-z0-9_\-.~()/:@])`) +var hrefre = regexp.MustCompile(`([^A-Za-z0-9_\-.~()/:@!$])`) // encodePath encodes the path of a url. // diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go index 011db50040..94f8614da0 100644 --- a/internal/http/services/owncloud/ocdav/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind.go @@ -88,6 +88,51 @@ func (s *svc) handlePathPropfind(w http.ResponseWriter, r *http.Request, ns stri s.propfindResponse(ctx, w, r, ns, pf, parentInfo, resourceInfos, sublog) } +func (s *svc) handleSpacesPropfind(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_propfind") + defer span.End() + + sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Logger() + + pf, status, err := readPropfind(r.Body) + if err != nil { + sublog.Debug().Err(err).Msg("error reading propfind request") + w.WriteHeader(status) + return + } + + // retrieve a specific storage space + ref, rpcStatus, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if rpcStatus.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, rpcStatus) + return + } + + parentInfo, resourceInfos, ok := s.getResourceInfos(ctx, w, r, pf, ref, sublog) + if !ok { + // getResourceInfos handles responses in case of an error so we can just return here. + return + } + + // parentInfo Path is the name but we need / + parentInfo.Path = "/" + + // prefix space id to paths + for i := range resourceInfos { + resourceInfos[i].Path = path.Join("/", spaceID, r.URL.Path, resourceInfos[i].Path) + } + + s.propfindResponse(ctx, w, r, "", pf, parentInfo, resourceInfos, sublog) + +} + func (s *svc) propfindResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, namespace string, pf propfindXML, parentInfo *provider.ResourceInfo, resourceInfos []*provider.ResourceInfo, log zerolog.Logger) { propRes, err := s.formatPropfind(ctx, &pf, resourceInfos, namespace) if err != nil { diff --git a/internal/http/services/owncloud/ocdav/proppatch.go b/internal/http/services/owncloud/ocdav/proppatch.go index 9b7fcbb459..326550cf2d 100644 --- a/internal/http/services/owncloud/ocdav/proppatch.go +++ b/internal/http/services/owncloud/ocdav/proppatch.go @@ -104,6 +104,70 @@ func (s *svc) handlePathProppatch(w http.ResponseWriter, r *http.Request, ns str s.handleProppatchResponse(ctx, w, r, acceptedProps, removedProps, nRef, sublog) } +func (s *svc) handleSpacesProppatch(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_proppatch") + defer span.End() + + sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Logger() + + pp, status, err := readProppatch(r.Body) + if err != nil { + sublog.Debug().Err(err).Msg("error reading proppatch") + w.WriteHeader(status) + return + } + + // retrieve a specific storage space + ref, rpcStatus, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if rpcStatus.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, rpcStatus) + return + } + + c, err := s.getClient() + if err != nil { + sublog.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return + } + // check if resource exists + statReq := &provider.StatRequest{ + Ref: ref, + } + statRes, err := c.Stat(ctx, statReq) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc stat request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if statRes.Status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, statRes.Status) + return + } + + acceptedProps, removedProps, ok := s.handleProppatch(ctx, w, r, ref, pp, sublog) + if !ok { + // handleProppatch handles responses in error cases so we can just return + return + } + + nRef := path.Join(spaceID, statRes.Info.Path) + nRef = path.Join(ctx.Value(ctxKeyBaseURI).(string), nRef) + if statRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + nRef += "/" + } + + s.handleProppatchResponse(ctx, w, r, acceptedProps, removedProps, nRef, sublog) +} + func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, patches []Proppatch, log zerolog.Logger) ([]xml.Name, []xml.Name, bool) { c, err := s.getClient() if err != nil { diff --git a/internal/http/services/owncloud/ocdav/publicfile.go b/internal/http/services/owncloud/ocdav/publicfile.go index 3d06da037d..ba4cc78ee8 100644 --- a/internal/http/services/owncloud/ocdav/publicfile.go +++ b/internal/http/services/owncloud/ocdav/publicfile.go @@ -131,7 +131,7 @@ func (s *svc) adjustResourcePathInURL(w http.ResponseWriter, r *http.Request) bo // ns is the namespace that is prefixed to the path in the cs3 namespace func (s *svc) handlePropfindOnToken(w http.ResponseWriter, r *http.Request, ns string, onContainer bool) { ctx := r.Context() - ctx, span := trace.StartSpan(ctx, "propfind") + ctx, span := trace.StartSpan(ctx, "token_propfind") defer span.End() tokenStatInfo := ctx.Value(tokenStatInfoKey{}).(*provider.ResourceInfo) diff --git a/internal/http/services/owncloud/ocdav/put.go b/internal/http/services/owncloud/ocdav/put.go index ed2b539db9..9b8701ca3b 100644 --- a/internal/http/services/owncloud/ocdav/put.go +++ b/internal/http/services/owncloud/ocdav/put.go @@ -36,6 +36,7 @@ import ( "github.com/cs3org/reva/pkg/storage/utils/chunking" "github.com/cs3org/reva/pkg/utils" "github.com/rs/zerolog" + "go.opencensus.io/trace" ) func sufferMacOSFinder(r *http.Request) bool { @@ -105,6 +106,9 @@ func isContentRange(r *http.Request) bool { func (s *svc) handlePathPut(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "put") + defer span.End() + fn := path.Join(ns, r.URL.Path) sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() @@ -274,6 +278,7 @@ func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Requ message: "The computed checksum does not match the one received from the client.", }) HandleWebdavError(&log, w, b, err) + return } log.Error().Err(err).Msg("PUT request to data server failed") w.WriteHeader(httpRes.StatusCode) @@ -328,6 +333,28 @@ func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Requ w.WriteHeader(http.StatusNoContent) } +func (s *svc) handleSpacesPut(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_put") + defer span.End() + + sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Logger() + + spaceRef, status, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) + return + } + + s.handlePut(ctx, w, r, spaceRef, sublog) +} + func checkPreconditions(w http.ResponseWriter, r *http.Request, log zerolog.Logger) bool { if isContentRange(r) { log.Debug().Msg("Content-Range not supported for PUT") diff --git a/internal/http/services/owncloud/ocdav/spaces.go b/internal/http/services/owncloud/ocdav/spaces.go new file mode 100644 index 0000000000..309d20ae3c --- /dev/null +++ b/internal/http/services/owncloud/ocdav/spaces.go @@ -0,0 +1,132 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocdav + +import ( + "context" + "fmt" + "net/http" + + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/pkg/rhttp/router" + "github.com/cs3org/reva/pkg/utils" +) + +// SpacesHandler handles trashbin requests +type SpacesHandler struct { + gatewaySvc string +} + +func (h *SpacesHandler) init(c *Config) error { + h.gatewaySvc = c.GatewaySvc + return nil +} + +// Handler handles requests +func (h *SpacesHandler) Handler(s *svc) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // ctx := r.Context() + // log := appctx.GetLogger(ctx) + + if r.Method == http.MethodOptions { + s.handleOptions(w, r, "spaces") + return + } + + var spaceID string + spaceID, r.URL.Path = router.ShiftPath(r.URL.Path) + + if spaceID == "" { + // listing is disabled, no auth will change that + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + switch r.Method { + case MethodPropfind: + s.handleSpacesPropfind(w, r, spaceID) + case MethodProppatch: + s.handleSpacesProppatch(w, r, spaceID) + case MethodLock: + s.handleLock(w, r, spaceID) + case MethodUnlock: + s.handleUnlock(w, r, spaceID) + case MethodMkcol: + s.handleSpacesMkCol(w, r, spaceID) + case MethodMove: + s.handleSpacesMove(w, r, spaceID) + case MethodCopy: + s.handleSpacesCopy(w, r, spaceID) + case MethodReport: + s.handleReport(w, r, spaceID) + case http.MethodGet: + s.handleSpacesGet(w, r, spaceID) + case http.MethodPut: + s.handleSpacesPut(w, r, spaceID) + case http.MethodPost: + s.handleSpacesTusPost(w, r, spaceID) + case http.MethodOptions: + s.handleOptions(w, r, spaceID) + case http.MethodHead: + s.handleSpacesHead(w, r, spaceID) + case http.MethodDelete: + s.handleSpacesDelete(w, r, spaceID) + default: + http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) + } + }) +} + +func (s *svc) lookUpStorageSpaceReference(ctx context.Context, spaceID string, relativePath string) (*storageProvider.Reference, *rpc.Status, error) { + // Get the getway client + gatewayClient, err := s.getClient() + if err != nil { + return nil, nil, err + } + + // retrieve a specific storage space + lSSReq := &storageProvider.ListStorageSpacesRequest{ + Filters: []*storageProvider.ListStorageSpacesRequest_Filter{ + { + Type: storageProvider.ListStorageSpacesRequest_Filter_TYPE_ID, + Term: &storageProvider.ListStorageSpacesRequest_Filter_Id{ + Id: &storageProvider.StorageSpaceId{ + OpaqueId: spaceID, + }, + }, + }, + }, + } + + lSSRes, err := gatewayClient.ListStorageSpaces(ctx, lSSReq) + if err != nil || lSSRes.Status.Code != rpc.Code_CODE_OK { + return nil, lSSRes.Status, err + } + + if len(lSSRes.StorageSpaces) != 1 { + return nil, nil, fmt.Errorf("unexpected number of spaces") + } + space := lSSRes.StorageSpaces[0] + + return &storageProvider.Reference{ + ResourceId: space.Root, + Path: utils.MakeRelativePath(relativePath), + }, lSSRes.Status, nil +} diff --git a/internal/http/services/owncloud/ocdav/tus.go b/internal/http/services/owncloud/ocdav/tus.go index 01ee9778a7..57c128aedc 100644 --- a/internal/http/services/owncloud/ocdav/tus.go +++ b/internal/http/services/owncloud/ocdav/tus.go @@ -61,6 +61,34 @@ func (s *svc) handlePathTusPost(w http.ResponseWriter, r *http.Request, ns strin s.handleTusPost(ctx, w, r, meta, ref, sublog) } +func (s *svc) handleSpacesTusPost(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces-tus-post") + defer span.End() + + // read filename from metadata + meta := tusd.ParseMetadataHeader(r.Header.Get(HeaderUploadMetadata)) + if meta["filename"] == "" { + w.WriteHeader(http.StatusPreconditionFailed) + return + } + + sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Logger() + + spaceRef, status, err := s.lookUpStorageSpaceReference(ctx, spaceID, path.Join(r.URL.Path, meta["filename"])) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) + return + } + + s.handleTusPost(ctx, w, r, meta, spaceRef, sublog) +} + func (s *svc) handleTusPost(ctx context.Context, w http.ResponseWriter, r *http.Request, meta map[string]string, ref *provider.Reference, log zerolog.Logger) { w.Header().Add(HeaderAccessControlAllowHeaders, strings.Join([]string{HeaderTusResumable, HeaderUploadLength, HeaderUploadMetadata, HeaderIfMatch}, ", ")) w.Header().Add(HeaderAccessControlExposeHeaders, strings.Join([]string{HeaderTusResumable, HeaderLocation}, ", ")) diff --git a/pkg/auth/scope/publicshare.go b/pkg/auth/scope/publicshare.go index 31a0400df0..6c798366c4 100644 --- a/pkg/auth/scope/publicshare.go +++ b/pkg/auth/scope/publicshare.go @@ -71,7 +71,7 @@ func publicshareScope(scope *authpb.Scope, resource interface{}) (bool, error) { } func checkStorageRef(s *link.PublicShare, r *provider.Reference) bool { - // r: > + // r: path:$path > > if r.ResourceId != nil && r.Path == "" { // path must be empty return utils.ResourceIDEqual(s.ResourceId, r.GetResourceId()) } diff --git a/pkg/auth/scope/resourceinfo.go b/pkg/auth/scope/resourceinfo.go index 7ace8b3e12..f6267ff3d1 100644 --- a/pkg/auth/scope/resourceinfo.go +++ b/pkg/auth/scope/resourceinfo.go @@ -68,10 +68,17 @@ func resourceinfoScope(scope *authpb.Scope, resource interface{}) (bool, error) } func checkResourceInfo(inf *provider.ResourceInfo, ref *provider.Reference) bool { - // ref: > + // ref: > if ref.ResourceId != nil { // path can be empty or a relative path - // TODO what about the path? - return utils.ResourceIDEqual(inf.Id, ref.ResourceId) + if inf.Id.StorageId == ref.ResourceId.StorageId && inf.Id.OpaqueId == ref.ResourceId.OpaqueId { + if ref.Path == "" { + // id only reference + return true + } + // check path has same prefix below + } else { + return false + } } // ref: if strings.HasPrefix(ref.GetPath(), inf.Path) { diff --git a/pkg/rhttp/datatx/manager/loader/loader.go b/pkg/rhttp/datatx/manager/loader/loader.go index be814f3e86..15a11bb5f4 100644 --- a/pkg/rhttp/datatx/manager/loader/loader.go +++ b/pkg/rhttp/datatx/manager/loader/loader.go @@ -21,6 +21,7 @@ package loader import ( // Load core data transfer protocols _ "github.com/cs3org/reva/pkg/rhttp/datatx/manager/simple" + _ "github.com/cs3org/reva/pkg/rhttp/datatx/manager/spaces" _ "github.com/cs3org/reva/pkg/rhttp/datatx/manager/tus" // Add your own here ) diff --git a/pkg/rhttp/datatx/manager/simple/simple.go b/pkg/rhttp/datatx/manager/simple/simple.go index 50445f1df6..21bfde8244 100644 --- a/pkg/rhttp/datatx/manager/simple/simple.go +++ b/pkg/rhttp/datatx/manager/simple/simple.go @@ -68,7 +68,7 @@ func (m *manager) Handler(fs storage.FS) (http.Handler, error) { switch r.Method { case "GET", "HEAD": - download.GetOrHeadFile(w, r, fs) + download.GetOrHeadFile(w, r, fs, "") case "PUT": fn := r.URL.Path defer r.Body.Close() diff --git a/pkg/rhttp/datatx/manager/spaces/spaces.go b/pkg/rhttp/datatx/manager/spaces/spaces.go new file mode 100644 index 0000000000..bb982c7a85 --- /dev/null +++ b/pkg/rhttp/datatx/manager/spaces/spaces.go @@ -0,0 +1,124 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package spaces + +import ( + "net/http" + "path" + "strings" + + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/rhttp/datatx" + "github.com/cs3org/reva/pkg/rhttp/datatx/manager/registry" + "github.com/cs3org/reva/pkg/rhttp/datatx/utils/download" + "github.com/cs3org/reva/pkg/rhttp/router" + "github.com/cs3org/reva/pkg/storage" + "github.com/cs3org/reva/pkg/utils" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +func init() { + registry.Register("spaces", New) +} + +type config struct{} + +type manager struct { + conf *config +} + +func parseConfig(m map[string]interface{}) (*config, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + err = errors.Wrap(err, "error decoding conf") + return nil, err + } + return c, nil +} + +// New returns a datatx manager implementation that relies on HTTP PUT/GET. +func New(m map[string]interface{}) (datatx.DataTX, error) { + c, err := parseConfig(m) + if err != nil { + return nil, err + } + + return &manager{conf: c}, nil +} + +func (m *manager) Handler(fs storage.FS) (http.Handler, error) { + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var spaceID string + spaceID, r.URL.Path = router.ShiftPath(r.URL.Path) + + sublog := appctx.GetLogger(ctx).With().Str("datatx", "spaces").Str("space", spaceID).Logger() + + switch r.Method { + case "GET", "HEAD": + download.GetOrHeadFile(w, r, fs, spaceID) + case "PUT": + // make a clean relative path + fn := path.Clean(strings.TrimLeft(r.URL.Path, "/")) + defer r.Body.Close() + + // TODO refactor: pass Reference to Upload & GetOrHeadFile + // build a storage space reference + storageid, opaqeid, err := utils.SplitStorageSpaceID(spaceID) + if err != nil { + sublog.Error().Msg("space id must be separated by !") + w.WriteHeader(http.StatusBadRequest) + return + } + + ref := &provider.Reference{ + ResourceId: &provider.ResourceId{StorageId: storageid, OpaqueId: opaqeid}, + Path: fn, + } + err = fs.Upload(ctx, ref, r.Body) + switch v := err.(type) { + case nil: + w.WriteHeader(http.StatusOK) + case errtypes.PartialContent: + w.WriteHeader(http.StatusPartialContent) + case errtypes.ChecksumMismatch: + w.WriteHeader(errtypes.StatusChecksumMismatch) + case errtypes.NotFound: + w.WriteHeader(http.StatusNotFound) + case errtypes.PermissionDenied: + w.WriteHeader(http.StatusForbidden) + case errtypes.InvalidCredentials: + w.WriteHeader(http.StatusUnauthorized) + case errtypes.InsufficientStorage: + w.WriteHeader(http.StatusInsufficientStorage) + default: + sublog.Error().Err(v).Msg("error uploading file") + w.WriteHeader(http.StatusInternalServerError) + } + return + default: + w.WriteHeader(http.StatusNotImplemented) + } + }) + return h, nil +} diff --git a/pkg/rhttp/datatx/manager/tus/tus.go b/pkg/rhttp/datatx/manager/tus/tus.go index 09694cb68a..7ece3466fb 100644 --- a/pkg/rhttp/datatx/manager/tus/tus.go +++ b/pkg/rhttp/datatx/manager/tus/tus.go @@ -103,7 +103,7 @@ func (m *manager) Handler(fs storage.FS) (http.Handler, error) { case "DELETE": handler.DelFile(w, r) case "GET": - download.GetOrHeadFile(w, r, fs) + download.GetOrHeadFile(w, r, fs, "") default: w.WriteHeader(http.StatusNotImplemented) } diff --git a/pkg/rhttp/datatx/utils/download/download.go b/pkg/rhttp/datatx/utils/download/download.go index 84879b83eb..f96e9d0328 100644 --- a/pkg/rhttp/datatx/utils/download/download.go +++ b/pkg/rhttp/datatx/utils/download/download.go @@ -24,17 +24,19 @@ import ( "io" "mime/multipart" "net/http" + "path" "strconv" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/storage" + "github.com/cs3org/reva/pkg/utils" "github.com/rs/zerolog" ) // GetOrHeadFile returns the requested file content -func GetOrHeadFile(w http.ResponseWriter, r *http.Request, fs storage.FS) { +func GetOrHeadFile(w http.ResponseWriter, r *http.Request, fs storage.FS, spaceID string) { ctx := r.Context() sublog := appctx.GetLogger(ctx).With().Str("svc", "datatx").Str("handler", "download").Logger() @@ -46,8 +48,24 @@ func GetOrHeadFile(w http.ResponseWriter, r *http.Request, fs storage.FS) { fn = files[0] } - ref := &provider.Reference{Path: fn} - + var ref *provider.Reference + if spaceID == "" { + // ensure the absolute path starts with '/' + ref = &provider.Reference{Path: path.Join("/", fn)} + } else { + // build a storage space reference + storageid, opaqeid, err := utils.SplitStorageSpaceID(spaceID) + if err != nil { + sublog.Error().Str("space_id", spaceID).Str("path", fn).Msg("invalid reference") + w.WriteHeader(http.StatusBadRequest) + return + } + ref = &provider.Reference{ + ResourceId: &provider.ResourceId{StorageId: storageid, OpaqueId: opaqeid}, + // ensure the relative path starts with '.' + Path: utils.MakeRelativePath(fn), + } + } // TODO check preconditions like If-Range, If-Match ... var md *provider.ResourceInfo diff --git a/pkg/storage/fs/owncloud/owncloud.go b/pkg/storage/fs/owncloud/owncloud.go index 471da92e07..8eba17ca41 100644 --- a/pkg/storage/fs/owncloud/owncloud.go +++ b/pkg/storage/fs/owncloud/owncloud.go @@ -1152,8 +1152,12 @@ func (fs *ocfs) GetHome(ctx context.Context) (string, error) { return "", nil } -func (fs *ocfs) CreateDir(ctx context.Context, sp string) (err error) { - ip := fs.toInternalPath(ctx, sp) +func (fs *ocfs) CreateDir(ctx context.Context, ref *provider.Reference) (err error) { + + ip, err := fs.resolve(ctx, ref) + if err != nil { + return err + } // check permissions of parent dir if perm, err := fs.readPermissions(ctx, filepath.Dir(ip)); err == nil { @@ -1162,17 +1166,17 @@ func (fs *ocfs) CreateDir(ctx context.Context, sp string) (err error) { } } else { if isNotFound(err) { - return errtypes.NotFound(fs.toStoragePath(ctx, filepath.Dir(ip))) + return errtypes.NotFound(ref.Path) } return errors.Wrap(err, "ocfs: error reading permissions") } if err = os.Mkdir(ip, 0700); err != nil { if os.IsNotExist(err) { - return errtypes.NotFound(sp) + return errtypes.NotFound(ref.Path) } // FIXME we also need already exists error, webdav expects 405 MethodNotAllowed - return errors.Wrap(err, "ocfs: error creating dir "+ip) + return errors.Wrap(err, "ocfs: error creating dir "+ref.Path) } return fs.propagate(ctx, ip) } diff --git a/pkg/storage/fs/owncloudsql/owncloudsql.go b/pkg/storage/fs/owncloudsql/owncloudsql.go index a33297d2bd..e2a8539c63 100644 --- a/pkg/storage/fs/owncloudsql/owncloudsql.go +++ b/pkg/storage/fs/owncloudsql/owncloudsql.go @@ -575,8 +575,7 @@ func (fs *owncloudsqlfs) resolve(ctx context.Context, ref *provider.Reference) ( } p = filepath.Join(owner, p) } - - return fs.toInternalPath(ctx, p), nil + return p, nil } if ref.GetPath() != "" { @@ -702,8 +701,12 @@ func (fs *owncloudsqlfs) GetHome(ctx context.Context) (string, error) { return "", nil } -func (fs *owncloudsqlfs) CreateDir(ctx context.Context, sp string) (err error) { - ip := fs.toInternalPath(ctx, sp) +func (fs *owncloudsqlfs) CreateDir(ctx context.Context, ref *provider.Reference) (err error) { + + ip, err := fs.resolve(ctx, ref) + if err != nil { + return err + } // check permissions of parent dir if perm, err := fs.readPermissions(ctx, filepath.Dir(ip)); err == nil { @@ -712,17 +715,17 @@ func (fs *owncloudsqlfs) CreateDir(ctx context.Context, sp string) (err error) { } } else { if isNotFound(err) { - return errtypes.NotFound(fs.toStoragePath(ctx, filepath.Dir(ip))) + return errtypes.NotFound(ref.Path) } return errors.Wrap(err, "owncloudsql: error reading permissions") } if err = os.Mkdir(ip, 0700); err != nil { if os.IsNotExist(err) { - return errtypes.NotFound(sp) + return errtypes.NotFound(ref.Path) } // FIXME we also need already exists error, webdav expects 405 MethodNotAllowed - return errors.Wrap(err, "owncloudsql: error creating dir "+ip) + return errors.Wrap(err, "owncloudsql: error creating dir "+fs.toStoragePath(ctx, filepath.Dir(ip))) } fi, err := os.Stat(ip) diff --git a/pkg/storage/fs/s3/s3.go b/pkg/storage/fs/s3/s3.go index d3b491a652..eaf717562a 100644 --- a/pkg/storage/fs/s3/s3.go +++ b/pkg/storage/fs/s3/s3.go @@ -115,11 +115,11 @@ func (fs *s3FS) addRoot(p string) string { } func (fs *s3FS) resolve(ctx context.Context, ref *provider.Reference) (string, error) { - if ref.Path != "" { + if strings.HasPrefix(ref.Path, "/") { return fs.addRoot(ref.GetPath()), nil } - if ref.ResourceId != nil { + if ref.ResourceId != nil && ref.ResourceId.OpaqueId != "" { fn := path.Join("/", strings.TrimPrefix(ref.ResourceId.OpaqueId, "fileid-")) fn = fs.addRoot(fn) return fn, nil @@ -293,8 +293,14 @@ func (fs *s3FS) CreateHome(ctx context.Context) error { return errtypes.NotSupported("s3fs: not supported") } -func (fs *s3FS) CreateDir(ctx context.Context, fn string) error { +func (fs *s3FS) CreateDir(ctx context.Context, ref *provider.Reference) error { log := appctx.GetLogger(ctx) + + fn, err := fs.resolve(ctx, ref) + if err != nil { + return nil + } + fn = fs.addRoot(fn) + "/" // append / to indicate folder // TODO only if fn does not end in / input := &s3.PutObjectInput{ @@ -309,11 +315,11 @@ func (fs *s3FS) CreateDir(ctx context.Context, fn string) error { log.Error().Err(err) if aerr, ok := err.(awserr.Error); ok { if aerr.Code() == s3.ErrCodeNoSuchBucket { - return errtypes.NotFound(fn) + return errtypes.NotFound(ref.Path) } } // FIXME we also need already exists error, webdav expects 405 MethodNotAllowed - return errors.Wrap(err, "s3fs: error creating dir "+fn) + return errors.Wrap(err, "s3fs: error creating dir "+ref.Path) } log.Debug().Interface("result", result) // todo cache etag? diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index fb4c19fa02..1b65c30fdd 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -31,7 +31,7 @@ import ( type FS interface { GetHome(ctx context.Context) (string, error) CreateHome(ctx context.Context) error - CreateDir(ctx context.Context, fn string) error + CreateDir(ctx context.Context, ref *provider.Reference) error Delete(ctx context.Context, ref *provider.Reference) error Move(ctx context.Context, oldRef, newRef *provider.Reference) error GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string) (*provider.ResourceInfo, error) diff --git a/pkg/storage/utils/decomposedfs/decomposedfs.go b/pkg/storage/utils/decomposedfs/decomposedfs.go index 122a329d28..d851323f85 100644 --- a/pkg/storage/utils/decomposedfs/decomposedfs.go +++ b/pkg/storage/utils/decomposedfs/decomposedfs.go @@ -23,14 +23,15 @@ package decomposedfs import ( "context" - "fmt" "io" "math" "net/url" "os" + "path" "path/filepath" "strconv" "strings" + "syscall" userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" @@ -148,7 +149,7 @@ func (fs *Decomposedfs) GetQuota(ctx context.Context) (total uint64, inUse uint6 return 0, 0, errtypes.PermissionDenied(n.ID) } - ri, err := n.AsResourceInfo(ctx, &rp, []string{"treesize", "quota"}) + ri, err := n.AsResourceInfo(ctx, &rp, []string{"treesize", "quota"}, true) if err != nil { return 0, 0, err } @@ -217,14 +218,14 @@ func (fs *Decomposedfs) CreateHome(ctx context.Context) (err error) { } // add storage space - if err := fs.createStorageSpace("personal", h.ID); err != nil { + if err := fs.createStorageSpace(ctx, "personal", h.ID); err != nil { return err } return } -func (fs *Decomposedfs) createStorageSpace(spaceType, nodeID string) error { +func (fs *Decomposedfs) createStorageSpace(ctx context.Context, spaceType, nodeID string) error { // create space type dir if err := os.MkdirAll(filepath.Join(fs.o.Root, "spaces", spaceType), 0700); err != nil { @@ -234,12 +235,28 @@ func (fs *Decomposedfs) createStorageSpace(spaceType, nodeID string) error { // we can reuse the node id as the space id err := os.Symlink("../../nodes/"+nodeID, filepath.Join(fs.o.Root, "spaces", spaceType, nodeID)) if err != nil { - fmt.Printf("could not create symlink for '%s' space %s, %s\n", spaceType, nodeID, err) + if isAlreadyExists(err) { + appctx.GetLogger(ctx).Debug().Err(err).Str("node", nodeID).Str("spacetype", spaceType).Msg("symlink already exists") + } else { + // TODO how should we handle error cases here? + appctx.GetLogger(ctx).Error().Err(err).Str("node", nodeID).Str("spacetype", spaceType).Msg("could not create symlink") + } } return nil } +// The os not exists error is buried inside the xattr error, +// so we cannot just use os.IsNotExists(). +func isAlreadyExists(err error) bool { + if xerr, ok := err.(*os.LinkError); ok { + if serr, ok2 := xerr.Err.(syscall.Errno); ok2 { + return serr == syscall.EEXIST + } + } + return false +} + // GetHome is called to look up the home path for a user // It is NOT supposed to return the internal path but the external path func (fs *Decomposedfs) GetHome(ctx context.Context) (string, error) { @@ -262,14 +279,22 @@ func (fs *Decomposedfs) GetPathByID(ctx context.Context, id *provider.ResourceId } // CreateDir creates the specified directory -func (fs *Decomposedfs) CreateDir(ctx context.Context, fn string) (err error) { +func (fs *Decomposedfs) CreateDir(ctx context.Context, ref *provider.Reference) (err error) { + name := path.Base(ref.Path) + if name == "" || name == "." || name == "/" { + return errtypes.BadRequest("Invalid path") + } + ref.Path = path.Dir(ref.Path) var n *node.Node - if n, err = fs.lu.NodeFromPath(ctx, fn); err != nil { + if n, err = fs.lu.NodeFromResource(ctx, ref); err != nil { + return + } + if n, err = n.Child(ctx, name); err != nil { return } if n.Exists { - return errtypes.AlreadyExists(fn) + return errtypes.AlreadyExists(ref.Path) } pn, err := n.Parent() if err != nil { @@ -399,7 +424,7 @@ func (fs *Decomposedfs) GetMD(ctx context.Context, ref *provider.Reference, mdKe return nil, errtypes.PermissionDenied(node.ID) } - return node.AsResourceInfo(ctx, &rp, mdKeys) + return node.AsResourceInfo(ctx, &rp, mdKeys, utils.IsRelativeReference(ref)) } // ListFolder returns a list of resources in the specified folder @@ -433,7 +458,7 @@ func (fs *Decomposedfs) ListFolder(ctx context.Context, ref *provider.Reference, // add this childs permissions pset := n.PermissionSet(ctx) node.AddPermissions(&np, &pset) - if ri, err := children[i].AsResourceInfo(ctx, &np, mdKeys); err == nil { + if ri, err := children[i].AsResourceInfo(ctx, &np, mdKeys, utils.IsRelativeReference(ref)); err == nil { finfos = append(finfos, ri) } } diff --git a/pkg/storage/utils/decomposedfs/decomposedfs_concurrency_test.go b/pkg/storage/utils/decomposedfs/decomposedfs_concurrency_test.go index 06dfd52d5a..00d4de1e79 100644 --- a/pkg/storage/utils/decomposedfs/decomposedfs_concurrency_test.go +++ b/pkg/storage/utils/decomposedfs/decomposedfs_concurrency_test.go @@ -138,9 +138,10 @@ var _ = Describe("Decomposed", func() { It("handle already existing directories", func() { for i := 0; i < 10; i++ { go func() { - err := fs.CreateDir(ctx, "fightforit") + defer GinkgoRecover() + err := fs.CreateDir(ctx, &provider.Reference{Path: "/fightforit"}) if err != nil { - rinfo, err := fs.GetMD(ctx, &provider.Reference{Path: "fightforit"}, nil) + rinfo, err := fs.GetMD(ctx, &provider.Reference{Path: "/fightforit"}, nil) Expect(err).ToNot(HaveOccurred()) Expect(rinfo).ToNot(BeNil()) } diff --git a/pkg/storage/utils/decomposedfs/grants.go b/pkg/storage/utils/decomposedfs/grants.go index 3b9c025859..f630f2f0fb 100644 --- a/pkg/storage/utils/decomposedfs/grants.go +++ b/pkg/storage/utils/decomposedfs/grants.go @@ -68,7 +68,7 @@ func (fs *Decomposedfs) AddGrant(ctx context.Context, ref *provider.Reference, g return err } - if err := fs.createStorageSpace("share", node.ID); err != nil { + if err := fs.createStorageSpace(ctx, "share", node.ID); err != nil { return err } diff --git a/pkg/storage/utils/decomposedfs/lookup.go b/pkg/storage/utils/decomposedfs/lookup.go index 726453f8ed..bcd9ca72e7 100644 --- a/pkg/storage/utils/decomposedfs/lookup.go +++ b/pkg/storage/utils/decomposedfs/lookup.go @@ -41,7 +41,26 @@ type Lookup struct { // NodeFromResource takes in a request path or request id and converts it to a Node func (lu *Lookup) NodeFromResource(ctx context.Context, ref *provider.Reference) (*node.Node, error) { if ref.ResourceId != nil { - return lu.NodeFromID(ctx, ref.ResourceId) + // check if a storage space reference is used + // currently, the decomposed fs uses the root node id as the space id + n, err := lu.NodeFromID(ctx, ref.ResourceId) + if err != nil { + return nil, err + } + + p := filepath.Clean(ref.Path) + if p != "." { + // walk the relative path + n, err = lu.WalkPath(ctx, n, p, func(ctx context.Context, n *node.Node) error { + return nil + }) + if err != nil { + return nil, err + } + return n, nil + } + + return n, nil } if ref.Path != "" { @@ -63,7 +82,8 @@ func (lu *Lookup) NodeFromPath(ctx context.Context, fn string) (*node.Node, erro } // TODO collect permissions of the current user on every segment - if fn != "/" { + fn = filepath.Clean(fn) + if fn != "/" && fn != "." { n, err = lu.WalkPath(ctx, n, fn, func(ctx context.Context, n *node.Node) error { log.Debug().Interface("node", n).Msg("NodeFromPath() walk") return nil diff --git a/pkg/storage/utils/decomposedfs/node/node.go b/pkg/storage/utils/decomposedfs/node/node.go index 42b2b2dc50..7233042b88 100644 --- a/pkg/storage/utils/decomposedfs/node/node.go +++ b/pkg/storage/utils/decomposedfs/node/node.go @@ -331,7 +331,7 @@ func (n *Node) PermissionSet(ctx context.Context) provider.ResourcePermissions { appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("no user in context, returning default permissions") return NoPermissions() } - if o, _ := n.Owner(); isSameUserID(u.Id, o) { + if o, _ := n.Owner(); utils.UserEqual(u.Id, o) { return OwnerPermissions() } // read the permissions for the current user from the acls of the current node @@ -443,7 +443,7 @@ func (n *Node) SetFavorite(uid *userpb.UserId, val string) error { } // AsResourceInfo return the node as CS3 ResourceInfo -func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissions, mdKeys []string) (ri *provider.ResourceInfo, err error) { +func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissions, mdKeys []string, returnBasename bool) (ri *provider.ResourceInfo, err error) { sublog := appctx.GetLogger(ctx).With().Interface("node", n).Logger() var fn string @@ -474,9 +474,13 @@ func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissi id := &provider.ResourceId{OpaqueId: n.ID} - fn, err = n.lu.Path(ctx, n) - if err != nil { - return nil, err + if returnBasename { + fn = n.Name + } else { + fn, err = n.lu.Path(ctx, n) + if err != nil { + return nil, err + } } ri = &provider.ResourceInfo{ @@ -758,7 +762,7 @@ func (n *Node) ReadUserPermissions(ctx context.Context, u *userpb.User) (ap prov // TODO what if no owner is set but grants are present? return NoOwnerPermissions(), nil } - if isSameUserID(u.Id, o) { + if utils.UserEqual(u.Id, o) { appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("user is owner, returning owner permissions") return OwnerPermissions(), nil } @@ -881,17 +885,6 @@ func (n *Node) hasUserShares(ctx context.Context) bool { return false } -func isSameUserID(i *userpb.UserId, j *userpb.UserId) bool { - switch { - case i == nil, j == nil: - return false - case i.OpaqueId == j.OpaqueId && i.Idp == j.Idp: - return true - default: - return false - } -} - func parseMTime(v string) (t time.Time, err error) { p := strings.SplitN(v, ".", 2) var sec, nsec int64 diff --git a/pkg/storage/utils/decomposedfs/node/node_test.go b/pkg/storage/utils/decomposedfs/node/node_test.go index d7bac23011..a0166bc672 100644 --- a/pkg/storage/utils/decomposedfs/node/node_test.go +++ b/pkg/storage/utils/decomposedfs/node/node_test.go @@ -172,21 +172,21 @@ var _ = Describe("Node", func() { Describe("the Etag field", func() { It("is set", func() { perms := node.OwnerPermissions() - ri, err := n.AsResourceInfo(env.Ctx, &perms, []string{}) + ri, err := n.AsResourceInfo(env.Ctx, &perms, []string{}, false) Expect(err).ToNot(HaveOccurred()) Expect(len(ri.Etag)).To(Equal(34)) }) It("changes when the tmtime is set", func() { perms := node.OwnerPermissions() - ri, err := n.AsResourceInfo(env.Ctx, &perms, []string{}) + ri, err := n.AsResourceInfo(env.Ctx, &perms, []string{}, false) Expect(err).ToNot(HaveOccurred()) Expect(len(ri.Etag)).To(Equal(34)) before := ri.Etag Expect(n.SetTMTime(time.Now().UTC())).To(Succeed()) - ri, err = n.AsResourceInfo(env.Ctx, &perms, []string{}) + ri, err = n.AsResourceInfo(env.Ctx, &perms, []string{}, false) Expect(err).ToNot(HaveOccurred()) Expect(len(ri.Etag)).To(Equal(34)) Expect(ri.Etag).ToNot(Equal(before)) diff --git a/pkg/storage/utils/decomposedfs/node/permissions.go b/pkg/storage/utils/decomposedfs/node/permissions.go index 7d0df0a46b..05a1282e86 100644 --- a/pkg/storage/utils/decomposedfs/node/permissions.go +++ b/pkg/storage/utils/decomposedfs/node/permissions.go @@ -28,6 +28,7 @@ import ( "github.com/cs3org/reva/pkg/appctx" ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/xattrs" + "github.com/cs3org/reva/pkg/utils" "github.com/pkg/errors" "github.com/pkg/xattr" ) @@ -113,7 +114,7 @@ func (p *Permissions) AssemblePermissions(ctx context.Context, n *Node) (ap prov // TODO what if no owner is set but grants are present? return NoOwnerPermissions(), nil } - if isSameUserID(u.Id, o) { + if utils.UserEqual(u.Id, o) { lp, err := n.lu.Path(ctx, n) if err == nil && lp == n.lu.ShareFolder() { return ShareFolderPermissions(), nil @@ -276,7 +277,7 @@ func (p *Permissions) getUserAndPermissions(ctx context.Context, n *Node) (*user perms := NoOwnerPermissions() return nil, &perms } - if isSameUserID(u.Id, o) { + if utils.UserEqual(u.Id, o) { appctx.GetLogger(ctx).Debug().Interface("node", n).Msg("user is owner, returning owner permissions") perms := OwnerPermissions() return u, &perms diff --git a/pkg/storage/utils/decomposedfs/testhelpers/helpers.go b/pkg/storage/utils/decomposedfs/testhelpers/helpers.go index 8b7dc40a5a..3d189f1177 100644 --- a/pkg/storage/utils/decomposedfs/testhelpers/helpers.go +++ b/pkg/storage/utils/decomposedfs/testhelpers/helpers.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/mock" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" ruser "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/storage" "github.com/cs3org/reva/pkg/storage/utils/decomposedfs" @@ -112,7 +113,7 @@ func NewTestEnv() (*TestEnv, error) { } // Create dir1 - dir1, err := env.CreateTestDir("dir1") + dir1, err := env.CreateTestDir("/dir1") if err != nil { return nil, err } @@ -124,13 +125,13 @@ func NewTestEnv() (*TestEnv, error) { } // Create subdir1 in dir1 - err = fs.CreateDir(ctx, "dir1/subdir1") + err = fs.CreateDir(ctx, &providerv1beta1.Reference{Path: "/dir1/subdir1"}) if err != nil { return nil, err } // Create emptydir - err = fs.CreateDir(ctx, "emptydir") + err = fs.CreateDir(ctx, &providerv1beta1.Reference{Path: "/emptydir"}) if err != nil { return nil, err } @@ -145,7 +146,7 @@ func (t *TestEnv) Cleanup() { // CreateTestDir create a directory and returns a corresponding Node func (t *TestEnv) CreateTestDir(name string) (*node.Node, error) { - err := t.Fs.CreateDir(t.Ctx, name) + err := t.Fs.CreateDir(t.Ctx, &providerv1beta1.Reference{Path: name}) if err != nil { return nil, err } diff --git a/pkg/storage/utils/decomposedfs/tree/tree_test.go b/pkg/storage/utils/decomposedfs/tree/tree_test.go index d7f920578e..d7e41f9f69 100644 --- a/pkg/storage/utils/decomposedfs/tree/tree_test.go +++ b/pkg/storage/utils/decomposedfs/tree/tree_test.go @@ -238,13 +238,13 @@ var _ = Describe("Tree", func() { Expect(err).ToNot(HaveOccurred()) perms := node.OwnerPermissions() - riBefore, err := dir.AsResourceInfo(env.Ctx, &perms, []string{}) + riBefore, err := dir.AsResourceInfo(env.Ctx, &perms, []string{}, false) Expect(err).ToNot(HaveOccurred()) err = env.Tree.Propagate(env.Ctx, file) Expect(err).ToNot(HaveOccurred()) - riAfter, err := dir.AsResourceInfo(env.Ctx, &perms, []string{}) + riAfter, err := dir.AsResourceInfo(env.Ctx, &perms, []string{}, false) Expect(err).ToNot(HaveOccurred()) Expect(riAfter.Etag).ToNot(Equal(riBefore.Etag)) }) diff --git a/pkg/storage/utils/decomposedfs/upload.go b/pkg/storage/utils/decomposedfs/upload.go index f4b8e47e14..b986ba384d 100644 --- a/pkg/storage/utils/decomposedfs/upload.go +++ b/pkg/storage/utils/decomposedfs/upload.go @@ -114,7 +114,7 @@ func (fs *Decomposedfs) InitiateUpload(ctx context.Context, ref *provider.Refere log := appctx.GetLogger(ctx) - n, err := fs.lookupNode(ctx, ref.Path) + n, err := fs.lu.NodeFromResource(ctx, ref) if err != nil { return nil, err } diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index 798b1cbe4c..73b66c9b71 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -1158,12 +1158,17 @@ func (fs *eosfs) createUserDir(ctx context.Context, u *userpb.User, path string, return nil } -func (fs *eosfs) CreateDir(ctx context.Context, p string) error { +func (fs *eosfs) CreateDir(ctx context.Context, ref *provider.Reference) error { log := appctx.GetLogger(ctx) u, err := getUser(ctx) if err != nil { return errors.Wrap(err, "eosfs: no user in ctx") } + p, err := fs.resolve(ctx, ref) + if err != nil { + return nil + } + auth, err := fs.getUserAuth(ctx, u, p) if err != nil { return err diff --git a/pkg/storage/utils/localfs/localfs.go b/pkg/storage/utils/localfs/localfs.go index c3a5aaab22..2af306dde0 100644 --- a/pkg/storage/utils/localfs/localfs.go +++ b/pkg/storage/utils/localfs/localfs.go @@ -751,7 +751,12 @@ func (fs *localfs) createHomeInternal(ctx context.Context, fn string) error { return nil } -func (fs *localfs) CreateDir(ctx context.Context, fn string) error { +func (fs *localfs) CreateDir(ctx context.Context, ref *provider.Reference) error { + + fn, err := fs.resolve(ctx, ref) + if err != nil { + return nil + } if fs.isShareFolder(ctx, fn) { return errtypes.PermissionDenied("localfs: cannot create folder under the share folder") @@ -761,7 +766,7 @@ func (fs *localfs) CreateDir(ctx context.Context, fn string) error { if _, err := os.Stat(fn); err == nil { return errtypes.AlreadyExists(fn) } - err := os.Mkdir(fn, 0700) + err = os.Mkdir(fn, 0700) if err != nil { if os.IsNotExist(err) { return errtypes.NotFound(fn) diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 13c28007dc..244a065dc9 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -19,6 +19,7 @@ package utils import ( + "fmt" "math/rand" "net" "net/http" @@ -218,6 +219,12 @@ func IsAbsoluteReference(ref *provider.Reference) bool { return (ref.ResourceId != nil && ref.Path == "") || (ref.ResourceId == nil) && strings.HasPrefix(ref.Path, "/") } +// IsAbsolutePathReference returns true if the given reference qualifies as a global path +// when only the path is set and starts with / +func IsAbsolutePathReference(ref *provider.Reference) bool { + return ref.ResourceId == nil && strings.HasPrefix(ref.Path, "/") +} + // MakeRelativePath prefixes the path with a . to use it in a relative reference func MakeRelativePath(p string) string { p = path.Join("/", p) @@ -271,3 +278,16 @@ func UserTypeToString(accountType userpb.UserType) string { } return t } + +// SplitStorageSpaceID can be used to split `storagespaceid` into `storageid` and `nodeid` +// Currently they are built using `!` in the decomposedfs, but other drivers might return different ids. +// any place in the code that relies on this function should instead use the storage registry to look up the responsible storage provider. +// Note: This would in effect change the storage registry into a storage space registry. +func SplitStorageSpaceID(ssid string) (storageid, nodeid string, err error) { + // query that specific storage provider + parts := strings.SplitN(ssid, "!", 2) + if len(parts) != 2 { + return "", "", fmt.Errorf("storage space id must be separated by '!'") + } + return parts[0], parts[1], nil +}