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 diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 466bb7f303..80fa004610 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -115,37 +115,130 @@ 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 { + + 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 storage 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 = 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 { + // 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 { + 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] + } + } + spaces := make([]*provider.StorageSpace, 0, len(uniqueSpaces)) + for spaceID := range uniqueSpaces { + spaces = append(spaces, uniqueSpaces[spaceID]) + } + if len(spaces) == 0 { return &provider.ListStorageSpacesResponse{ - Status: status.NewInvalidArg(ctx, "space id must be separated by !"), + Status: status.NewNotFound(ctx, "space not found"), }, 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], - }}) + + 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..9c8529971e 100644 --- a/internal/grpc/services/storageprovider/storageprovider.go +++ b/internal/grpc/services/storageprovider/storageprovider.go @@ -428,9 +428,48 @@ 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) { + log := appctx.GetLogger(ctx) + 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 + } + } 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") + } + } + 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..589338da11 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, errtypes.NotSupported("list storage spaces") +} + 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..eb31b6eb97 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 '%s' space %s, %s\n", spaceType, 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,158 @@ 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 + + var ( + 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 + } + + spaces := make([]*provider.StorageSpace, 0, len(matches)) + + 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 + + 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 + } + } 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.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") + } + } + + 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