diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 466bb7f3036..6755f07a791 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() } } + + 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") + } + + 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 } - c, err := s.find(ctx, &provider.Reference{ResourceId: &provider.ResourceId{ - StorageId: parts[0], // FIXME REFERENCE the StorageSpaceId is a storageid + a opaqueid + 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, "error finding path", err), + 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{}) - res, 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"), + 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 } - return res, 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 { + *e = errors.Wrap(err, "error connecting to storage provider="+p.Address) + return + } + + r, err := c.ListStorageSpaces(ctx, req) + if err != nil { + *e = errors.Wrap(err, "gateway: error calling ListStorageSpaces") + return + } + + *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 cc2f296c552..0fe2a3600f6 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 bc4fcf00581..7260767aa75 100644 --- a/pkg/storage/fs/owncloud/owncloud.go +++ b/pkg/storage/fs/owncloud/owncloud.go @@ -2217,6 +2217,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 030d079ce53..a9d5e784fec 100644 --- a/pkg/storage/fs/owncloudsql/owncloudsql.go +++ b/pkg/storage/fs/owncloudsql/owncloudsql.go @@ -2160,6 +2160,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 83c1fe1db8b..38c3b6b4f10 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 3074dd467e7..45704c87994 100644 --- a/pkg/storage/registry/static/static.go +++ b/pkg/storage/registry/static/static.go @@ -145,6 +145,7 @@ 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 { + if ref.ResourceId.StorageId != "" { for prefix, rule := range b.c.Rules { addr := getProviderAddr(ctx, rule) r, err := regexp.Compile("^" + prefix + "$") @@ -159,6 +160,12 @@ func (b *reg) FindProviders(ctx context.Context, ref *provider.Reference) ([]*re }}, 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()) + } + } } // Try to find by path as most storage operations will be done using the path. diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 8481c45a989..627a649a003 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 3d55a99a194..e524bf2d409 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 09d4465a722..bb8a9ff431d 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 5224d2fb66d..953b994c552 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/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index 6ac5882b536..ae435473d16 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -1352,6 +1352,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 d9c7e138703..5416f6fa390 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