Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ocis driver: synctime based etag propagation #1180

Merged
merged 2 commits into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/unreleased/ocis-synctime-accounting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: introduce ocis driver treetime accounting

We added tree time accounting to the ocis storage driver which is modeled after [eos synctime accounting](http://eos-docs.web.cern.ch/eos-docs/configuration/namespace.html#enable-subtree-accounting).
It can be enabled using the new `treetime_accounting` option, which defaults to `false`
The `tmtime` is stored in an extended attribute `user.ocis.tmtime`. The treetime accounting is enabled for nodes which have the `user.ocis.propagation` extended attribute set to `"1"`. Currently, propagation is in sync.

https://github.com/cs3org/reva/pull/1180
1 change: 1 addition & 0 deletions pkg/storage/fs/ocis/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func (fs *ocisfs) SetArbitraryMetadata(ctx context.Context, ref *provider.Refere
}
nodePath := filepath.Join(fs.pw.Root, "nodes", n.ID)
for k, v := range md.Metadata {
// TODO set etag as temporary etag tmpEtagAttr
attrName := metadataPrefix + k
if err = xattr.Set(nodePath, attrName, []byte(v)); err != nil {
return errors.Wrap(err, "ocisfs: could not set metadata attribute "+attrName+" to "+k)
Expand Down
110 changes: 87 additions & 23 deletions pkg/storage/fs/ocis/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ package ocis

import (
"context"
"crypto/md5"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"

userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
Expand All @@ -47,17 +50,17 @@ type Node struct {

func (n *Node) writeMetadata(owner *userpb.UserId) (err error) {
nodePath := filepath.Join(n.pw.Root, "nodes", n.ID)
if err = xattr.Set(nodePath, "user.ocis.parentid", []byte(n.ParentID)); err != nil {
if err = xattr.Set(nodePath, parentidAttr, []byte(n.ParentID)); err != nil {
return errors.Wrap(err, "ocisfs: could not set parentid attribute")
}
if err = xattr.Set(nodePath, "user.ocis.name", []byte(n.Name)); err != nil {
if err = xattr.Set(nodePath, nameAttr, []byte(n.Name)); err != nil {
return errors.Wrap(err, "ocisfs: could not set name attribute")
}
if owner != nil {
if err = xattr.Set(nodePath, "user.ocis.owner.id", []byte(owner.OpaqueId)); err != nil {
if err = xattr.Set(nodePath, ownerIDAttr, []byte(owner.OpaqueId)); err != nil {
return errors.Wrap(err, "ocisfs: could not set owner id attribute")
}
if err = xattr.Set(nodePath, "user.ocis.owner.idp", []byte(owner.Idp)); err != nil {
if err = xattr.Set(nodePath, ownerIDPAttr, []byte(owner.Idp)); err != nil {
return errors.Wrap(err, "ocisfs: could not set owner idp attribute")
}
}
Expand All @@ -75,13 +78,13 @@ func ReadNode(ctx context.Context, pw *Path, id string) (n *Node, err error) {

// lookup parent id in extended attributes
var attrBytes []byte
if attrBytes, err = xattr.Get(nodePath, "user.ocis.parentid"); err == nil {
if attrBytes, err = xattr.Get(nodePath, parentidAttr); err == nil {
n.ParentID = string(attrBytes)
} else {
return
}
// lookup name in extended attributes
if attrBytes, err = xattr.Get(nodePath, "user.ocis.name"); err == nil {
if attrBytes, err = xattr.Get(nodePath, nameAttr); err == nil {
n.Name = string(attrBytes)
} else {
return
Expand All @@ -99,7 +102,7 @@ func ReadNode(ctx context.Context, pw *Path, id string) (n *Node, err error) {
// walk to root to check node is not part of a deleted subtree
parentPath := filepath.Join(n.pw.Root, "nodes", parentID)

if attrBytes, err = xattr.Get(parentPath, "user.ocis.parentid"); err == nil {
if attrBytes, err = xattr.Get(parentPath, parentidAttr); err == nil {
parentID = string(attrBytes)
log.Debug().Interface("node", n).Str("root.ID", root.ID).Str("parentID", parentID).Msg("ReadNode() found parent")
} else {
Expand Down Expand Up @@ -156,13 +159,13 @@ func (n *Node) Parent() (p *Node, err error) {

// lookup parent id in extended attributes
var attrBytes []byte
if attrBytes, err = xattr.Get(parentPath, "user.ocis.parentid"); err == nil {
if attrBytes, err = xattr.Get(parentPath, parentidAttr); err == nil {
p.ParentID = string(attrBytes)
} else {
return
}
// lookup name in extended attributes
if attrBytes, err = xattr.Get(parentPath, "user.ocis.name"); err == nil {
if attrBytes, err = xattr.Get(parentPath, nameAttr); err == nil {
p.Name = string(attrBytes)
} else {
return
Expand All @@ -186,13 +189,13 @@ func (n *Node) Owner() (id string, idp string, err error) {
// lookup parent id in extended attributes
var attrBytes []byte
// lookup name in extended attributes
if attrBytes, err = xattr.Get(nodePath, "user.ocis.owner.id"); err == nil {
if attrBytes, err = xattr.Get(nodePath, ownerIDAttr); err == nil {
n.ownerID = string(attrBytes)
} else {
return
}
// lookup name in extended attributes
if attrBytes, err = xattr.Get(nodePath, "user.ocis.owner.idp"); err == nil {
if attrBytes, err = xattr.Get(nodePath, ownerIDPAttr); err == nil {
n.ownerIDP = string(attrBytes)
} else {
return
Expand Down Expand Up @@ -230,32 +233,22 @@ func (n *Node) AsResourceInfo(ctx context.Context) (ri *provider.ResourceInfo, e
// nodeType = provider.ResourceType_RESOURCE_TYPE_REFERENCE
}

var etag []byte
// TODO optionally store etag in new `root/attributes/<uuid>` file
if etag, err = xattr.Get(nodePath, "user.ocis.etag"); err != nil {
log.Error().Err(err).Interface("node", n).Msg("could not read etag")
}

id := &provider.ResourceId{OpaqueId: n.ID}

fn, err = n.pw.Path(ctx, n)
if err != nil {
return nil, err
}

ri = &provider.ResourceInfo{
Id: id,
Path: fn,
Type: nodeType,
Etag: string(etag),
MimeType: mime.Detect(nodeType == provider.ResourceType_RESOURCE_TYPE_CONTAINER, fn),
Size: uint64(fi.Size()),
// TODO fix permissions
PermissionSet: &provider.ResourcePermissions{ListContainer: true, CreateContainer: true},
Mtime: &types.Timestamp{
Seconds: uint64(fi.ModTime().Unix()),
// TODO read nanos from where? Nanos: fi.MTimeNanos,
},
Target: string(target),
Target: string(target),
}

if owner, idp, err := n.Owner(); err == nil {
Expand All @@ -265,6 +258,41 @@ func (n *Node) AsResourceInfo(ctx context.Context) (ri *provider.ResourceInfo, e
}
}

// etag currently is a hash of fileid + tmtime (or mtime)
// TODO make etag of files use fileid and checksum
// TODO implment adding temporery etag in an attribute to restore backups
h := md5.New()
if _, err := io.WriteString(h, n.ID); err != nil {
return nil, err
}
var tmTime time.Time
if tmTime, err = n.GetTMTime(); err != nil {
// no tmtime, use mtime
tmTime = fi.ModTime()
}
if tb, err := tmTime.UTC().MarshalBinary(); err == nil {
if _, err := h.Write(tb); err != nil {
return nil, err
}
} else {
return nil, err
}

// use temporary etag if it is set
if b, err := xattr.Get(nodePath, tmpEtagAttr); err == nil {
ri.Etag = string(b)
} else {
ri.Etag = fmt.Sprintf("%x", h.Sum(nil))
}

// mtime uses tmtime if present
// TODO expose mtime and tmtime separately?
un := tmTime.UnixNano()
ri.Mtime = &types.Timestamp{
Seconds: uint64(un / 1000000000),
Nanos: uint32(un % 1000000000),
}

// TODO only read the requested metadata attributes
if attrs, err := xattr.List(nodePath); err == nil {
ri.ArbitraryMetadata = &provider.ArbitraryMetadata{
Expand All @@ -290,3 +318,39 @@ func (n *Node) AsResourceInfo(ctx context.Context) (ri *provider.ResourceInfo, e

return ri, nil
}

// HasPropagation checks if the propagation attribute exists and is set to "1"
func (n *Node) HasPropagation() (propagation bool) {
nodePath := filepath.Join(n.pw.Root, "nodes", n.ID)
if b, err := xattr.Get(nodePath, propagationAttr); err == nil {
return string(b) == "1"
}
return false
}

// GetTMTime reads the tmtime from the extended attributes
func (n *Node) GetTMTime() (tmTime time.Time, err error) {
nodePath := filepath.Join(n.pw.Root, "nodes", n.ID)
var b []byte
if b, err = xattr.Get(nodePath, treeMTimeAttr); err != nil {
return
}
return time.Parse(time.RFC3339Nano, string(b))
}

// SetTMTime writes the tmtime to the extended attributes
func (n *Node) SetTMTime(t time.Time) (err error) {
nodePath := filepath.Join(n.pw.Root, "nodes", n.ID)
return xattr.Set(nodePath, treeMTimeAttr, []byte(t.UTC().Format(time.RFC3339Nano)))
}

// UnsetTempEtag removes the temporary etag attribute
func (n *Node) UnsetTempEtag() (err error) {
nodePath := filepath.Join(n.pw.Root, "nodes", n.ID)
if err = xattr.Remove(nodePath, tmpEtagAttr); err != nil {
if e, ok := err.(*xattr.Error); ok && e.Err.Error() == "no data available" {
return nil
}
}
return err
}
67 changes: 55 additions & 12 deletions pkg/storage/fs/ocis/ocis.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"strings"

provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/pkg/appctx"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/logger"
"github.com/cs3org/reva/pkg/storage"
Expand All @@ -47,16 +48,39 @@ const (
// collisions with other apps We are going to introduce a sub namespace
// "user.ocis."

ocisPrefix string = "user.ocis."
parentidAttr string = ocisPrefix + "parentid"
ownerIDAttr string = ocisPrefix + "owner.id"
ownerIDPAttr string = ocisPrefix + "owner.idp"
// the base name of the node
// updated when the file is renamed or moved
nameAttr string = ocisPrefix + "name"

// SharePrefix is the prefix for sharing related extended attributes
sharePrefix string = "user.ocis.acl."
metadataPrefix string = "user.ocis.md."
sharePrefix string = ocisPrefix + "acl."
metadataPrefix string = ocisPrefix + "md."
// TODO implement favorites metadata flag
//favPrefix string = "user.ocis.fav." // favorite flag, per user
// TODO use etag prefix instead of single etag property
//etagPrefix string = "user.ocis.etag." // allow overriding a calculated etag with one from the extended attributes
referenceAttr string = "user.ocis.cs3.ref" // arbitrary metadata
//checksumPrefix string = "user.ocis.cs." // TODO add checksum support
trashOriginAttr string = "user.ocis.trash.origin" // trash origin
//favPrefix string = ocisPrefix + "fav." // favorite flag, per user

// a temporary etag for a folder that is removed when the mtime propagation happens
tmpEtagAttr string = ocisPrefix + "tmp.etag"
referenceAttr string = ocisPrefix + "cs3.ref" // arbitrary metadata
//checksumPrefix string = ocisPrefix + "cs." // TODO add checksum support
trashOriginAttr string = ocisPrefix + "trash.origin" // trash origin

// we use a single attribute to enable or disable propagation of both: synctime and treesize
propagationAttr string = ocisPrefix + "propagation"

// the tree modification time of the tree below this node,
// propagated when synctime_accounting is true and
// user.ocis.propagation=1 is set
// stored as a readable time.RFC3339Nano
treeMTimeAttr string = ocisPrefix + "tmtime"

// the size of the tree below this node,
// propagated when treesize_accounting is true and
// user.ocis.propagation=1 is set
//treesizeAttr string = ocisPrefix + "treesize"
)

func init() {
Expand Down Expand Up @@ -149,18 +173,27 @@ func (fs *ocisfs) CreateHome(ctx context.Context) (err error) {
return errtypes.NotSupported("ocisfs: CreateHome() home supported disabled")
}

var n *Node
var n, h *Node
if n, err = fs.pw.RootNode(ctx); err != nil {
return
}
_, err = fs.pw.WalkPath(ctx, n, fs.pw.mustGetUserLayout(ctx), func(ctx context.Context, n *Node) error {
h, err = fs.pw.WalkPath(ctx, n, fs.pw.mustGetUserLayout(ctx), func(ctx context.Context, n *Node) error {
if !n.Exists {
if err := fs.tp.CreateDir(ctx, n); err != nil {
return err
}
}
return nil
})

if fs.pw.TreeTimeAccounting {
homePath := filepath.Join(fs.pw.Root, "nodes", h.ID)
// mark the home node as the end of propagation
if err = xattr.Set(homePath, propagationAttr, []byte("1")); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Interface("node", h).Msg("could not mark home as propagation root")
return
}
}
return
}

Expand Down Expand Up @@ -190,7 +223,17 @@ func (fs *ocisfs) CreateDir(ctx context.Context, fn string) (err error) {
if node.Exists {
return errtypes.AlreadyExists(fn)
}
return fs.tp.CreateDir(ctx, node)
err = fs.tp.CreateDir(ctx, node)

if fs.pw.TreeTimeAccounting {
nodePath := filepath.Join(fs.pw.Root, "nodes", node.ID)
// mark the home node as the end of propagation
if err = xattr.Set(nodePath, propagationAttr, []byte("1")); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Interface("node", node).Msg("could not mark node to propagate")
return
}
}
return
}

// CreateReference creates a reference as a node folder with the target stored in extended attributes
Expand Down Expand Up @@ -347,7 +390,7 @@ func (fs *ocisfs) copyMD(s string, t string) (err error) {
return err
}
for i := range attrs {
if strings.HasPrefix(attrs[i], "user.ocis.") {
if strings.HasPrefix(attrs[i], ocisPrefix) {
var d []byte
if d, err = xattr.Get(s, attrs[i]); err != nil {
return err
Expand Down
10 changes: 8 additions & 2 deletions pkg/storage/fs/ocis/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,16 @@ type Path struct {
UserLayout string `mapstructure:"user_layout"`

// TODO NodeLayout option to save nodes as eg. nodes/1d/d8/1dd84abf-9466-4e14-bb86-02fc4ea3abcf
ShareFolder string `mapstructure:"share_folder"`

// EnableHome enables the creation of home directories.
EnableHome bool `mapstructure:"enable_home"`
ShareFolder string `mapstructure:"share_folder"`
EnableHome bool `mapstructure:"enable_home"`

// propagate mtime changes as tmtime (tree modification time) to the parent directory when user.ocis.propagation=1 is set on a node
TreeTimeAccounting bool `mapstructure:"treetime_accounting"`

// propagate size changes as treesize
TreeSizeAccounting bool `mapstructure:"treesize_accounting"`
}

// NodeFromResource takes in a request path or request id and converts it to a Node
Expand Down
Loading