diff --git a/pkg/storage/utils/decomposedfs/lookup/lookup.go b/pkg/storage/utils/decomposedfs/lookup/lookup.go new file mode 100644 index 00000000000..5be3fadf43b --- /dev/null +++ b/pkg/storage/utils/decomposedfs/lookup/lookup.go @@ -0,0 +1,198 @@ +// 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 lookup + +import ( + "context" + "fmt" + "path/filepath" + "strings" + + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/pkg/appctx" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/node" + "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/options" + "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/xattrs" + "github.com/cs3org/reva/pkg/storage/utils/templates" +) + +// Lookup implements transformations from filepath to node and back +type Lookup struct { + Options *options.Options +} + +// 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 { + // 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 + } + // is this a relative reference? + if ref.Path != "" { + p := filepath.Clean(ref.Path) + if p != "." && p != "/" { + // walk the relative path + n, err = lu.WalkPath(ctx, n, p, false, func(ctx context.Context, n *node.Node) error { return nil }) + if err != nil { + return nil, err + } + n.SpaceID = ref.ResourceId.StorageId + } + } + return n, nil + } + + // reference is invalid + return nil, fmt.Errorf("invalid reference %+v. resource_id must be set", ref) +} + +// NodeFromID returns the internal path for the id +func (lu *Lookup) NodeFromID(ctx context.Context, id *provider.ResourceId) (n *node.Node, err error) { + if id == nil { + return nil, fmt.Errorf("invalid resource id %+v", id) + } + if id.OpaqueId == "" { + // The Resource references the root of a space + return lu.NodeFromSpaceID(ctx, id) + } + n, err = node.ReadNode(ctx, lu, id.StorageId, id.OpaqueId) + if err != nil { + return nil, err + } + + return n, n.FindStorageSpaceRoot() +} + +// Pathify segments the beginning of a string into depth segments of width length +// Pathify("aabbccdd", 3, 1) will return "a/a/b/bccdd" +func Pathify(id string, depth, width int) string { + b := strings.Builder{} + i := 0 + for ; i < depth; i++ { + if len(id) <= i*width+width { + break + } + b.WriteString(id[i*width : i*width+width]) + b.WriteRune(filepath.Separator) + } + b.WriteString(id[i*width:]) + return b.String() +} + +// NodeFromSpaceID converts a resource id without an opaque id into a Node +func (lu *Lookup) NodeFromSpaceID(ctx context.Context, id *provider.ResourceId) (n *node.Node, err error) { + node, err := node.ReadNode(ctx, lu, id.StorageId, id.StorageId) + if err != nil { + return nil, err + } + + node.SpaceRoot = node + return node, nil +} + +// Path returns the path for node +func (lu *Lookup) Path(ctx context.Context, n *node.Node) (p string, err error) { + root := n.SpaceRoot + for n.ID != root.ID { + p = filepath.Join(n.Name, p) + if n, err = n.Parent(); err != nil { + appctx.GetLogger(ctx). + Error().Err(err). + Str("path", p). + Interface("node", n). + Msg("Path()") + return + } + } + p = filepath.Join("/", p) + return +} + +// RootNode returns the root node of the storage +func (lu *Lookup) RootNode(ctx context.Context) (*node.Node, error) { + n := node.New(node.NoSpaceID, node.RootID, "", "", 0, "", nil, lu) + n.Exists = true + return n, nil +} + +// WalkPath calls n.Child(segment) on every path segment in p starting at the node r. +// If a function f is given it will be executed for every segment node, but not the root node r. +// If followReferences is given the current visited reference node is replaced by the referenced node. +func (lu *Lookup) WalkPath(ctx context.Context, r *node.Node, p string, followReferences bool, f func(ctx context.Context, n *node.Node) error) (*node.Node, error) { + segments := strings.Split(strings.Trim(p, "/"), "/") + var err error + for i := range segments { + if r, err = r.Child(ctx, segments[i]); err != nil { + return r, err + } + + if followReferences { + if attrBytes, err := r.GetMetadata(xattrs.ReferenceAttr); err == nil { + realNodeID := attrBytes + ref, err := xattrs.ReferenceFromAttr([]byte(realNodeID)) + if err != nil { + return nil, err + } + + r, err = lu.NodeFromID(ctx, ref.ResourceId) + if err != nil { + return nil, err + } + } + } + if node.IsSpaceRoot(r) { + r.SpaceRoot = r + } + + if !r.Exists && i < len(segments)-1 { + return r, errtypes.NotFound(segments[i]) + } + if f != nil { + if err = f(ctx, r); err != nil { + return r, err + } + } + } + return r, nil +} + +// InternalRoot returns the internal storage root directory +func (lu *Lookup) InternalRoot() string { + return lu.Options.Root +} + +// InternalPath returns the internal path for a given ID +func (lu *Lookup) InternalPath(spaceID, nodeID string) string { + return filepath.Join(lu.Options.Root, "spaces", Pathify(spaceID, 1, 2), "nodes", Pathify(nodeID, 4, 2)) +} + +func (lu *Lookup) mustGetUserLayout(ctx context.Context) string { + u := ctxpkg.ContextMustGetUser(ctx) + return templates.WithUser(u, lu.Options.UserLayout) +} + +// ShareFolder returns the internal storage root directory +func (lu *Lookup) ShareFolder() string { + return lu.Options.ShareFolder +} diff --git a/pkg/storage/utils/decomposedfs/lookup/lookup_test.go b/pkg/storage/utils/decomposedfs/lookup/lookup_test.go new file mode 100644 index 00000000000..4faf6f58acb --- /dev/null +++ b/pkg/storage/utils/decomposedfs/lookup/lookup_test.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 lookup_test + +import ( + 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/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Lookup", func() { + var ( + env *helpers.TestEnv + ) + + JustBeforeEach(func() { + var err error + env, err = helpers.NewTestEnv() + Expect(err).ToNot(HaveOccurred()) + }) + + AfterEach(func() { + if env != nil { + env.Cleanup() + } + }) + + Describe("Node from path", func() { + It("returns the path including a leading slash", func() { + n, err := env.Lookup.NodeFromResource(env.Ctx, &provider.Reference{ + ResourceId: env.SpaceRootRes, + Path: "/dir1/file1", + }) + Expect(err).ToNot(HaveOccurred()) + + path, err := env.Lookup.Path(env.Ctx, n) + Expect(err).ToNot(HaveOccurred()) + Expect(path).To(Equal("/dir1/file1")) + }) + }) + + Describe("Node From Resource only by path", func() { + It("returns the path including a leading slash and the space root is set", func() { + n, err := env.Lookup.NodeFromResource(env.Ctx, &provider.Reference{ + ResourceId: env.SpaceRootRes, + Path: "/dir1/subdir1/file2", + }) + Expect(err).ToNot(HaveOccurred()) + + path, err := env.Lookup.Path(env.Ctx, n) + Expect(err).ToNot(HaveOccurred()) + Expect(path).To(Equal("/dir1/subdir1/file2")) + Expect(n.SpaceRoot.Name).To(Equal("userid")) + Expect(n.SpaceRoot.ParentID).To(Equal("root")) + }) + }) + + Describe("Node From Resource only by id", func() { + It("returns the path including a leading slash and the space root is set", func() { + // do a node lookup by path + nRef, err := env.Lookup.NodeFromResource(env.Ctx, &provider.Reference{ + ResourceId: env.SpaceRootRes, + Path: "/dir1/file1", + }) + Expect(err).ToNot(HaveOccurred()) + + // try to find the same node by id + n, err := env.Lookup.NodeFromResource(env.Ctx, &provider.Reference{ResourceId: &provider.ResourceId{OpaqueId: nRef.ID}}) + Expect(err).ToNot(HaveOccurred()) + Expect(n.SpaceRoot).ToNot(BeNil()) + + // Check if we got the right node and spaceRoot + path, err := env.Lookup.Path(env.Ctx, n) + Expect(err).ToNot(HaveOccurred()) + Expect(path).To(Equal("/dir1/file1")) + Expect(n.SpaceRoot.Name).To(Equal("userid")) + Expect(n.SpaceRoot.ParentID).To(Equal("root")) + }) + }) + + Describe("Node From Resource by id and relative path", func() { + It("returns the path including a leading slash and the space root is set", func() { + // do a node lookup by path for the parent + nRef, err := env.Lookup.NodeFromResource(env.Ctx, &provider.Reference{ + ResourceId: env.SpaceRootRes, + Path: "/dir1", + }) + Expect(err).ToNot(HaveOccurred()) + + // try to find the child node by parent id and relative path + n, err := env.Lookup.NodeFromResource(env.Ctx, &provider.Reference{ResourceId: &provider.ResourceId{OpaqueId: nRef.ID}, Path: "./file1"}) + Expect(err).ToNot(HaveOccurred()) + Expect(n.SpaceRoot).ToNot(BeNil()) + + // Check if we got the right node and spaceRoot + path, err := env.Lookup.Path(env.Ctx, n) + Expect(err).ToNot(HaveOccurred()) + Expect(path).To(Equal("/dir1/file1")) + Expect(n.SpaceRoot.Name).To(Equal("userid")) + Expect(n.SpaceRoot.ParentID).To(Equal("root")) + }) + }) + + Describe("Reference Parsing", func() { + It("parses a valid cs3 reference", func() { + in := []byte("cs3:bede11a0-ea3d-11eb-a78b-bf907adce8ed/c402d01c-ea3d-11eb-a0fc-c32f9d32528f") + ref, err := xattrs.ReferenceFromAttr(in) + + Expect(err).ToNot(HaveOccurred()) + Expect(ref.ResourceId.StorageId).To(Equal("bede11a0-ea3d-11eb-a78b-bf907adce8ed")) + Expect(ref.ResourceId.OpaqueId).To(Equal("c402d01c-ea3d-11eb-a0fc-c32f9d32528f")) + }) + }) +})