diff --git a/pkg/storage/utils/decomposedfs/decomposedfs.go b/pkg/storage/utils/decomposedfs/decomposedfs.go index 7c7c0dce3ab..a3befde1b49 100644 --- a/pkg/storage/utils/decomposedfs/decomposedfs.go +++ b/pkg/storage/utils/decomposedfs/decomposedfs.go @@ -34,6 +34,7 @@ import ( "syscall" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" ctxpkg "github.com/cs3org/reva/pkg/ctx" @@ -204,57 +205,18 @@ func (fs *Decomposedfs) CreateHome(ctx context.Context) (err error) { return errtypes.NotSupported("Decomposedfs: CreateHome() home supported disabled") } - var n, h *node.Node - if n, err = fs.lu.RootNode(ctx); err != nil { - return - } - h, err = fs.lu.WalkPath(ctx, n, fs.lu.mustGetUserLayout(ctx), false, func(ctx context.Context, n *node.Node) error { - if !n.Exists { - if err := fs.tp.CreateDir(ctx, n); err != nil { - return err - } - } - return nil + u := ctxpkg.ContextMustGetUser(ctx) + res, err := fs.CreateStorageSpace(ctx, &provider.CreateStorageSpaceRequest{ + Type: spaceTypePersonal, + Owner: u, }) - - // make sure to delete the created directory if things go wrong - defer func() { - if err != nil { - // do not catch the error to not shadow the original error - if tmpErr := fs.tp.Delete(ctx, n); tmpErr != nil { - appctx.GetLogger(ctx).Error().Err(tmpErr).Msg("Can not revert file system change after error") - } - } - }() - if err != nil { - return - } - - // update the owner - u := ctxpkg.ContextMustGetUser(ctx) - if err = h.WriteAllNodeMetadata(u.Id); err != nil { - return - } - - if fs.o.TreeTimeAccounting || fs.o.TreeSizeAccounting { - // mark the home node as the end of propagation - if err = h.SetMetadata(xattrs.PropagationAttr, "1"); err != nil { - appctx.GetLogger(ctx).Error().Err(err).Interface("node", h).Msg("could not mark home as propagation root") - return - } - } - - if err := h.SetMetadata(xattrs.SpaceNameAttr, u.DisplayName); err != nil { return err } - - // add storage space - if err := fs.createStorageSpace(ctx, spaceTypePersonal, h.ID); err != nil { - return err + if res.Status.Code != rpcv1beta1.Code_CODE_OK { + return errtypes.NewErrtypeFromStatus(res.Status) } - - return + return nil } // The os not exists error is buried inside the xattr error, diff --git a/pkg/storage/utils/decomposedfs/grants.go b/pkg/storage/utils/decomposedfs/grants.go index 923a4478a16..c57f6f87ac3 100644 --- a/pkg/storage/utils/decomposedfs/grants.go +++ b/pkg/storage/utils/decomposedfs/grants.go @@ -122,7 +122,7 @@ func (fs *Decomposedfs) ListGrants(ctx context.Context, ref *provider.Reference) } log := appctx.GetLogger(ctx) - np := fs.lu.InternalPath(node.ID) + np := node.InternalPath() var attrs []string if attrs, err = xattr.List(np); err != nil { log.Error().Err(err).Msg("error listing attributes") @@ -174,8 +174,7 @@ func (fs *Decomposedfs) RemoveGrant(ctx context.Context, ref *provider.Reference attr = xattrs.GrantUserAcePrefix + g.Grantee.GetUserId().OpaqueId } - np := fs.lu.InternalPath(node.ID) - if err = xattr.Remove(np, attr); err != nil { + if err = xattr.Remove(node.InternalPath(), attr); err != nil { return } diff --git a/pkg/storage/utils/decomposedfs/lookup.go b/pkg/storage/utils/decomposedfs/lookup.go index 06017e96a25..bbe70c385c0 100644 --- a/pkg/storage/utils/decomposedfs/lookup.go +++ b/pkg/storage/utils/decomposedfs/lookup.go @@ -21,7 +21,6 @@ package decomposedfs import ( "context" "fmt" - "os" "path/filepath" "strings" @@ -58,6 +57,7 @@ func (lu *Lookup) NodeFromResource(ctx context.Context, ref *provider.Reference) if err != nil { return nil, err } + n.SpaceID = ref.ResourceId.StorageId } } return n, nil @@ -76,7 +76,7 @@ func (lu *Lookup) NodeFromID(ctx context.Context, id *provider.ResourceId) (n *n // The Resource references the root of a space return lu.NodeFromSpaceID(ctx, id) } - n, err = node.ReadNode(ctx, lu, id.OpaqueId) + n, err = node.ReadNode(ctx, lu, id.StorageId, id.OpaqueId) if err != nil { return nil, err } @@ -84,24 +84,23 @@ func (lu *Lookup) NodeFromID(ctx context.Context, id *provider.ResourceId) (n *n return n, n.FindStorageSpaceRoot() } -// 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) { - d := filepath.Join(lu.Options.Root, "spaces", spaceTypeAny, id.StorageId) - matches, err := filepath.Glob(d) - if err != nil { - return nil, err - } - - if len(matches) != 1 { - return nil, fmt.Errorf("can't determine node from spaceID: found %d matching spaces. Path: %s", len(matches), d) - } - - target, err := os.Readlink(matches[0]) - if err != nil { - appctx.GetLogger(ctx).Error().Err(err).Str("match", matches[0]).Msg("could not read link, skipping") +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() +} - node, err := node.ReadNode(ctx, lu, filepath.Base(target)) +// 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 } @@ -130,7 +129,7 @@ func (lu *Lookup) Path(ctx context.Context, n *node.Node) (p string, err error) // RootNode returns the root node of the storage func (lu *Lookup) RootNode(ctx context.Context) (*node.Node, error) { - n := node.New(node.RootID, "", "", 0, "", nil, lu) + n := node.New(node.NoSpaceID, node.RootID, "", "", 0, "", nil, lu) n.Exists = true return n, nil } @@ -182,8 +181,8 @@ func (lu *Lookup) InternalRoot() string { } // InternalPath returns the internal path for a given ID -func (lu *Lookup) InternalPath(id string) string { - return filepath.Join(lu.Options.Root, "nodes", 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 { diff --git a/pkg/storage/utils/decomposedfs/node/node.go b/pkg/storage/utils/decomposedfs/node/node.go index cc4e5bcad9e..031075a5479 100644 --- a/pkg/storage/utils/decomposedfs/node/node.go +++ b/pkg/storage/utils/decomposedfs/node/node.go @@ -64,6 +64,7 @@ const ( // TrashIDDelimiter represents the characters used to separate the nodeid and the deletion time. TrashIDDelimiter = ".T." + NoSpaceID = "" // RootID defines the root node's ID RootID = "root" @@ -71,6 +72,7 @@ const ( // Node represents a node in the tree and provides methods to get a Parent or Child instance type Node struct { + SpaceID string ParentID string ID string Name string @@ -88,17 +90,18 @@ type PathLookup interface { RootNode(ctx context.Context) (node *Node, err error) InternalRoot() string - InternalPath(ID string) string + InternalPath(spaceID, nodeID string) string Path(ctx context.Context, n *Node) (path string, err error) ShareFolder() string } // New returns a new instance of Node -func New(id, parentID, name string, blobsize int64, blobID string, owner *userpb.UserId, lu PathLookup) *Node { +func New(spaceID, id, parentID, name string, blobsize int64, blobID string, owner *userpb.UserId, lu PathLookup) *Node { if blobID == "" { blobID = uuid.New().String() } return &Node{ + SpaceID: spaceID, ID: id, ParentID: parentID, Name: name, @@ -135,6 +138,18 @@ func (n *Node) SetMetadata(key string, val string) (err error) { return nil } +// RemoveMetadata removes a given key +func (n *Node) RemoveMetadata(key string) (err error) { + if err = xattr.Remove(n.InternalPath(), key); err != nil { + if e, ok := err.(*xattr.Error); ok && (e.Err.Error() == "no data available" || + // darwin + e.Err.Error() == "attribute not found") { + return nil + } + } + return err +} + // GetMetadata reads the metadata for the given key func (n *Node) GetMetadata(key string) (val string, err error) { nodePath := n.InternalPath() @@ -170,10 +185,11 @@ func (n *Node) WriteAllNodeMetadata(owner *userpb.UserId) (err error) { } // ReadNode creates a new instance from an id and checks if it exists -func ReadNode(ctx context.Context, lu PathLookup, id string) (n *Node, err error) { +func ReadNode(ctx context.Context, lu PathLookup, spaceID, nodeID string) (n *Node, err error) { n = &Node{ - lu: lu, - ID: id, + SpaceID: spaceID, + lu: lu, + ID: nodeID, } nodePath := n.InternalPath() @@ -216,8 +232,7 @@ func ReadNode(ctx context.Context, lu PathLookup, id string) (n *Node, err error return } - // Check if parent exists. Otherwise this node is part of a deleted subtree - _, err = os.Stat(lu.InternalPath(n.ParentID)) + _, err = os.Stat(n.ParentInternalPath()) if err != nil { if os.IsNotExist(err) { return nil, errtypes.NotFound(err.Error()) @@ -238,12 +253,30 @@ func isNotDir(err error) bool { return false } +func readChildNodeFromLink(path string) (string, error) { + link, err := os.Readlink(path) + if err != nil { + return "", err + } + nodeID := strings.TrimLeft(link, "/.") + nodeID = strings.ReplaceAll(nodeID, "/", "") + return nodeID, nil +} + // Child returns the child node with the given name func (n *Node) Child(ctx context.Context, name string) (*Node, error) { - link, err := os.Readlink(filepath.Join(n.InternalPath(), filepath.Join("/", name))) + spaceID := n.SpaceID + if spaceID == "" && n.ParentID == "root" { + spaceID = n.ID + } else if n.SpaceRoot != nil { + spaceID = n.SpaceRoot.ID + } + nodeID, err := readChildNodeFromLink(filepath.Join(n.InternalPath(), name)) if err != nil { if os.IsNotExist(err) || isNotDir(err) { + c := &Node{ + SpaceID: spaceID, lu: n.lu, ParentID: n.ID, Name: name, @@ -256,15 +289,11 @@ func (n *Node) Child(ctx context.Context, name string) (*Node, error) { } var c *Node - if strings.HasPrefix(link, "../") { - c, err = ReadNode(ctx, n.lu, filepath.Base(link)) - if err != nil { - return nil, errors.Wrap(err, "could not read child node") - } - c.SpaceRoot = n.SpaceRoot - } else { - return nil, fmt.Errorf("decomposedfs: expected '../ prefix, got' %+v", link) + c, err = ReadNode(ctx, n.lu, spaceID, nodeID) + if err != nil { + return nil, errors.Wrap(err, "could not read child node") } + c.SpaceRoot = n.SpaceRoot return c, nil } @@ -275,12 +304,14 @@ func (n *Node) Parent() (p *Node, err error) { return nil, fmt.Errorf("decomposedfs: root has no parent") } p = &Node{ + SpaceID: n.SpaceID, lu: n.lu, ID: n.ParentID, SpaceRoot: n.SpaceRoot, } - parentPath := n.lu.InternalPath(n.ParentID) + // parentPath := n.lu.InternalPath(spaceID, n.ParentID) + parentPath := p.InternalPath() // lookup parent id in extended attributes if p.ParentID, err = xattrs.Get(parentPath, xattrs.ParentidAttr); err != nil { @@ -376,12 +407,16 @@ func (n *Node) PermissionSet(ctx context.Context) provider.ResourcePermissions { // InternalPath returns the internal path of the Node func (n *Node) InternalPath() string { - return n.lu.InternalPath(n.ID) + return n.lu.InternalPath(n.SpaceID, n.ID) +} + +func (n *Node) ParentInternalPath() string { + return n.lu.InternalPath(n.SpaceID, n.ParentID) } // LockFilePath returns the internal path of the lock file of the node func (n *Node) LockFilePath() string { - return n.lu.InternalPath(n.ID) + ".lock" + return n.InternalPath() + ".lock" } // CalculateEtag returns a hash of fileid + tmtime (or mtime) @@ -409,7 +444,7 @@ func calculateEtag(nodeID string, tmTime time.Time) (string, error) { func (n *Node) SetMtime(ctx context.Context, mtime string) error { sublog := appctx.GetLogger(ctx).With().Interface("node", n).Logger() if mt, err := parseMTime(mtime); err == nil { - nodePath := n.lu.InternalPath(n.ID) + nodePath := n.InternalPath() // updating mtime also updates atime if err := os.Chtimes(nodePath, mt, mt); err != nil { sublog.Error().Err(err). @@ -429,7 +464,7 @@ func (n *Node) SetMtime(ctx context.Context, mtime string) error { // SetEtag sets the temporary etag of a node if it differs from the current etag func (n *Node) SetEtag(ctx context.Context, val string) (err error) { sublog := appctx.GetLogger(ctx).With().Interface("node", n).Logger() - nodePath := n.lu.InternalPath(n.ID) + nodePath := n.InternalPath() var tmTime time.Time if tmTime, err = n.GetTMTime(); err != nil { // no tmtime, use mtime @@ -474,7 +509,7 @@ func (n *Node) SetEtag(ctx context.Context, val string) (err error) { // obviously this only is secure when the u/s/g/a namespaces are not accessible by users in the filesystem // public tags can be mapped to extended attributes func (n *Node) SetFavorite(uid *userpb.UserId, val string) error { - nodePath := n.lu.InternalPath(n.ID) + nodePath := n.InternalPath() // the favorite flag is specific to the user, so we need to incorporate the userid fa := fmt.Sprintf("%s:%s:%s@%s", xattrs.FavPrefix, utils.UserTypeToString(uid.GetType()), uid.GetOpaqueId(), uid.GetIdp()) return xattrs.Set(nodePath, fa, val) @@ -485,7 +520,7 @@ func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissi sublog := appctx.GetLogger(ctx).With().Interface("node", n.ID).Logger() var fn string - nodePath := n.lu.InternalPath(n.ID) + nodePath := n.InternalPath() var fi os.FileInfo @@ -749,7 +784,7 @@ func readQuotaIntoOpaque(ctx context.Context, nodePath string, ri *provider.Reso // HasPropagation checks if the propagation attribute exists and is set to "1" func (n *Node) HasPropagation() (propagation bool) { - if b, err := xattrs.Get(n.lu.InternalPath(n.ID), xattrs.PropagationAttr); err == nil { + if b, err := xattrs.Get(n.InternalPath(), xattrs.PropagationAttr); err == nil { return b == "1" } return false @@ -758,7 +793,7 @@ func (n *Node) HasPropagation() (propagation bool) { // GetTMTime reads the tmtime from the extended attributes func (n *Node) GetTMTime() (tmTime time.Time, err error) { var b string - if b, err = xattrs.Get(n.lu.InternalPath(n.ID), xattrs.TreeMTimeAttr); err != nil { + if b, err = xattrs.Get(n.InternalPath(), xattrs.TreeMTimeAttr); err != nil { return } return time.Parse(time.RFC3339Nano, b) @@ -766,7 +801,7 @@ func (n *Node) GetTMTime() (tmTime time.Time, err error) { // SetTMTime writes the tmtime to the extended attributes func (n *Node) SetTMTime(t time.Time) (err error) { - return xattrs.Set(n.lu.InternalPath(n.ID), xattrs.TreeMTimeAttr, t.UTC().Format(time.RFC3339Nano)) + return xattrs.Set(n.InternalPath(), xattrs.TreeMTimeAttr, t.UTC().Format(time.RFC3339Nano)) } // GetTreeSize reads the treesize from the extended attributes @@ -790,7 +825,7 @@ func (n *Node) SetChecksum(csType string, h hash.Hash) (err error) { // UnsetTempEtag removes the temporary etag attribute func (n *Node) UnsetTempEtag() (err error) { - if err = xattr.Remove(n.lu.InternalPath(n.ID), xattrs.TmpEtagAttr); err != nil { + if err = xattr.Remove(n.InternalPath(), xattrs.TmpEtagAttr); err != nil { if e, ok := err.(*xattr.Error); ok && (e.Err.Error() == "no data available" || // darwin e.Err.Error() == "attribute not found") { diff --git a/pkg/storage/utils/decomposedfs/node/node_test.go b/pkg/storage/utils/decomposedfs/node/node_test.go index d1db6202470..9c422d11a4a 100644 --- a/pkg/storage/utils/decomposedfs/node/node_test.go +++ b/pkg/storage/utils/decomposedfs/node/node_test.go @@ -56,8 +56,8 @@ var _ = Describe("Node", func() { Describe("New", func() { It("generates unique blob ids if none are given", func() { - n1 := node.New(id, "", name, 10, "", env.Owner.Id, env.Lookup) - n2 := node.New(id, "", name, 10, "", env.Owner.Id, env.Lookup) + n1 := node.New(env.SpaceRootRes.StorageId, id, "", name, 10, "", env.Owner.Id, env.Lookup) + n2 := node.New(env.SpaceRootRes.StorageId, id, "", name, 10, "", env.Owner.Id, env.Lookup) Expect(len(n1.BlobID)).To(Equal(36)) Expect(n1.BlobID).ToNot(Equal(n2.BlobID)) @@ -72,7 +72,7 @@ var _ = Describe("Node", func() { }) Expect(err).ToNot(HaveOccurred()) - n, err := node.ReadNode(env.Ctx, env.Lookup, lookupNode.ID) + n, err := node.ReadNode(env.Ctx, env.Lookup, lookupNode.SpaceID, lookupNode.ID) Expect(err).ToNot(HaveOccurred()) Expect(n.BlobID).To(Equal("file1-blobid")) }) diff --git a/pkg/storage/utils/decomposedfs/recycle.go b/pkg/storage/utils/decomposedfs/recycle.go index 4670e8281b2..add667dd3cd 100644 --- a/pkg/storage/utils/decomposedfs/recycle.go +++ b/pkg/storage/utils/decomposedfs/recycle.go @@ -95,7 +95,7 @@ func (fs *Decomposedfs) ListRecycle(ctx context.Context, ref *provider.Reference return nil, err } else if !md.IsDir() { // this is the case when we want to directly list a file in the trashbin - item, err := fs.createTrashItem(ctx, parentNode, filepath.Dir(relativePath), filepath.Join(trashRoot, key, relativePath)) + item, err := fs.createTrashItem(ctx, spaceID, parentNode, filepath.Dir(relativePath), filepath.Join(trashRoot, key, relativePath)) if err != nil { return items, err } @@ -108,14 +108,14 @@ func (fs *Decomposedfs) ListRecycle(ctx context.Context, ref *provider.Reference return nil, err } for i := range names { - if item, err := fs.createTrashItem(ctx, parentNode, relativePath, filepath.Join(trashRoot, key, relativePath, names[i])); err == nil { + if item, err := fs.createTrashItem(ctx, spaceID, parentNode, relativePath, filepath.Join(trashRoot, key, relativePath, names[i])); err == nil { items = append(items, item) } } return items, nil } -func (fs *Decomposedfs) createTrashItem(ctx context.Context, parentNode, intermediatePath, itemPath string) (*provider.RecycleItem, error) { +func (fs *Decomposedfs) createTrashItem(ctx context.Context, spaceID, parentNode, intermediatePath, itemPath string) (*provider.RecycleItem, error) { log := appctx.GetLogger(ctx) trashnode, err := os.Readlink(itemPath) if err != nil { @@ -128,7 +128,7 @@ func (fs *Decomposedfs) createTrashItem(ctx context.Context, parentNode, interme return nil, errors.New("malformed trash link") } - nodePath := fs.lu.InternalPath(filepath.Base(trashnode)) + nodePath := fs.lu.InternalPath(spaceID, filepath.Base(trashnode)) md, err := os.Stat(nodePath) if err != nil { log.Error().Err(err).Str("trashnode", trashnode).Msg("could not stat trash item, skipping") @@ -150,7 +150,7 @@ func (fs *Decomposedfs) createTrashItem(ctx context.Context, parentNode, interme } // lookup origin path in extended attributes - parentPath := fs.lu.InternalPath(filepath.Base(parentNode)) + parentPath := fs.lu.InternalPath(spaceID, filepath.Base(parentNode)) if attrBytes, err := xattr.Get(parentPath, xattrs.TrashOriginAttr); err == nil { item.Ref = &provider.Reference{Path: filepath.Join(string(attrBytes), intermediatePath, filepath.Base(itemPath))} } else { @@ -166,56 +166,58 @@ func (fs *Decomposedfs) createTrashItem(ctx context.Context, parentNode, interme return item, nil } +// readTrashLink returns nodeID and timestamp +func readTrashLink(path string) (string, string, error) { + link, err := os.Readlink(path) + if err != nil { + return "", "", err + } + // ../../../../../nodes/e5/6c/75/a8/-d235-4cbb-8b4e-48b6fd0f2094.T.2022-02-16T14:38:11.769917408Z + // TODO use filepath.Separator to support windows + link = strings.ReplaceAll(link, "/", "") + // ..........nodese56c75a8-d235-4cbb-8b4e-48b6fd0f2094.T.2022-02-16T14:38:11.769917408Z + if link[0:15] != "..........nodes" || link[51:54] != ".T." { + return "", "", errtypes.InternalError("malformed trash link") + } + return link[15:51], link[54:], nil +} + func (fs *Decomposedfs) listTrashRoot(ctx context.Context, spaceID string) ([]*provider.RecycleItem, error) { log := appctx.GetLogger(ctx) items := make([]*provider.RecycleItem, 0) trashRoot := fs.getRecycleRoot(ctx, spaceID) - f, err := os.Open(trashRoot) - if err != nil { - if os.IsNotExist(err) { - return items, nil - } - return nil, errors.Wrap(err, "tree: error listing "+trashRoot) - } - defer f.Close() - - names, err := f.Readdirnames(0) + matches, err := filepath.Glob(trashRoot + "/*/*/*/*/*") if err != nil { return nil, err } - for i := range names { - trashnode, err := os.Readlink(filepath.Join(trashRoot, names[i])) + for _, itemPath := range matches { + nodeID, timeSuffix, err := readTrashLink(itemPath) if err != nil { - log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Msg("error reading trash link, skipping") - continue - } - parts := strings.SplitN(filepath.Base(trashnode), node.TrashIDDelimiter, 2) - if len(parts) != 2 { - log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("trashnode", trashnode).Interface("parts", parts).Msg("malformed trash link, skipping") + log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Msg("error reading trash link, skipping") continue } - nodePath := fs.lu.InternalPath(filepath.Base(trashnode)) + nodePath := fs.lu.InternalPath(spaceID, nodeID) + node.TrashIDDelimiter + timeSuffix md, err := os.Stat(nodePath) if err != nil { - log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("trashnode", trashnode). /*.Interface("parts", parts)*/ Msg("could not stat trash item, skipping") + log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node_path", nodePath).Msg("could not stat trash item, skipping") continue } item := &provider.RecycleItem{ Type: getResourceType(md.IsDir()), Size: uint64(md.Size()), - Key: parts[0], + Key: nodeID, } - if deletionTime, err := time.Parse(time.RFC3339Nano, parts[1]); err == nil { + if deletionTime, err := time.Parse(time.RFC3339Nano, timeSuffix); err == nil { item.DeletionTime = &types.Timestamp{ Seconds: uint64(deletionTime.Unix()), // TODO nanos } } else { - log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", trashnode).Interface("parts", parts).Msg("could parse time format, ignoring") + log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node", nodeID).Str("dtime", timeSuffix).Msg("could not parse time format, ignoring") } // lookup origin path in extended attributes @@ -223,7 +225,7 @@ func (fs *Decomposedfs) listTrashRoot(ctx context.Context, spaceID string) ([]*p if attrBytes, err = xattr.Get(nodePath, xattrs.TrashOriginAttr); err == nil { item.Ref = &provider.Reference{Path: string(attrBytes)} } else { - log.Error().Err(err).Str("trashRoot", trashRoot).Str("name", names[i]).Str("link", trashnode).Msg("could not read origin path, skipping") + log.Error().Err(err).Str("trashRoot", trashRoot).Str("item", itemPath).Str("node", nodeID).Str("dtime", timeSuffix).Msg("could not read origin path, skipping") continue } // TODO filter results by permission ... on the original parent? or the trashed node? @@ -326,5 +328,5 @@ func getResourceType(isDir bool) provider.ResourceType { } func (fs *Decomposedfs) getRecycleRoot(ctx context.Context, spaceID string) string { - return filepath.Join(fs.o.Root, "trash", spaceID) + return filepath.Join(fs.o.Root, "spaces", Pathify(spaceID, 1, 2), "trash") } diff --git a/pkg/storage/utils/decomposedfs/recycle_test.go b/pkg/storage/utils/decomposedfs/recycle_test.go index eb71af14a2e..dc03d7a1394 100644 --- a/pkg/storage/utils/decomposedfs/recycle_test.go +++ b/pkg/storage/utils/decomposedfs/recycle_test.go @@ -289,7 +289,7 @@ var _ = Describe("Recycle", func() { Expect(len(items)).To(Equal(1)) // use up 2000 byte quota - _, err = env.CreateTestFile("largefile", "largefile-blobid", 2000, projectID.OpaqueId) + _, err = env.CreateTestFile("largefile", "largefile-blobid", projectID.OpaqueId, projectID.StorageId, 2000) Expect(err).ToNot(HaveOccurred()) err = env.Fs.RestoreRecycleItem(env.Ctx, &provider.Reference{ResourceId: projectID}, items[0].Key, "/", nil) diff --git a/pkg/storage/utils/decomposedfs/revisions.go b/pkg/storage/utils/decomposedfs/revisions.go index 8e50b8fcc23..f72efd0f132 100644 --- a/pkg/storage/utils/decomposedfs/revisions.go +++ b/pkg/storage/utils/decomposedfs/revisions.go @@ -101,8 +101,9 @@ func (fs *Decomposedfs) DownloadRevision(ctx context.Context, ref *provider.Refe } log.Debug().Str("revisionKey", revisionKey).Msg("DownloadRevision") + spaceID := ref.ResourceId.OpaqueId // check if the node is available and has not been deleted - n, err := node.ReadNode(ctx, fs.lu, kp[0]) + n, err := node.ReadNode(ctx, fs.lu, spaceID, kp[0]) if err != nil { return nil, err } @@ -122,7 +123,7 @@ func (fs *Decomposedfs) DownloadRevision(ctx context.Context, ref *provider.Refe return nil, errtypes.PermissionDenied(filepath.Join(n.ParentID, n.Name)) } - contentPath := fs.lu.InternalPath(revisionKey) + contentPath := fs.lu.InternalPath(spaceID, revisionKey) r, err := os.Open(contentPath) if err != nil { @@ -145,8 +146,9 @@ func (fs *Decomposedfs) RestoreRevision(ctx context.Context, ref *provider.Refer return errtypes.NotFound(revisionKey) } + spaceID := ref.ResourceId.OpaqueId // check if the node is available and has not been deleted - n, err := node.ReadNode(ctx, fs.lu, kp[0]) + n, err := node.ReadNode(ctx, fs.lu, spaceID, kp[0]) if err != nil { return err } @@ -171,11 +173,11 @@ func (fs *Decomposedfs) RestoreRevision(ctx context.Context, ref *provider.Refer } // move current version to new revision - nodePath := fs.lu.InternalPath(kp[0]) + nodePath := fs.lu.InternalPath(spaceID, kp[0]) var fi os.FileInfo if fi, err = os.Stat(nodePath); err == nil { // versions are stored alongside the actual file, so a rename can be efficient and does not cross storage / partition boundaries - versionsPath := fs.lu.InternalPath(kp[0] + ".REV." + fi.ModTime().UTC().Format(time.RFC3339Nano)) + versionsPath := fs.lu.InternalPath(spaceID, kp[0]+".REV."+fi.ModTime().UTC().Format(time.RFC3339Nano)) err = os.Rename(nodePath, versionsPath) if err != nil { @@ -184,7 +186,7 @@ func (fs *Decomposedfs) RestoreRevision(ctx context.Context, ref *provider.Refer // copy old revision to current location - revisionPath := fs.lu.InternalPath(revisionKey) + revisionPath := fs.lu.InternalPath(spaceID, revisionKey) if err = os.Rename(revisionPath, nodePath); err != nil { return diff --git a/pkg/storage/utils/decomposedfs/spaces.go b/pkg/storage/utils/decomposedfs/spaces.go index 4fa9aab1270..1a411b8d9f8 100644 --- a/pkg/storage/utils/decomposedfs/spaces.go +++ b/pkg/storage/utils/decomposedfs/spaces.go @@ -45,6 +45,7 @@ import ( "github.com/cs3org/reva/pkg/utils" "github.com/cs3org/reva/pkg/utils/resourceid" "github.com/google/uuid" + "github.com/pkg/errors" ) const ( @@ -57,11 +58,6 @@ const ( // CreateStorageSpace creates a storage space func (fs *Decomposedfs) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { - // spaces will be located by default in the root of the storage. - r, err := fs.lu.RootNode(ctx) - if err != nil { - return nil, err - } // "everything is a resource" this is the unique ID for the Space resource. spaceID := uuid.New().String() @@ -81,30 +77,29 @@ func (fs *Decomposedfs) CreateStorageSpace(ctx context.Context, req *provider.Cr // TODO enforce a uuid? // TODO clarify if we want to enforce a single personal storage space or if we want to allow sending the spaceid if req.Type == spaceTypePersonal { - spaceID = req.Owner.Id.OpaqueId - } - - n, err := r.Child(ctx, spaceID) - if err != nil { - return nil, err + spaceID = req.GetOwner().GetId().GetOpaqueId() } - if n.Exists { + root, err := node.ReadNode(ctx, fs.lu, spaceID, spaceID) + if err == nil && root.Exists { return nil, errtypes.AlreadyExists("decomposedfs: spaces: space already exists") } - // spaceid and nodeid must be the same - // TODO enforce a uuid? - n.ID = spaceID + // create a directory node + rootPath := root.InternalPath() + if err = os.MkdirAll(rootPath, 0700); err != nil { + return nil, errors.Wrap(err, "decomposedfs: error creating node") + } - if err := fs.tp.CreateDir(ctx, n); err != nil { + root.WriteAllNodeMetadata(req.GetOwner().GetId()) + if err := root.WriteAllNodeMetadata(req.GetOwner().GetId()); err != nil { return nil, err } // always enable propagation on the storage space root // mark the space root node as the end of propagation - if err = n.SetMetadata(xattrs.PropagationAttr, "1"); err != nil { - appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not mark node to propagate") + if err = root.SetMetadata(xattrs.PropagationAttr, "1"); err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("node", root).Msg("could not mark space root node to propagate") return nil, err } @@ -118,11 +113,11 @@ func (fs *Decomposedfs) CreateStorageSpace(ctx context.Context, req *provider.Cr ownerID = &userv1beta1.UserId{} } - if err := n.ChangeOwner(ownerID); err != nil { + if err := root.ChangeOwner(ownerID); err != nil { return nil, err } - err = fs.createStorageSpace(ctx, req.Type, n.ID) + err = fs.createStorageSpace(ctx, req.Type, root.ID) if err != nil { return nil, err } @@ -137,7 +132,7 @@ func (fs *Decomposedfs) CreateStorageSpace(ctx context.Context, req *provider.Cr if description != "" { metadata[xattrs.SpaceDescriptionAttr] = description } - if err := xattrs.SetMultiple(n.InternalPath(), metadata); err != nil { + if err := xattrs.SetMultiple(root.InternalPath(), metadata); err != nil { return nil, err } @@ -160,7 +155,7 @@ func (fs *Decomposedfs) CreateStorageSpace(ctx context.Context, req *provider.Cr return nil, err } - space, err := fs.storageSpaceFromNode(ctx, n, "*", n.InternalPath(), false) + space, err := fs.storageSpaceFromNode(ctx, root, "*", root.InternalPath(), false) if err != nil { return nil, err } @@ -174,6 +169,43 @@ func (fs *Decomposedfs) CreateStorageSpace(ctx context.Context, req *provider.Cr return resp, nil } +func (fs *Decomposedfs) canListAllSpaces(ctx context.Context) bool { + client, err := pool.GetGatewayServiceClient(fs.o.GatewayAddr) + if err != nil { + return false + } + + user := ctxpkg.ContextMustGetUser(ctx) + checkRes, err := client.CheckPermission(ctx, &permissionsv1beta1.CheckPermissionRequest{ + Permission: "list-all-spaces", + SubjectRef: &permissionsv1beta1.SubjectReference{ + Spec: &permissionsv1beta1.SubjectReference_UserId{ + UserId: user.Id, + }, + }, + }) + if err != nil { + return false + } + + return checkRes.Status.Code == v1beta11.Code_CODE_OK +} + +func readSpaceAndNodeFromSpaceTypeLink(path string) (string, string, error) { + link, err := os.Readlink(path) + if err != nil { + return "", "", err + } + // ../../spaces/4c/510ada-c86b-4815-8820-42cdf82c3d51/nodes/4c/51/0a/da/-c86b-4815-8820-42cdf82c3d51 + // TODO use filepath.Separator to support windows + link = strings.ReplaceAll(link, "/", "") + // ....spaces4c510ada-c86b-4815-8820-42cdf82c3d51nodes4c510ada-c86b-4815-8820-42cdf82c3d51 + if link[0:10] != "....spaces" || link[46:51] != "nodes" { + return "", "", errtypes.InternalError("malformed link") + } + return link[10:46], link[51:], 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 @@ -218,15 +250,38 @@ func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provide spaceTypes = []string{"*"} } + canListAllSpaces := fs.canListAllSpaces(ctx) + spaces := []*provider.StorageSpace{} // build the glob path, eg. // /path/to/root/spaces/{spaceType}/{spaceId} // /path/to/root/spaces/personal/nodeid // /path/to/root/spaces/shared/nodeid + if spaceID != spaceIDAny && nodeID != spaceIDAny { + // try directly reading the node + n, err := node.ReadNode(ctx, fs.lu, spaceID, nodeID) + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Str("id", nodeID).Msg("could not read node") + return nil, err + } + space, err := fs.storageSpaceFromNode(ctx, n, spaceTypeAny, n.InternalPath(), canListAllSpaces) + if err != nil { + return nil, err + } + // filter space types + for _, spaceType := range spaceTypes { + if spaceType == "*" || spaceType == space.SpaceType { + spaces = append(spaces, space) + } + } + + return spaces, nil + } + matches := []string{} for _, spaceType := range spaceTypes { - path := filepath.Join(fs.o.Root, "spaces", spaceType, nodeID) + path := filepath.Join(fs.o.Root, "spacetypes", spaceType, nodeID) m, err := filepath.Glob(path) if err != nil { return nil, err @@ -246,41 +301,19 @@ func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provide // the personal spaces must also use the nodeid and not the name numShares := 0 - client, err := pool.GetGatewayServiceClient(fs.o.GatewayAddr) - if err != nil { - return nil, err - } - - user := ctxpkg.ContextMustGetUser(ctx) - checkRes, err := client.CheckPermission(ctx, &permissionsv1beta1.CheckPermissionRequest{ - Permission: "list-all-spaces", - SubjectRef: &permissionsv1beta1.SubjectReference{ - Spec: &permissionsv1beta1.SubjectReference_UserId{ - UserId: user.Id, - }, - }, - }) - if err != nil { - return nil, err - } - - canListAllSpaces := false - if checkRes.Status.Code == v1beta11.Code_CODE_OK { - canListAllSpaces = true - } for i := range matches { - var target string var err error // always read link in case storage space id != node id - if target, err = os.Readlink(matches[i]); err != nil { + spaceID, nodeID, err = readSpaceAndNodeFromSpaceTypeLink(matches[i]) + if err != nil { appctx.GetLogger(ctx).Error().Err(err).Str("match", matches[i]).Msg("could not read link, skipping") continue } - n, err := node.ReadNode(ctx, fs.lu, filepath.Base(target)) + n, err := node.ReadNode(ctx, fs.lu, spaceID, nodeID) if err != nil { - appctx.GetLogger(ctx).Error().Err(err).Str("id", filepath.Base(target)).Msg("could not read node, skipping") + appctx.GetLogger(ctx).Error().Err(err).Str("id", nodeID).Msg("could not read node, skipping") continue } @@ -307,8 +340,7 @@ func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provide // if there are no matches (or they happened to be spaces for the owner) and the node is a child return a space if len(matches) <= numShares && nodeID != spaceID { // try node id - target := filepath.Join(fs.o.Root, "nodes", nodeID) - n, err := node.ReadNode(ctx, fs.lu, filepath.Base(target)) + n, err := node.ReadNode(ctx, fs.lu, spaceID, nodeID) if err != nil { return nil, err } @@ -333,10 +365,10 @@ func (fs *Decomposedfs) UpdateStorageSpace(ctx context.Context, req *provider.Up } space := req.StorageSpace - _, spaceID, _ := utils.SplitStorageSpaceID(space.Id.OpaqueId) + spaceID, nodeID, _ := utils.SplitStorageSpaceID(space.Id.OpaqueId) if restore { - matches, err := filepath.Glob(filepath.Join(fs.o.Root, "spaces", spaceTypeAny, spaceID)) + matches, err := filepath.Glob(filepath.Join(fs.o.Root, "spacetypes", spaceTypeAny, nodeID)) if err != nil { return nil, err } @@ -355,8 +387,9 @@ func (fs *Decomposedfs) UpdateStorageSpace(ctx context.Context, req *provider.Up if err != nil { appctx.GetLogger(ctx).Error().Err(err).Str("match", matches[0]).Msg("could not read link, skipping") } - - n, err := node.ReadNode(ctx, fs.lu, filepath.Base(target)) + nodeID := strings.TrimLeft(target, "/.") + nodeID = strings.ReplaceAll(nodeID, "/", "") + n, err := node.ReadNode(ctx, fs.lu, spaceID, nodeID) if err != nil { return nil, err } @@ -371,26 +404,7 @@ func (fs *Decomposedfs) UpdateStorageSpace(ctx context.Context, req *provider.Up } } - matches, err := filepath.Glob(filepath.Join(fs.o.Root, "spaces", spaceTypeAny, spaceID)) - if err != nil { - return nil, err - } - - if len(matches) != 1 { - return &provider.UpdateStorageSpaceResponse{ - Status: &v1beta11.Status{ - Code: v1beta11.Code_CODE_NOT_FOUND, - Message: fmt.Sprintf("update space failed: found %d matching spaces", len(matches)), - }, - }, nil - } - - target, err := os.Readlink(matches[0]) - if err != nil { - appctx.GetLogger(ctx).Error().Err(err).Str("match", matches[0]).Msg("could not read link, skipping") - } - - node, err := node.ReadNode(ctx, fs.lu, filepath.Base(target)) + node, err := node.ReadNode(ctx, fs.lu, spaceID, spaceID) if err != nil { return nil, err } @@ -482,21 +496,7 @@ func (fs *Decomposedfs) DeleteStorageSpace(ctx context.Context, req *provider.De spaceID := req.Id.OpaqueId - matches, err := filepath.Glob(filepath.Join(fs.o.Root, "spaces", spaceTypeAny, spaceID)) - if err != nil { - return err - } - - if len(matches) != 1 { - return fmt.Errorf("delete space failed: found %d matching spaces", len(matches)) - } - - target, err := os.Readlink(matches[0]) - if err != nil { - appctx.GetLogger(ctx).Error().Err(err).Str("match", matches[0]).Msg("could not read link, skipping") - } - - n, err := node.ReadNode(ctx, fs.lu, filepath.Base(target)) + n, err := node.ReadNode(ctx, fs.lu, spaceID, spaceID) if err != nil { return err } @@ -505,7 +505,7 @@ func (fs *Decomposedfs) DeleteStorageSpace(ctx context.Context, req *provider.De if !strings.Contains(n.Name, node.TrashIDDelimiter) { return errtypes.NewErrtypeFromStatus(status.NewInvalidArg(ctx, "can't purge enabled space")) } - ip := fs.lu.InternalPath(req.Id.OpaqueId) + ip := n.InternalPath() matches, err := filepath.Glob(ip) if err != nil { return err @@ -516,7 +516,7 @@ func (fs *Decomposedfs) DeleteStorageSpace(ctx context.Context, req *provider.De return err } - matches, err = filepath.Glob(filepath.Join(fs.o.Root, "spaces", spaceTypeAny, req.Id.OpaqueId)) + matches, err = filepath.Glob(filepath.Join(fs.o.Root, "spacetypes", spaceTypeAny, req.Id.OpaqueId)) if err != nil { return err } @@ -528,7 +528,7 @@ func (fs *Decomposedfs) DeleteStorageSpace(ctx context.Context, req *provider.De return err } - matches, err = filepath.Glob(filepath.Join(fs.o.Root, "nodes", node.RootID, req.Id.OpaqueId+node.TrashIDDelimiter+"*")) + matches, err = filepath.Glob(filepath.Join(fs.o.Root, "spaces", spaceID, "nodes", node.RootID, req.Id.OpaqueId+node.TrashIDDelimiter+"*")) if err != nil { return err } @@ -536,9 +536,9 @@ func (fs *Decomposedfs) DeleteStorageSpace(ctx context.Context, req *provider.De if len(matches) != 1 { return fmt.Errorf("delete root node failed: found %d matching root nodes", len(matches)) } - return os.RemoveAll(matches[0]) } + // don't delete - just rename dn := *n deletionTime := time.Now().UTC().Format(time.RFC3339Nano) @@ -548,6 +548,14 @@ func (fs *Decomposedfs) DeleteStorageSpace(ctx context.Context, req *provider.De if err != nil { return err } + matches, err := filepath.Glob(filepath.Join(fs.o.Root, "spacetypes", spaceTypeAny, spaceID)) + if err != nil { + return err + } + + if len(matches) != 1 { + return fmt.Errorf("delete space failed: found %d matching spaces", len(matches)) + } err = os.RemoveAll(matches[0]) if err != nil { @@ -559,14 +567,15 @@ func (fs *Decomposedfs) DeleteStorageSpace(ctx context.Context, req *provider.De return os.Symlink(trashPath, np) } -func (fs *Decomposedfs) createStorageSpace(ctx context.Context, spaceType, spaceID string) error { +func (fs *Decomposedfs) createStorageSpace(ctx context.Context, spaceType string, spaceID string) error { // create space type dir - if err := os.MkdirAll(filepath.Join(fs.o.Root, "spaces", spaceType), 0700); err != nil { + if err := os.MkdirAll(filepath.Join(fs.o.Root, "spacetypes", spaceType), 0700); err != nil { return err } // we can reuse the node id as the space id - err := os.Symlink("../../nodes/"+spaceID, filepath.Join(fs.o.Root, "spaces", spaceType, spaceID)) + // TODO pathify spaceid + err := os.Symlink("../../spaces/"+Pathify(spaceID, 1, 2)+"/nodes/"+Pathify(spaceID, 4, 2), filepath.Join(fs.o.Root, "spacetypes", spaceType, spaceID)) if err != nil { if isAlreadyExists(err) { appctx.GetLogger(ctx).Debug().Err(err).Str("space", spaceID).Str("spacetype", spaceType).Msg("symlink already exists") @@ -618,7 +627,7 @@ func (fs *Decomposedfs) storageSpaceFromNode(ctx context.Context, n *node.Node, return nil, err } - glob := filepath.Join(fs.o.Root, "spaces", spaceType, n.SpaceRoot.ID) + glob := filepath.Join(fs.o.Root, "spacetypes", spaceType, n.SpaceRoot.ID) matches, err := filepath.Glob(glob) if err != nil { return nil, err diff --git a/pkg/storage/utils/decomposedfs/testhelpers/helpers.go b/pkg/storage/utils/decomposedfs/testhelpers/helpers.go index 102647811aa..06ee32e9c64 100644 --- a/pkg/storage/utils/decomposedfs/testhelpers/helpers.go +++ b/pkg/storage/utils/decomposedfs/testhelpers/helpers.go @@ -136,9 +136,10 @@ func (t *TestEnv) CreateTestDir(name string, parentRef *providerv1beta1.Referenc } // CreateTestFile creates a new file and its metadata and returns a corresponding Node -func (t *TestEnv) CreateTestFile(name, blobID string, blobSize int64, parentID string) (*node.Node, error) { - // Create file in dir1 - file := node.New( +func (t *TestEnv) CreateTestFile(name, blobID, parentID, spaceID string, blobSize int64) (*node.Node, error) { + // Create n in dir1 + n := node.New( + spaceID, uuid.New().String(), parentID, name, @@ -147,22 +148,22 @@ func (t *TestEnv) CreateTestFile(name, blobID string, blobSize int64, parentID s nil, t.Lookup, ) - _, err := os.OpenFile(file.InternalPath(), os.O_CREATE, 0700) + _, err := os.OpenFile(n.InternalPath(), os.O_CREATE, 0700) if err != nil { return nil, err } - err = file.WriteAllNodeMetadata(t.Owner.Id) + err = n.WriteAllNodeMetadata(t.Owner.Id) if err != nil { return nil, err } // Link in parent - childNameLink := filepath.Join(t.Lookup.InternalPath(file.ParentID), file.Name) - err = os.Symlink("../"+file.ID, childNameLink) + childNameLink := filepath.Join(n.ParentInternalPath(), n.Name) + err = os.Symlink("../"+n.ID, childNameLink) if err != nil { return nil, err } - return file, err + return n, err } // CreateTestStorageSpace will create a storage space with some directories and files @@ -185,7 +186,8 @@ func (t *TestEnv) CreateTestStorageSpace(typ string, quota *providerv1beta1.Quot ref := buildRef(space.StorageSpace.Id.OpaqueId, "") // the space name attribute is the stop condition in the lookup - h, err := node.ReadNode(t.Ctx, t.Lookup, space.StorageSpace.Id.OpaqueId) + // Since we want to lookup the space node itself we do not provide a spaceID. + h, err := node.ReadNode(t.Ctx, t.Lookup, node.NoSpaceID, space.StorageSpace.Id.OpaqueId) if err != nil { return nil, err } @@ -201,7 +203,7 @@ func (t *TestEnv) CreateTestStorageSpace(typ string, quota *providerv1beta1.Quot } // Create file1 in dir1 - _, err = t.CreateTestFile("file1", "file1-blobid", 1234, dir1.ID) + _, err = t.CreateTestFile("file1", "file1-blobid", dir1.ID, dir1.SpaceID, 1234) if err != nil { return nil, err } diff --git a/pkg/storage/utils/decomposedfs/tree/tree.go b/pkg/storage/utils/decomposedfs/tree/tree.go index 3a657896148..005e62c96cf 100644 --- a/pkg/storage/utils/decomposedfs/tree/tree.go +++ b/pkg/storage/utils/decomposedfs/tree/tree.go @@ -38,7 +38,6 @@ import ( "github.com/cs3org/reva/pkg/utils" "github.com/google/uuid" "github.com/pkg/errors" - "github.com/pkg/xattr" "github.com/rs/zerolog/log" ) @@ -63,7 +62,7 @@ type PathLookup interface { RootNode(ctx context.Context) (node *node.Node, err error) InternalRoot() string - InternalPath(ID string) string + InternalPath(spaceID, nodeID string) string Path(ctx context.Context, n *node.Node) (path string, err error) ShareFolder() string } @@ -96,7 +95,8 @@ func New(root string, tta bool, tsa bool, lu PathLookup, bs Blobstore) *Tree { func (t *Tree) Setup(owner *userpb.UserId, propagateToRoot bool) error { // create data paths for internal layout dataPaths := []string{ - filepath.Join(t.root, "nodes"), + filepath.Join(t.root, "spaces"), + //filepath.Join(t.root, "nodes"), // notes contain symlinks from nodes//uploads/ to ../../uploads/ // better to keep uploads on a fast / volatile storage before a workflow finally moves them to the nodes dir filepath.Join(t.root, "uploads"), @@ -109,37 +109,12 @@ func (t *Tree) Setup(owner *userpb.UserId, propagateToRoot bool) error { } } - // the root node has an empty name - // the root node has no parent - n := node.New(node.RootID, "", "", 0, "", nil, t.lookup) - err := t.createNode(n, owner) - if err != nil { - return err - } - - // set propagation flag - v := "0" - if propagateToRoot { - v = "1" - } - if err = n.SetMetadata(xattrs.PropagationAttr, v); 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, spaceTypePersonal), 0700); err != nil { - return err - } - // create share spaces dir - if err := os.MkdirAll(filepath.Join(spacesPath, spaceTypeShare), 0700); err != nil { - return err - } + nodesPath := filepath.Join(t.root, "nodes") + fi, err := os.Stat(nodesPath) + if err == nil && fi.IsDir() { - f, err := os.Open(filepath.Join(t.root, "nodes")) + f, err := os.Open(nodesPath) if err != nil { return err } @@ -148,41 +123,80 @@ func (t *Tree) Setup(owner *userpb.UserId, propagateToRoot bool) error { return err } - for i := range nodes { - nodePath := filepath.Join(t.root, "nodes", nodes[i].Name()) + for _, node := range nodes { + nodePath := filepath.Join(nodesPath, node.Name()) - // is it a user root? -> create personal space if isRootNode(nodePath) { - // we can reuse the node id as the space id - t.linkSpace(spaceTypePersonal, nodes[i].Name(), nodes[i].Name()) + if err := t.moveNode(node.Name(), node.Name()); err != nil { + logger.New().Error().Err(err). + Str("space", node.Name()). + Msg("could not move space") + continue + } + t.linkSpace("personal", node.Name()) } + } + // TODO delete nodesPath if empty + + } - // is it a shared node? -> create share space - if isSharedNode(nodePath) { - // we can reuse the node id as the space id - t.linkSpace(spaceTypeShare, nodes[i].Name(), nodes[i].Name()) + return nil +} +func (t *Tree) moveNode(spaceID, nodeID string) error { + dirPath := filepath.Join(t.root, "nodes", nodeID) + f, err := os.Open(dirPath) + if err != nil { + return err + } + children, err := f.Readdir(0) + if err != nil { + return err + } + for _, child := range children { + old := filepath.Join(t.root, "nodes", child.Name()) + new := filepath.Join(t.root, "spaces", spaceID, "nodes", child.Name()) + if err := os.Rename(old, new); err != nil { + logger.New().Error().Err(err). + Str("space", spaceID). + Str("nodes", child.Name()). + Str("oldpath", old). + Str("newpath", new). + Msg("could not rename node") + } + if child.IsDir() { + if err := t.moveNode(spaceID, child.Name()); err != nil { + return err } } - } else if !fi.IsDir() { - // check if it is a directory - return fmt.Errorf("%s is not a directory", spacesPath) } - return nil } +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() +} + // linkSpace creates a new symbolic link for a space with the given type st, and node id -func (t *Tree) linkSpace(spaceType, spaceID, nodeID string) { - spacesPath := filepath.Join(t.root, "spaces", spaceType, spaceID) - expectedTarget := "../../nodes/" + nodeID - linkTarget, err := os.Readlink(spacesPath) +func (t *Tree) linkSpace(spaceType, spaceID string) { + spaceTypesPath := filepath.Join(t.root, "spacetypes", spaceType, spaceID) + expectedTarget := "../../spaces/" + Pathify(spaceID, 1, 2) + "/nodes/" + Pathify(spaceID, 4, 2) + linkTarget, err := os.Readlink(spaceTypesPath) if errors.Is(err, os.ErrNotExist) { - err = os.Symlink(expectedTarget, spacesPath) + err = os.Symlink(expectedTarget, spaceTypesPath) if err != nil { logger.New().Error().Err(err). Str("space_type", spaceType). Str("space", spaceID). - Str("node", nodeID). Msg("could not create symlink") } } else { @@ -190,14 +204,12 @@ func (t *Tree) linkSpace(spaceType, spaceID, nodeID string) { logger.New().Error().Err(err). Str("space_type", spaceType). Str("space", spaceID). - Str("node", nodeID). Msg("could not read symlink") } if linkTarget != expectedTarget { logger.New().Warn(). Str("space_type", spaceType). Str("space", spaceID). - Str("node", nodeID). Str("expected", expectedTarget). Str("actual", linkTarget). Msg("expected a different link target") @@ -209,6 +221,8 @@ func isRootNode(nodePath string) bool { attr, err := xattrs.Get(nodePath, xattrs.ParentidAttr) return err == nil && attr == node.RootID } + +/* func isSharedNode(nodePath string) bool { if attrs, err := xattr.List(nodePath); err == nil { for i := range attrs { @@ -219,6 +233,7 @@ func isSharedNode(nodePath string) bool { } 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) { @@ -263,7 +278,8 @@ func (t *Tree) CreateDir(ctx context.Context, n *node.Node) (err error) { } // make child appear in listings - err = os.Symlink("../"+n.ID, filepath.Join(t.lookup.InternalPath(n.ParentID), n.Name)) + relativeNodePath := filepath.Join("../../../../../", Pathify(n.ID, 4, 2)) + err = os.Symlink(relativeNodePath, filepath.Join(n.ParentInternalPath(), n.Name)) if err != nil { // no better way to check unfortunately if !strings.Contains(err.Error(), "file exists") { @@ -298,7 +314,8 @@ func (t *Tree) Move(ctx context.Context, oldNode *node.Node, newNode *node.Node) // are we just renaming (parent stays the same)? if oldNode.ParentID == newNode.ParentID { - parentPath := t.lookup.InternalPath(oldNode.ParentID) + // parentPath := t.lookup.InternalPath(oldNode.SpaceID, oldNode.ParentID) + parentPath := oldNode.ParentInternalPath() // rename child err = os.Rename( @@ -322,8 +339,8 @@ func (t *Tree) Move(ctx context.Context, oldNode *node.Node, newNode *node.Node) // rename child err = os.Rename( - filepath.Join(t.lookup.InternalPath(oldNode.ParentID), oldNode.Name), - filepath.Join(t.lookup.InternalPath(newNode.ParentID), newNode.Name), + filepath.Join(oldNode.ParentInternalPath(), oldNode.Name), + filepath.Join(newNode.ParentInternalPath(), newNode.Name), ) if err != nil { return errors.Wrap(err, "Decomposedfs: could not move child") @@ -352,6 +369,16 @@ func (t *Tree) Move(ctx context.Context, oldNode *node.Node, newNode *node.Node) return nil } +func readChildNodeFromLink(path string) (string, error) { + link, err := os.Readlink(path) + if err != nil { + return "", err + } + nodeID := strings.TrimLeft(link, "/.") + nodeID = strings.ReplaceAll(nodeID, "/", "") + return nodeID, nil +} + // ListFolder lists the content of a folder node func (t *Tree) ListFolder(ctx context.Context, n *node.Node) ([]*node.Node, error) { dir := n.InternalPath() @@ -370,13 +397,13 @@ func (t *Tree) ListFolder(ctx context.Context, n *node.Node) ([]*node.Node, erro } nodes := []*node.Node{} for i := range names { - link, err := os.Readlink(filepath.Join(dir, names[i])) + nodeID, err := readChildNodeFromLink(filepath.Join(dir, names[i])) if err != nil { // TODO log continue } - child, err := node.ReadNode(ctx, t.lookup, filepath.Base(link)) + child, err := node.ReadNode(ctx, t.lookup, n.SpaceID, nodeID) if err != nil { // TODO log continue @@ -394,14 +421,9 @@ func (t *Tree) Delete(ctx context.Context, n *node.Node) (err error) { deletingSharedResource := ctx.Value(appctx.DeletingSharedResource) if deletingSharedResource != nil && deletingSharedResource.(bool) { - src := filepath.Join(t.lookup.InternalPath(n.ParentID), n.Name) + src := filepath.Join(n.ParentInternalPath(), n.Name) return os.Remove(src) } - // Prepare the trash - err = os.MkdirAll(filepath.Join(t.root, "trash", n.SpaceRoot.ID), 0700) - if err != nil { - return - } // get the original path origin, err := t.lookup.Path(ctx, n) @@ -417,13 +439,24 @@ func (t *Tree) Delete(ctx context.Context, n *node.Node) (err error) { deletionTime := time.Now().UTC().Format(time.RFC3339Nano) + // Prepare the trash + trashLink := filepath.Join(t.root, "spaces", Pathify(n.SpaceRoot.ID, 1, 2), "trash", Pathify(n.ID, 4, 2)) + if err := os.MkdirAll(filepath.Dir(trashLink), 0700); err != nil { + // Roll back changes + n.RemoveMetadata(xattrs.TrashOriginAttr) + return err + } + + // FIXME can we just move the node into the trash dir? instead of adding another symlink and appending a trash timestamp? + // can we just use the mtime as the trash time? + // TODO store a trashed by userid + // first make node appear in the space trash // parent id and name are stored as extended attributes in the node itself - trashLink := filepath.Join(t.root, "trash", n.SpaceRoot.ID, n.ID) - err = os.Symlink("../../nodes/"+n.ID+node.TrashIDDelimiter+deletionTime, trashLink) + err = os.Symlink("../../../../../nodes/"+Pathify(n.ID, 4, 2)+node.TrashIDDelimiter+deletionTime, trashLink) if err != nil { - // To roll back changes - // TODO unset trashOriginAttr + // Roll back changes + n.RemoveMetadata(xattrs.TrashOriginAttr) return } @@ -435,7 +468,8 @@ func (t *Tree) Delete(ctx context.Context, n *node.Node) (err error) { if err != nil { // To roll back changes // TODO remove symlink - // TODO unset trashOriginAttr + // Roll back changes + n.RemoveMetadata(xattrs.TrashOriginAttr) return } @@ -443,13 +477,14 @@ func (t *Tree) Delete(ctx context.Context, n *node.Node) (err error) { _ = os.Remove(n.LockFilePath()) // finally remove the entry from the parent dir - src := filepath.Join(t.lookup.InternalPath(n.ParentID), n.Name) + src := filepath.Join(n.ParentInternalPath(), n.Name) err = os.Remove(src) if err != nil { // To roll back changes // TODO revert the rename // TODO remove symlink - // TODO unset trashOriginAttr + // Roll back changes + n.RemoveMetadata(xattrs.TrashOriginAttr) return } @@ -490,7 +525,7 @@ func (t *Tree) RestoreRecycleItemFunc(ctx context.Context, spaceid, key, trashPa } // add the entry for the parent dir - err = os.Symlink("../"+recycleNode.ID, filepath.Join(t.lookup.InternalPath(targetNode.ParentID), targetNode.Name)) + err = os.Symlink("../"+recycleNode.ID, filepath.Join(targetNode.ParentInternalPath(), targetNode.Name)) if err != nil { return err } @@ -782,26 +817,40 @@ func (t *Tree) createNode(n *node.Node, owner *userpb.UserId) (err error) { return n.WriteAllNodeMetadata(owner) } +// readTrashLink returns nodeID and timestamp +func readTrashLink(path string) (string, string, error) { + link, err := os.Readlink(path) + if err != nil { + return "", "", err + } + // ../../../../../nodes/e5/6c/75/a8/-d235-4cbb-8b4e-48b6fd0f2094.T.2022-02-16T14:38:11.769917408Z + // TODO use filepath.Separator to support windows + link = strings.ReplaceAll(link, "/", "") + // ..........nodese56c75a8-d235-4cbb-8b4e-48b6fd0f2094.T.2022-02-16T14:38:11.769917408Z + if link[0:15] != "..........nodes" || link[51:54] != ".T." { + return "", "", errtypes.InternalError("malformed trash link") + } + return link[15:51], link[54:], nil +} + // TODO refactor the returned params into Node properties? would make all the path transformations go away... -func (t *Tree) readRecycleItem(ctx context.Context, spaceid, key, path string) (recycleNode *node.Node, trashItem string, deletedNodePath string, origin string, err error) { +func (t *Tree) readRecycleItem(ctx context.Context, spaceID, key, path string) (recycleNode *node.Node, trashItem string, deletedNodePath string, origin string, err error) { if key == "" { return nil, "", "", "", errtypes.InternalError("key is empty") } - trashItem = filepath.Join(t.lookup.InternalRoot(), "trash", spaceid, key, path) + trashItem = filepath.Join(t.lookup.InternalRoot(), "spaces", Pathify(spaceID, 1, 2), "trash", Pathify(key, 4, 2), path) - var link string - link, err = os.Readlink(trashItem) + nodeID, timeSuffix, err := readTrashLink(trashItem) if err != nil { appctx.GetLogger(ctx).Error().Err(err).Str("trashItem", trashItem).Msg("error reading trash link") return } - var attrStr string - trashNodeID := filepath.Base(link) - deletedNodePath = t.lookup.InternalPath(trashNodeID) + deletedNodePath = t.lookup.InternalPath(spaceID, nodeID) + node.TrashIDDelimiter + timeSuffix owner := &userpb.UserId{} + var attrStr string // lookup ownerId in extended attributes if attrStr, err = xattrs.Get(deletedNodePath, xattrs.OwnerIDAttr); err == nil { owner.OpaqueId = attrStr @@ -821,7 +870,7 @@ func (t *Tree) readRecycleItem(ctx context.Context, spaceid, key, path string) ( return } - recycleNode = node.New(trashNodeID, "", "", 0, "", owner, t.lookup) + recycleNode = node.New(spaceID, nodeID, "", "", 0, "", owner, t.lookup) // lookup blobID in extended attributes if attrStr, err = xattrs.Get(deletedNodePath, xattrs.BlobIDAttr); err == nil { recycleNode.BlobID = attrStr @@ -844,16 +893,8 @@ func (t *Tree) readRecycleItem(ctx context.Context, spaceid, key, path string) ( } // look up space root from the trashed node - err = recycleNode.FindStorageSpaceRoot() - - if path == "" || path == "/" { - parts := strings.SplitN(filepath.Base(link), node.TrashIDDelimiter, 2) - if len(parts) != 2 { - appctx.GetLogger(ctx).Error().Err(err).Str("trashItem", trashItem).Interface("parts", parts).Msg("malformed trash link") - return - } - // update the node id, drop the `.T.{timestamp}` suffix - recycleNode.ID = parts[0] + if err = recycleNode.FindStorageSpaceRoot(); err != nil { + return } // get origin node, is relative to space root @@ -861,20 +902,19 @@ func (t *Tree) readRecycleItem(ctx context.Context, spaceid, key, path string) ( deletedNodeRootPath := deletedNodePath if path != "" && path != "/" { - trashItemRoot := filepath.Join(t.lookup.InternalRoot(), "trash", spaceid, key) - var rootLink string - rootLink, err = os.Readlink(trashItemRoot) + trashItemRoot := filepath.Join(t.lookup.InternalRoot(), "spaces", Pathify(spaceID, 1, 2), "trash", Pathify(key, 4, 2)) + nodeID, _, err = readTrashLink(trashItemRoot) if err != nil { appctx.GetLogger(ctx).Error().Err(err).Str("trashItem", trashItem).Msg("error reading trash link") return } - deletedNodeRootPath = t.lookup.InternalPath(filepath.Base(rootLink)) + deletedNodeRootPath = t.lookup.InternalPath(spaceID, nodeID) } // lookup origin path in extended attributes if attrStr, err = xattrs.Get(deletedNodeRootPath, xattrs.TrashOriginAttr); err == nil { origin = filepath.Join(attrStr, path) } else { - log.Error().Err(err).Str("trashItem", trashItem).Str("link", link).Str("deletedNodePath", deletedNodePath).Msg("could not read origin path, restoring to /") + log.Error().Err(err).Str("trashItem", trashItem).Str("deletedNodePath", deletedNodePath).Msg("could not read origin path, restoring to /") } return diff --git a/pkg/storage/utils/decomposedfs/tree/tree_test.go b/pkg/storage/utils/decomposedfs/tree/tree_test.go index 09d79f072e9..75fe4460d42 100644 --- a/pkg/storage/utils/decomposedfs/tree/tree_test.go +++ b/pkg/storage/utils/decomposedfs/tree/tree_test.go @@ -278,7 +278,7 @@ var _ = Describe("Tree", func() { Describe("with TreeTimeAccounting enabled", func() { It("sets the tmtime of the parent", func() { - file, err := env.CreateTestFile("file1", "", 1, dir.ID) + file, err := env.CreateTestFile("file1", "", dir.ID, dir.SpaceID, 1) Expect(err).ToNot(HaveOccurred()) perms := node.OwnerPermissions() @@ -296,7 +296,7 @@ var _ = Describe("Tree", func() { Describe("with TreeSizeAccounting enabled", func() { It("calculates the size", func() { - file, err := env.CreateTestFile("file1", "", 1, dir.ID) + file, err := env.CreateTestFile("file1", "", dir.ID, dir.SpaceID, 1) Expect(err).ToNot(HaveOccurred()) err = env.Tree.Propagate(env.Ctx, file) @@ -307,9 +307,9 @@ var _ = Describe("Tree", func() { }) It("considers all files", func() { - _, err := env.CreateTestFile("file1", "", 1, dir.ID) + _, err := env.CreateTestFile("file1", "", dir.ID, dir.SpaceID, 1) Expect(err).ToNot(HaveOccurred()) - file2, err := env.CreateTestFile("file2", "", 100, dir.ID) + file2, err := env.CreateTestFile("file2", "", dir.ID, dir.SpaceID, 100) Expect(err).ToNot(HaveOccurred()) err = env.Tree.Propagate(env.Ctx, file2) @@ -325,7 +325,7 @@ var _ = Describe("Tree", func() { err = subdir.SetTreeSize(uint64(200)) Expect(err).ToNot(HaveOccurred()) - file, err := env.CreateTestFile("file1", "", 1, dir.ID) + file, err := env.CreateTestFile("file1", "", dir.ID, dir.SpaceID, 1) Expect(err).ToNot(HaveOccurred()) err = env.Tree.Propagate(env.Ctx, file) diff --git a/pkg/storage/utils/decomposedfs/upload.go b/pkg/storage/utils/decomposedfs/upload.go index 895b590d35a..aefc953811a 100644 --- a/pkg/storage/utils/decomposedfs/upload.go +++ b/pkg/storage/utils/decomposedfs/upload.go @@ -456,7 +456,9 @@ func (upload *fileUpload) FinishUpload(ctx context.Context) (err error) { return } + spaceID := upload.info.Storage["SpaceRoot"] n := node.New( + spaceID, upload.info.Storage["NodeId"], upload.info.Storage["NodeParentId"], upload.info.Storage["NodeName"], @@ -465,7 +467,7 @@ func (upload *fileUpload) FinishUpload(ctx context.Context) (err error) { nil, upload.fs.lu, ) - n.SpaceRoot = node.New(upload.info.Storage["SpaceRoot"], "", "", 0, "", nil, upload.fs.lu) + n.SpaceRoot = node.New(spaceID, spaceID, "", "", 0, "", nil, upload.fs.lu) // check lock if err := n.CheckLock(ctx); err != nil { @@ -540,7 +542,7 @@ func (upload *fileUpload) FinishUpload(ctx context.Context) (err error) { if fi, err = os.Stat(targetPath); err == nil { // FIXME move versioning to blobs ... no need to copy all the metadata! well ... it does if we want to version metadata... // versions are stored alongside the actual file, so a rename can be efficient and does not cross storage / partition boundaries - versionsPath = upload.fs.lu.InternalPath(n.ID + ".REV." + fi.ModTime().UTC().Format(time.RFC3339Nano)) + versionsPath = upload.fs.lu.InternalPath(spaceID, n.ID+".REV."+fi.ModTime().UTC().Format(time.RFC3339Nano)) // This move drops all metadata!!! We copy it below with CopyMetadata // FIXME the node must remain the same. otherwise we might restore share metadata @@ -573,6 +575,7 @@ func (upload *fileUpload) FinishUpload(ctx context.Context) (err error) { Msg("Decomposedfs: could not truncate") return } + os.MkdirAll(filepath.Dir(targetPath), 0700) if err = os.Rename(upload.binPath, targetPath); err != nil { sublog.Err(err). Msg("Decomposedfs: could not rename") @@ -611,7 +614,7 @@ func (upload *fileUpload) FinishUpload(ctx context.Context) (err error) { } // link child name to parent if it is new - childNameLink := filepath.Join(upload.fs.lu.InternalPath(n.ParentID), n.Name) + childNameLink := filepath.Join(n.ParentInternalPath(), n.Name) var link string link, err = os.Readlink(childNameLink) if err == nil && link != "../"+n.ID { @@ -626,7 +629,8 @@ func (upload *fileUpload) FinishUpload(ctx context.Context) (err error) { } } if os.IsNotExist(err) || link != "../"+n.ID { - if err = os.Symlink("../"+n.ID, childNameLink); err != nil { + relativeNodePath := filepath.Join("../../../../../", Pathify(n.ID, 4, 2)) + if err = os.Symlink(relativeNodePath, childNameLink); err != nil { return errors.Wrap(err, "Decomposedfs: could not symlink child entry") } }