From c5f3bf505cd1056693880998af8b7d63c9889b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Fri, 18 Jun 2021 09:00:29 +0000 Subject: [PATCH 1/5] initial list spaces implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- .../grpc/services/gateway/storageprovider.go | 119 ++++++++++-- .../storageprovider/storageprovider.go | 38 +++- pkg/storage/fs/owncloud/owncloud.go | 4 + pkg/storage/fs/owncloudsql/owncloudsql.go | 5 + pkg/storage/fs/s3/s3.go | 4 + pkg/storage/registry/static/static.go | 29 +-- pkg/storage/storage.go | 1 + .../utils/decomposedfs/decomposedfs.go | 183 ++++++++++++++++++ pkg/storage/utils/decomposedfs/grants.go | 5 + pkg/storage/utils/decomposedfs/grants_test.go | 31 ++- pkg/storage/utils/decomposedfs/tree/tree.go | 64 ++++++ pkg/storage/utils/eosfs/eosfs.go | 4 + pkg/storage/utils/localfs/localfs.go | 4 + 13 files changed, 455 insertions(+), 36 deletions(-) diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 466bb7f303..e7bafde985 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -115,37 +115,118 @@ func (s *svc) CreateStorageSpace(ctx context.Context, req *provider.CreateStorag func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSpacesRequest) (*provider.ListStorageSpacesResponse, error) { log := appctx.GetLogger(ctx) - // TODO: needs to be fixed var id *provider.StorageSpaceId for _, f := range req.Filters { if f.Type == provider.ListStorageSpacesRequest_Filter_TYPE_ID { id = f.GetId() } } - parts := strings.SplitN(id.OpaqueId, "!", 2) - if len(parts) != 2 { - return &provider.ListStorageSpacesResponse{ - Status: status.NewInvalidArg(ctx, "space id must be separated by !"), - }, nil + + var providers []*registry.ProviderInfo + var err error + c, err := pool.GetStorageRegistryClient(s.c.StorageRegistryEndpoint) + if err != nil { + return nil, errors.Wrap(err, "gateway: error getting storage registry client") } - 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], - }}) + + if id != nil { + // query that specific story provider + parts := strings.SplitN(id.OpaqueId, "!", 2) + if len(parts) != 2 { + 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], + }}, + }) + if err != nil { + return &provider.ListStorageSpacesResponse{ + Status: status.NewStatusFromErrType(ctx, "ListStorageSpaces filters: req "+req.String(), err), + }, nil + } + if res.Status.Code != rpc.Code_CODE_OK { + return &provider.ListStorageSpacesResponse{ + Status: res.Status, + }, nil + } + providers = res.Providers + } else { + // get list of all storage providers + res, err := c.ListStorageProviders(ctx, ®istry.ListStorageProvidersRequest{}) + + if err != nil { + return &provider.ListStorageSpacesResponse{ + Status: status.NewStatusFromErrType(ctx, "error listing providers", err), + }, nil + } + if res.Status.Code != rpc.Code_CODE_OK { + return &provider.ListStorageSpacesResponse{ + Status: res.Status, + }, nil + } + + providers = []*registry.ProviderInfo{} + // FIXME filter only providers that have an id set ... currently none have? + // bug? only ProviderPath is set + for i := range res.Providers { + // use only providers whose path does not start with a /? + if strings.HasPrefix(res.Providers[i].ProviderPath, "/") { + continue + } + providers = append(providers, res.Providers[i]) + } + } + + spacesFromProviders := make([][]*provider.StorageSpace, len(providers)) + errors := make([]error, len(providers)) + var wg sync.WaitGroup + + for i, p := range providers { + wg.Add(1) + go s.listStorageSpacesOnProvider(ctx, req, &spacesFromProviders[i], p, &errors[i], &wg) + } + wg.Wait() + + uniqueSpaces := map[string]*provider.StorageSpace{} + for i := range providers { + if errors[i] != nil { + log.Debug().Err(errors[i]).Msg("skipping provider") + continue + } + for j := range spacesFromProviders[i] { + uniqueSpaces[spacesFromProviders[i][j].Id.OpaqueId] = spacesFromProviders[i][j] + } + } + spaces := []*provider.StorageSpace{} + for spaceID := range uniqueSpaces { + spaces = append(spaces, uniqueSpaces[spaceID]) + } + + return &provider.ListStorageSpacesResponse{ + Status: status.NewOK(ctx), + StorageSpaces: spaces, + }, nil +} + +func (s *svc) listStorageSpacesOnProvider(ctx context.Context, req *provider.ListStorageSpacesRequest, res *[]*provider.StorageSpace, p *registry.ProviderInfo, e *error, wg *sync.WaitGroup) { + defer wg.Done() + c, err := s.getStorageProviderClient(ctx, p) if err != nil { - return &provider.ListStorageSpacesResponse{ - Status: status.NewStatusFromErrType(ctx, "error finding path", err), - }, nil + *e = errors.Wrap(err, "error connecting to storage provider="+p.Address) + return } - res, err := c.ListStorageSpaces(ctx, req) + r, err := c.ListStorageSpaces(ctx, req) if err != nil { - log.Err(err).Msg("gateway: error listing storage space on storage provider") - return &provider.ListStorageSpacesResponse{ - Status: status.NewInternal(ctx, err, "error calling ListStorageSpaces"), - }, nil + *e = errors.Wrap(err, "gateway: error calling ListStorageSpaces") + return } - return res, nil + + *res = r.StorageSpaces } func (s *svc) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) { diff --git a/internal/grpc/services/storageprovider/storageprovider.go b/internal/grpc/services/storageprovider/storageprovider.go index cc2f296c55..0fe2a3600f 100644 --- a/internal/grpc/services/storageprovider/storageprovider.go +++ b/internal/grpc/services/storageprovider/storageprovider.go @@ -428,9 +428,45 @@ func (s *service) CreateStorageSpace(ctx context.Context, req *provider.CreateSt }, nil } +func hasNodeID(s *provider.StorageSpace) bool { + return s != nil && s.Root != nil && s.Root.OpaqueId != "" +} + func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSpacesRequest) (*provider.ListStorageSpacesResponse, error) { + spaces, err := s.storage.ListStorageSpaces(ctx, req.Filters) + if err != nil { + var st *rpc.Status + switch err.(type) { + case errtypes.IsNotFound: + st = status.NewNotFound(ctx, "not found when listing spaces") + case errtypes.PermissionDenied: + st = status.NewPermissionDenied(ctx, err, "permission denied") + case errtypes.NotSupported: + st = status.NewUnimplemented(ctx, err, "not implemented") + default: + st = status.NewInternal(ctx, err, "error listing spaces") + } + return &provider.ListStorageSpacesResponse{ + Status: st, + }, nil + } + + for i := range spaces { + if hasNodeID(spaces[i]) { + // fill in storagespace id if it is not set + if spaces[i].Id == nil || spaces[i].Id.OpaqueId == "" { + spaces[i].Id = &provider.StorageSpaceId{OpaqueId: s.mountID + "!" + spaces[i].Root.OpaqueId} + } + // fill in storage id if it is not set + if spaces[i].Root.StorageId == "" { + spaces[i].Root.StorageId = s.mountID + } + } + } + return &provider.ListStorageSpacesResponse{ - Status: status.NewUnimplemented(ctx, errtypes.NotSupported("ListStorageSpaces not implemented"), "ListStorageSpaces not implemented"), + Status: status.NewOK(ctx), + StorageSpaces: spaces, }, nil } diff --git a/pkg/storage/fs/owncloud/owncloud.go b/pkg/storage/fs/owncloud/owncloud.go index 1e59ecb1e0..27c202aa5b 100644 --- a/pkg/storage/fs/owncloud/owncloud.go +++ b/pkg/storage/fs/owncloud/owncloud.go @@ -2221,6 +2221,10 @@ func (fs *ocfs) RestoreRecycleItem(ctx context.Context, key string, restoreRef * return fs.propagate(ctx, tgt) } +func (fs *ocfs) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) { + return nil, errtypes.NotSupported("list storage spaces") +} + func (fs *ocfs) propagate(ctx context.Context, leafPath string) error { var root string if fs.c.EnableHome { diff --git a/pkg/storage/fs/owncloudsql/owncloudsql.go b/pkg/storage/fs/owncloudsql/owncloudsql.go index 91625ae327..b9b31a40a5 100644 --- a/pkg/storage/fs/owncloudsql/owncloudsql.go +++ b/pkg/storage/fs/owncloudsql/owncloudsql.go @@ -2132,6 +2132,11 @@ func (fs *ocfs) HashFile(path string) (string, string, string, error) { } } +func (fs *ocfs) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) { + // TODO(corby): Implement + return nil, nil +} + func readChecksumIntoResourceChecksum(ctx context.Context, checksums, algo string, ri *provider.ResourceInfo) { re := regexp.MustCompile(strings.ToUpper(algo) + `:(.*)`) matches := re.FindStringSubmatch(checksums) diff --git a/pkg/storage/fs/s3/s3.go b/pkg/storage/fs/s3/s3.go index 83c1fe1db8..38c3b6b4f1 100644 --- a/pkg/storage/fs/s3/s3.go +++ b/pkg/storage/fs/s3/s3.go @@ -661,3 +661,7 @@ func (fs *s3FS) ListRecycle(ctx context.Context) ([]*provider.RecycleItem, error func (fs *s3FS) RestoreRecycleItem(ctx context.Context, key string, restoreRef *provider.Reference) error { return errtypes.NotSupported("restore recycle") } + +func (fs *s3FS) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) { + return nil, errtypes.NotSupported("list storage spaces") +} diff --git a/pkg/storage/registry/static/static.go b/pkg/storage/registry/static/static.go index 3074dd467e..69ce5e27cc 100644 --- a/pkg/storage/registry/static/static.go +++ b/pkg/storage/registry/static/static.go @@ -145,18 +145,25 @@ func (b *reg) FindProviders(ctx context.Context, ref *provider.Reference) ([]*re // If the reference has a resource id set, use it to route if ref.ResourceId != nil { - for prefix, rule := range b.c.Rules { - addr := getProviderAddr(ctx, rule) - r, err := regexp.Compile("^" + prefix + "$") - if err != nil { - continue + if ref.ResourceId.StorageId != "" { + for prefix, rule := range b.c.Rules { + addr := getProviderAddr(ctx, rule) + r, err := regexp.Compile("^" + prefix + "$") + if err != nil { + continue + } + // TODO(labkode): fill path info based on provider id, if path and storage id points to same id, take that. + if m := r.FindString(ref.ResourceId.StorageId); m != "" { + return []*registrypb.ProviderInfo{{ + ProviderId: ref.ResourceId.StorageId, + Address: addr, + }}, nil + } } - // TODO(labkode): fill path info based on provider id, if path and storage id points to same id, take that. - if m := r.FindString(ref.ResourceId.StorageId); m != "" { - return []*registrypb.ProviderInfo{{ - ProviderId: ref.ResourceId.StorageId, - Address: addr, - }}, nil + // TODO if the storage id is not set but node id is set we could poll all storage providers to check if the node is known there + // for now, say the reference is invalid + if ref.ResourceId.OpaqueId != "" { + return nil, errtypes.BadRequest("invalid reference " + ref.String()) } } } diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 8481c45a98..627a649a00 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -56,6 +56,7 @@ type FS interface { Shutdown(ctx context.Context) error SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error + ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) } // Registry is the interface that storage registries implement diff --git a/pkg/storage/utils/decomposedfs/decomposedfs.go b/pkg/storage/utils/decomposedfs/decomposedfs.go index 3d55a99a19..e524bf2d40 100644 --- a/pkg/storage/utils/decomposedfs/decomposedfs.go +++ b/pkg/storage/utils/decomposedfs/decomposedfs.go @@ -23,14 +23,18 @@ package decomposedfs import ( "context" + "fmt" "io" + "math" "net/url" "os" "path/filepath" "strconv" "strings" + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/logger" @@ -42,6 +46,7 @@ import ( "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/xattrs" "github.com/cs3org/reva/pkg/storage/utils/templates" "github.com/cs3org/reva/pkg/user" + "github.com/cs3org/reva/pkg/utils" "github.com/pkg/errors" "github.com/pkg/xattr" ) @@ -210,9 +215,31 @@ func (fs *Decomposedfs) CreateHome(ctx context.Context) (err error) { return } } + + // add storage space + if err := fs.createStorageSpace("personal", h.ID); err != nil { + return err + } + return } +func (fs *Decomposedfs) createStorageSpace(spaceType, nodeID string) error { + + // create space type dir + if err := os.MkdirAll(filepath.Join(fs.o.Root, "spaces", spaceType), 0700); err != nil { + return err + } + + // 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 personal space %s, %s\n", nodeID, err) + } + + return nil +} + // 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) { @@ -465,6 +492,162 @@ func (fs *Decomposedfs) Download(ctx context.Context, ref *provider.Reference) ( return reader, nil } +// ListStorageSpaces returns a list of StorageSpaces. +// The list can be filtered by space type or space id. +// Spaces are persisted with symlinks in /spaces// pointing to ../../nodes/, the root node of the space +// The spaceid is a concatenation of storageid + "!" + nodeid +func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) { + // TODO check filters + + // TODO when a space symlink is broken delete the space for cleanup + // read permissions are deduced from the node? + + // TODO for absolute references this actually requires us to move all user homes into a subfolder of /nodes/root, + // e.g. /nodes/root/ otherwise storage space names might collide even though they are of different types + // /nodes/root/personal/foo and /nodes/root/shares/foo might be two very different spaces, a /nodes/root/foo is not expressive enough + // we would not need /nodes/root if access always happened via spaceid+relative path + + spaceType := "*" + spaceID := "*" + + for i := range filter { + switch filter[i].Type { + case provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE: + spaceType = filter[i].GetSpaceType() + case provider.ListStorageSpacesRequest_Filter_TYPE_ID: + parts := strings.SplitN(filter[i].GetId().OpaqueId, "!", 2) + if len(parts) == 2 { + spaceID = parts[1] + } + } + } + + // build the glob path, eg. + // /path/to/root/spaces/personal/nodeid + // /path/to/root/spaces/shared/nodeid + matches, err := filepath.Glob(filepath.Join(fs.o.Root, "spaces", spaceType, spaceID)) + if err != nil { + return nil, err + } + + var spaces []*provider.StorageSpace + + u, ok := user.ContextGetUser(ctx) + if !ok { + appctx.GetLogger(ctx).Debug().Msg("expected user in context") + return spaces, nil + } + + for i := range matches { + // always read link in case storage space id != node id + if target, err := os.Readlink(matches[i]); err != nil { + appctx.GetLogger(ctx).Error().Err(err).Str("match", matches[i]).Msg("could not read link, skipping") + continue + } else { + n, err := node.ReadNode(ctx, fs.lu, filepath.Base(target)) + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Str("id", filepath.Base(target)).Msg("could not read node, skipping") + continue + } + owner, err := n.Owner() + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not read owner, skipping") + continue + } + + // TODO apply more filters + + // build return value + + space := &provider.StorageSpace{ + // FIXME the driver should know its id move setting the spaceid from the storage provider to the drivers + //Id: &provider.StorageSpaceId{OpaqueId: "1284d238-aa92-42ce-bdc4-0b0000009157!" + n.ID}, + Root: &provider.ResourceId{ + // FIXME the driver should know its id move setting the spaceid from the storage provider to the drivers + //StorageId: "1284d238-aa92-42ce-bdc4-0b0000009157", + OpaqueId: n.ID, + }, + Name: n.Name, + SpaceType: filepath.Base(filepath.Dir(matches[i])), + // Mtime is set either as node.tmtime or as fi.mtime below + } + + if space.SpaceType == "share" { + if utils.UserEqual(u.Id, owner) { + // do not list shares as spaces for the owner + continue + } + // return folder name? + space.Name = n.Name + } else { + space.Name = "root" // do not expose the id as name, this is the root of a space + // TODO read from extended attribute for project / group spaces + } + + // filter out spaces user cannot access (currently based on stat permission) + p, err := n.ReadUserPermissions(ctx, u) + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not read permissions, skipping") + continue + } + if !p.Stat { + continue + } + + // fill in user object if the current user is the owner + if utils.UserEqual(u.Id, owner) { + space.Owner = u + } else { + space.Owner = &userv1beta1.User{ // FIXME only return a UserID, not a full blown user object + Id: owner, + } + } + + // we set the space mtime to the root item mtime + // override the stat mtime with a tmtime if it is present + if tmt, err := n.GetTMTime(); err == nil { + un := tmt.UnixNano() + space.Mtime = &types.Timestamp{ + Seconds: uint64(un / 1000000000), + Nanos: uint32(un % 1000000000), + } + } else if fi, err := os.Stat(matches[i]); err == nil { + // fall back to stat mtime + un := fi.ModTime().UnixNano() + space.Mtime = &types.Timestamp{ + Seconds: uint64(un / 1000000000), + Nanos: uint32(un % 1000000000), + } + } + + // quota + v, err := xattr.Get(matches[i], xattrs.QuotaAttr) + if err == nil { + // make sure we have a proper signed int + // we use the same magic numbers to indicate: + // -1 = uncalculated + // -2 = unknown + // -3 = unlimited + if quota, err := strconv.ParseInt(string(v), 10, 64); err == nil { + if quota >= 0 { + space.Quota = &provider.Quota{ + QuotaMaxBytes: uint64(quota), + QuotaMaxFiles: math.MaxUint64, // TODO MaxUInt64? = unlimited? why even max files? 0 = unlimited? + } + } + } else { + appctx.GetLogger(ctx).Debug().Err(err).Str("nodepath", matches[i]).Msg("could not read quota") + } + } + + spaces = append(spaces, space) + } + } + + return spaces, nil + +} + func (fs *Decomposedfs) copyMD(s string, t string) (err error) { var attrs []string if attrs, err = xattr.List(s); err != nil { diff --git a/pkg/storage/utils/decomposedfs/grants.go b/pkg/storage/utils/decomposedfs/grants.go index 09d4465a72..bb8a9ff431 100644 --- a/pkg/storage/utils/decomposedfs/grants.go +++ b/pkg/storage/utils/decomposedfs/grants.go @@ -62,6 +62,11 @@ func (fs *Decomposedfs) AddGrant(ctx context.Context, ref *provider.Reference, g if err := xattr.Set(np, xattrs.GrantPrefix+principal, value); err != nil { return err } + + if err := fs.createStorageSpace("share", node.ID); err != nil { + return err + } + return fs.tp.Propagate(ctx, node) } diff --git a/pkg/storage/utils/decomposedfs/grants_test.go b/pkg/storage/utils/decomposedfs/grants_test.go index 5224d2fb66..953b994c55 100644 --- a/pkg/storage/utils/decomposedfs/grants_test.go +++ b/pkg/storage/utils/decomposedfs/grants_test.go @@ -19,25 +19,35 @@ package decomposedfs_test import ( + "io/fs" + "os" "path" + "path/filepath" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" helpers "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/testhelpers" "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/xattrs" - "github.com/pkg/xattr" - "github.com/stretchr/testify/mock" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + "github.com/pkg/xattr" + "github.com/stretchr/testify/mock" ) +type testFS struct { + root string +} + +func (t testFS) Open(name string) (fs.File, error) { + return os.Open(filepath.Join(t.root, name)) +} + var _ = Describe("Grants", func() { var ( - env *helpers.TestEnv - + env *helpers.TestEnv ref *provider.Reference grant *provider.Grant + tfs = &testFS{} ) BeforeEach(func() { @@ -103,6 +113,17 @@ var _ = Describe("Grants", func() { Expect(err).ToNot(HaveOccurred()) Expect(string(attr)).To(Equal("\x00t=A:f=:p=rw")) }) + + It("creates a storage space per created grant", func() { + err := env.Fs.AddGrant(env.Ctx, ref, grant) + Expect(err).ToNot(HaveOccurred()) + + spacesPath := filepath.Join(env.Root, "spaces") + tfs.root = spacesPath + entries, err := fs.ReadDir(tfs, "share") + Expect(err).ToNot(HaveOccurred()) + Expect(len(entries)).To(BeNumerically(">=", 1)) + }) }) Describe("ListGrants", func() { diff --git a/pkg/storage/utils/decomposedfs/tree/tree.go b/pkg/storage/utils/decomposedfs/tree/tree.go index e9ec02d33b..af74301078 100644 --- a/pkg/storage/utils/decomposedfs/tree/tree.go +++ b/pkg/storage/utils/decomposedfs/tree/tree.go @@ -115,9 +115,73 @@ func (t *Tree) Setup(owner string) error { if err != nil { return err } + + // create spaces folder and iterate over existing nodes to populate it + spacesPath := filepath.Join(t.root, "spaces") + fi, err := os.Stat(spacesPath) + if os.IsNotExist(err) { + // create personal spaces dir + if err := os.MkdirAll(filepath.Join(spacesPath, "personal"), 0700); err != nil { + return err + } + // create share spaces dir + if err := os.MkdirAll(filepath.Join(spacesPath, "share"), 0700); err != nil { + return err + } + + f, err := os.Open(filepath.Join(t.root, "nodes")) + if err != nil { + return err + } + nodes, err := f.Readdir(0) + if err != nil { + return err + } + + for i := range nodes { + nodePath := filepath.Join(t.root, "nodes", nodes[i].Name()) + + // is it a user root? -> create personal space + if isRootNode(nodePath) { + // create personal space + // we can reuse the node id as the space id + err = os.Symlink("../../nodes/"+nodes[i].Name(), filepath.Join(t.root, "spaces/personal", nodes[i].Name())) + if err != nil { + fmt.Printf("could not create symlink for personal space %s, %s\n", nodes[i].Name(), err) + } + } + + // is it a shared node? -> create shared space + if isSharedNode(nodePath) { + err = os.Symlink("../../nodes/"+nodes[i].Name(), filepath.Join(t.root, "spaces/share", nodes[i].Name())) + if err != nil { + fmt.Printf("could not create symlink for shared space %s, %s\n", nodes[i].Name(), err) + } + } + } + } else if !fi.IsDir() { + // check if it is a directory + return fmt.Errorf("%s is not a directory", spacesPath) + } + return nil } +func isRootNode(nodePath string) bool { + attrBytes, err := xattr.Get(nodePath, xattrs.ParentidAttr) + return err == nil && string(attrBytes) == "root" +} +func isSharedNode(nodePath string) bool { + if attrs, err := xattr.List(nodePath); err == nil { + for i := range attrs { + if strings.HasPrefix(attrs[i], xattrs.GrantPrefix) { + return true + } + } + } + return false +} + // GetMD returns the metadata of a node in the tree func (t *Tree) GetMD(ctx context.Context, n *node.Node) (os.FileInfo, error) { md, err := os.Stat(n.InternalPath()) diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index 39e0186cea..52d724a64f 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -1425,6 +1425,10 @@ func (fs *eosfs) RestoreRecycleItem(ctx context.Context, key string, restoreRef return fs.c.RestoreDeletedEntry(ctx, uid, gid, key) } +func (fs *eosfs) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) { + return nil, errtypes.NotSupported("list storage spaces") +} + func (fs *eosfs) convertToRecycleItem(ctx context.Context, eosDeletedItem *eosclient.DeletedEntry) (*provider.RecycleItem, error) { path, err := fs.unwrap(ctx, eosDeletedItem.RestorePath) if err != nil { diff --git a/pkg/storage/utils/localfs/localfs.go b/pkg/storage/utils/localfs/localfs.go index d9c7e13870..5416f6fa39 100644 --- a/pkg/storage/utils/localfs/localfs.go +++ b/pkg/storage/utils/localfs/localfs.go @@ -1245,6 +1245,10 @@ func (fs *localfs) RestoreRecycleItem(ctx context.Context, restoreKey string, re return fs.propagate(ctx, localRestorePath) } +func (fs *localfs) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) { + return nil, errtypes.NotSupported("list storage spaces") +} + func (fs *localfs) propagate(ctx context.Context, leafPath string) error { var root string From 6b0404a9ed0dce930b1e7a74a14856f8fa590b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Mon, 21 Jun 2021 21:09:12 +0000 Subject: [PATCH 2/5] add changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- changelog/unreleased/list-spaces.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changelog/unreleased/list-spaces.md diff --git a/changelog/unreleased/list-spaces.md b/changelog/unreleased/list-spaces.md new file mode 100644 index 0000000000..3a640879f4 --- /dev/null +++ b/changelog/unreleased/list-spaces.md @@ -0,0 +1,6 @@ +Enhancement: Introduce list spaces + +The ListStorageSpaces call now allows listing all user homes and shared resources using a storage space id. The gateway will forward requests to a specific storage provider when a filter by id is given. Otherwise it will query all storage providers. Results will be deduplicated. Currently, only the decomposed fs storage driver implements the necessary logic to demonstrate the implmentation. A new `/dav/spaces` WebDAV endpoint to directly access a storage space is introduced in a separate PR. + +https://github.com/cs3org/reva/pull/1802 +https://github.com/cs3org/reva/pull/1803 \ No newline at end of file From 629888862269c41f978d86b6a9454d1e3bb7a2a0 Mon Sep 17 00:00:00 2001 From: David Christofas Date: Thu, 24 Jun 2021 14:39:18 +0200 Subject: [PATCH 3/5] clean up code --- internal/grpc/services/gateway/storageprovider.go | 4 ++-- pkg/storage/fs/owncloudsql/owncloudsql.go | 2 +- pkg/storage/utils/decomposedfs/decomposedfs.go | 12 +++++------- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index e7bafde985..c944a29351 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -169,7 +169,7 @@ func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSp }, nil } - providers = []*registry.ProviderInfo{} + providers = make([]*registry.ProviderInfo, 0, len(res.Providers)) // FIXME filter only providers that have an id set ... currently none have? // bug? only ProviderPath is set for i := range res.Providers { @@ -201,7 +201,7 @@ func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSp uniqueSpaces[spacesFromProviders[i][j].Id.OpaqueId] = spacesFromProviders[i][j] } } - spaces := []*provider.StorageSpace{} + spaces := make([]*provider.StorageSpace, 0, len(uniqueSpaces)) for spaceID := range uniqueSpaces { spaces = append(spaces, uniqueSpaces[spaceID]) } diff --git a/pkg/storage/fs/owncloudsql/owncloudsql.go b/pkg/storage/fs/owncloudsql/owncloudsql.go index b9b31a40a5..589338da11 100644 --- a/pkg/storage/fs/owncloudsql/owncloudsql.go +++ b/pkg/storage/fs/owncloudsql/owncloudsql.go @@ -2134,7 +2134,7 @@ func (fs *ocfs) HashFile(path string) (string, string, string, error) { func (fs *ocfs) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) { // TODO(corby): Implement - return nil, nil + return nil, errtypes.NotSupported("list storage spaces") } func readChecksumIntoResourceChecksum(ctx context.Context, checksums, algo string, ri *provider.ResourceInfo) { diff --git a/pkg/storage/utils/decomposedfs/decomposedfs.go b/pkg/storage/utils/decomposedfs/decomposedfs.go index e524bf2d40..d2f649d454 100644 --- a/pkg/storage/utils/decomposedfs/decomposedfs.go +++ b/pkg/storage/utils/decomposedfs/decomposedfs.go @@ -530,7 +530,7 @@ func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provide return nil, err } - var spaces []*provider.StorageSpace + spaces := make([]*provider.StorageSpace, 0, len(matches)) u, ok := user.ContextGetUser(ctx) if !ok { @@ -628,12 +628,10 @@ func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provide // -1 = uncalculated // -2 = unknown // -3 = unlimited - if quota, err := strconv.ParseInt(string(v), 10, 64); err == nil { - if quota >= 0 { - space.Quota = &provider.Quota{ - QuotaMaxBytes: uint64(quota), - QuotaMaxFiles: math.MaxUint64, // TODO MaxUInt64? = unlimited? why even max files? 0 = unlimited? - } + if quota, err := strconv.ParseUint(string(v), 10, 64); err == nil { + space.Quota = &provider.Quota{ + QuotaMaxBytes: quota, + QuotaMaxFiles: math.MaxUint64, // TODO MaxUInt64? = unlimited? why even max files? 0 = unlimited? } } else { appctx.GetLogger(ctx).Debug().Err(err).Str("nodepath", matches[i]).Msg("could not read quota") From b6b040f1204a5d3e77067e3a13e79699fea7a0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Mon, 28 Jun 2021 14:43:08 +0200 Subject: [PATCH 4/5] Apply suggestions from code review address small review items Co-authored-by: Alex Unger <6905948+refs@users.noreply.github.com> --- internal/grpc/services/gateway/storageprovider.go | 10 ++++++---- pkg/storage/utils/decomposedfs/decomposedfs.go | 11 +++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index c944a29351..0de25d099b 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -122,15 +122,17 @@ func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSp } } - var providers []*registry.ProviderInfo - var err error + var ( + providers []*registry.ProviderInfo + err error + ) c, err := pool.GetStorageRegistryClient(s.c.StorageRegistryEndpoint) if err != nil { return nil, errors.Wrap(err, "gateway: error getting storage registry client") } if id != nil { - // query that specific story provider + // query that specific storage provider parts := strings.SplitN(id.OpaqueId, "!", 2) if len(parts) != 2 { return &provider.ListStorageSpacesResponse{ @@ -183,8 +185,8 @@ func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSp spacesFromProviders := make([][]*provider.StorageSpace, len(providers)) errors := make([]error, len(providers)) - var wg sync.WaitGroup + var wg sync.WaitGroup for i, p := range providers { wg.Add(1) go s.listStorageSpacesOnProvider(ctx, req, &spacesFromProviders[i], p, &errors[i], &wg) diff --git a/pkg/storage/utils/decomposedfs/decomposedfs.go b/pkg/storage/utils/decomposedfs/decomposedfs.go index d2f649d454..03208cbc95 100644 --- a/pkg/storage/utils/decomposedfs/decomposedfs.go +++ b/pkg/storage/utils/decomposedfs/decomposedfs.go @@ -234,7 +234,7 @@ 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 personal space %s, %s\n", nodeID, err) + fmt.Printf("could not create symlink for '%s' space %s, %s\n", spaceType, nodeID, err) } return nil @@ -507,8 +507,10 @@ func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provide // /nodes/root/personal/foo and /nodes/root/shares/foo might be two very different spaces, a /nodes/root/foo is not expressive enough // we would not need /nodes/root if access always happened via spaceid+relative path - spaceType := "*" - spaceID := "*" + var ( + spaceType = "*" + spaceID = "*" + ) for i := range filter { switch filter[i].Type { @@ -557,8 +559,6 @@ func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provide // TODO apply more filters - // build return value - space := &provider.StorageSpace{ // FIXME the driver should know its id move setting the spaceid from the storage provider to the drivers //Id: &provider.StorageSpaceId{OpaqueId: "1284d238-aa92-42ce-bdc4-0b0000009157!" + n.ID}, @@ -577,7 +577,6 @@ func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provide // do not list shares as spaces for the owner continue } - // return folder name? space.Name = n.Name } else { space.Name = "root" // do not expose the id as name, this is the root of a space From 55c5bde163b731adb7e2a5ca45178e1aa0485958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn=20Friedrich=20Dreyer?= Date: Wed, 7 Jul 2021 16:09:20 +0200 Subject: [PATCH 5/5] incorporate review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jörn Friedrich Dreyer --- internal/grpc/services/gateway/storageprovider.go | 14 ++++++++++++-- .../services/storageprovider/storageprovider.go | 3 +++ pkg/storage/utils/decomposedfs/decomposedfs.go | 1 - 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 0de25d099b..80fa004610 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -196,8 +196,13 @@ func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSp uniqueSpaces := map[string]*provider.StorageSpace{} for i := range providers { if errors[i] != nil { - log.Debug().Err(errors[i]).Msg("skipping provider") - continue + if len(providers) > 1 { + log.Debug().Err(errors[i]).Msg("skipping provider") + continue + } + return &provider.ListStorageSpacesResponse{ + Status: status.NewStatusFromErrType(ctx, "error listing space", errors[i]), + }, nil } for j := range spacesFromProviders[i] { uniqueSpaces[spacesFromProviders[i][j].Id.OpaqueId] = spacesFromProviders[i][j] @@ -207,6 +212,11 @@ func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSp for spaceID := range uniqueSpaces { spaces = append(spaces, uniqueSpaces[spaceID]) } + if len(spaces) == 0 { + return &provider.ListStorageSpacesResponse{ + Status: status.NewNotFound(ctx, "space not found"), + }, nil + } return &provider.ListStorageSpacesResponse{ Status: status.NewOK(ctx), diff --git a/internal/grpc/services/storageprovider/storageprovider.go b/internal/grpc/services/storageprovider/storageprovider.go index 0fe2a3600f..9c8529971e 100644 --- a/internal/grpc/services/storageprovider/storageprovider.go +++ b/internal/grpc/services/storageprovider/storageprovider.go @@ -433,6 +433,7 @@ func hasNodeID(s *provider.StorageSpace) bool { } func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSpacesRequest) (*provider.ListStorageSpacesResponse, error) { + log := appctx.GetLogger(ctx) spaces, err := s.storage.ListStorageSpaces(ctx, req.Filters) if err != nil { var st *rpc.Status @@ -461,6 +462,8 @@ func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStora if spaces[i].Root.StorageId == "" { spaces[i].Root.StorageId = s.mountID } + } else if spaces[i].Id == nil || spaces[i].Id.OpaqueId == "" { + log.Warn().Str("service", "storageprovider").Str("driver", s.conf.Driver).Interface("space", spaces[i]).Msg("space is missing space id and root id") } } diff --git a/pkg/storage/utils/decomposedfs/decomposedfs.go b/pkg/storage/utils/decomposedfs/decomposedfs.go index 03208cbc95..eb31b6eb97 100644 --- a/pkg/storage/utils/decomposedfs/decomposedfs.go +++ b/pkg/storage/utils/decomposedfs/decomposedfs.go @@ -577,7 +577,6 @@ func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provide // do not list shares as spaces for the owner continue } - space.Name = n.Name } else { space.Name = "root" // do not expose the id as name, this is the root of a space // TODO read from extended attribute for project / group spaces