diff --git a/pkg/storage/utils/decomposedfs/node/node.go b/pkg/storage/utils/decomposedfs/node/node.go index ce05db4ae3f..031075a5479 100644 --- a/pkg/storage/utils/decomposedfs/node/node.go +++ b/pkg/storage/utils/decomposedfs/node/node.go @@ -138,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() diff --git a/pkg/storage/utils/decomposedfs/recycle.go b/pkg/storage/utils/decomposedfs/recycle.go index f02d257aef3..add667dd3cd 100644 --- a/pkg/storage/utils/decomposedfs/recycle.go +++ b/pkg/storage/utils/decomposedfs/recycle.go @@ -166,56 +166,58 @@ func (fs *Decomposedfs) createTrashItem(ctx context.Context, spaceID, parentNode 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(spaceID, 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/spaces.go b/pkg/storage/utils/decomposedfs/spaces.go index 5bef609c2a8..141fff9302d 100644 --- a/pkg/storage/utils/decomposedfs/spaces.go +++ b/pkg/storage/utils/decomposedfs/spaces.go @@ -195,9 +195,10 @@ func readSpaceAndNodeFromSpaceTypeLink(path string) (string, string, error) { 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, "/", "") - // ../../spaces/4c/510ada-c86b-4815-8820-42cdf82c3d51/nodes/4c/51/0a/da/-c86b-4815-8820-42cdf82c3d51 + // ....spaces4c510ada-c86b-4815-8820-42cdf82c3d51nodes4c510ada-c86b-4815-8820-42cdf82c3d51 if link[0:10] != "....spaces" || link[46:51] != "nodes" { return "", "", errtypes.InternalError("malformed link") } @@ -363,10 +364,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, "spacetypes", spaceTypeAny, spaceID)) + matches, err := filepath.Glob(filepath.Join(fs.o.Root, "spacetypes", spaceTypeAny, nodeID)) if err != nil { return nil, err } @@ -402,28 +403,7 @@ func (fs *Decomposedfs) UpdateStorageSpace(ctx context.Context, req *provider.Up } } - matches, err := filepath.Glob(filepath.Join(fs.o.Root, "spacetypes", 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") - } - nodeID := strings.TrimLeft(target, "/.") - nodeID = strings.ReplaceAll(nodeID, "/", "") - node, err := node.ReadNode(ctx, fs.lu, spaceID, nodeID) - + node, err := node.ReadNode(ctx, fs.lu, spaceID, spaceID) if err != nil { return nil, err } @@ -581,6 +561,7 @@ func (fs *Decomposedfs) createStorageSpace(ctx context.Context, spaceType string } // we can reuse the node id as the space id + // 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) { diff --git a/pkg/storage/utils/decomposedfs/tree/tree.go b/pkg/storage/utils/decomposedfs/tree/tree.go index 601bcc9ae2f..c8723e5d04e 100644 --- a/pkg/storage/utils/decomposedfs/tree/tree.go +++ b/pkg/storage/utils/decomposedfs/tree/tree.go @@ -424,11 +424,6 @@ func (t *Tree) Delete(ctx context.Context, n *node.Node) (err error) { 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) @@ -444,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.SpaceRoot.ID+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 } @@ -462,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 } @@ -476,7 +483,8 @@ func (t *Tree) Delete(ctx context.Context, n *node.Node) (err error) { // To roll back changes // TODO revert the rename // TODO remove symlink - // TODO unset trashOriginAttr + // Roll back changes + n.RemoveMetadata(xattrs.TrashOriginAttr) return }