diff --git a/changelog/unreleased/ocis-favorites-etags-mtime-metadata.md b/changelog/unreleased/ocis-favorites-etags-mtime-metadata.md
new file mode 100644
index 0000000000..004175a62c
--- /dev/null
+++ b/changelog/unreleased/ocis-favorites-etags-mtime-metadata.md
@@ -0,0 +1,7 @@
+Enhancement: allow setting favorites, mtime and a temporary etag
+
+We now let the ocis driver persist favorites, set temporary etags and the mtime as arbitrary metadata.
+
+https://github.com/cs3org/reva/pull/1393
+https://github.com/owncloud/ocis/issues/567
+https://github.com/cs3org/reva/issues/1394
diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go
index b0c17af211..7c4a689e42 100644
--- a/internal/http/services/owncloud/ocdav/propfind.go
+++ b/internal/http/services/owncloud/ocdav/propfind.go
@@ -45,6 +45,14 @@ import (
"github.com/pkg/errors"
)
+const (
+ _nsDav = "DAV:"
+ _nsOwncloud = "http://owncloud.org/ns"
+ _nsOCS = "http://open-collaboration-services.org/ns"
+
+ _propOcFavorite = "http://owncloud.org/ns/favorite"
+)
+
// ns is the namespace that is prefixed to the path in the cs3 namespace
func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string) {
ctx := r.Context()
@@ -98,14 +106,23 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string)
return
}
+ metadataKeys := []string{}
+ if pf.Allprop != nil {
+ metadataKeys = append(metadataKeys, "*")
+ } else {
+ for i := range pf.Prop {
+ if requiresExplicitFetching(&pf.Prop[i]) {
+ metadataKeys = append(metadataKeys, metadataKeyOf(&pf.Prop[i]))
+ }
+ }
+ }
+
info := res.Info
infos := []*provider.ResourceInfo{info}
if info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER && depth == "1" {
req := &provider.ListContainerRequest{
- Ref: ref,
- ArbitraryMetadataKeys: []string{
- "http://owncloud.org/ns/share-types",
- },
+ Ref: ref,
+ ArbitraryMetadataKeys: metadataKeys,
}
res, err := client.ListContainer(ctx, req)
if err != nil {
@@ -130,10 +147,8 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string)
Spec: &provider.Reference_Path{Path: path},
}
req := &provider.ListContainerRequest{
- Ref: ref,
- ArbitraryMetadataKeys: []string{
- "http://owncloud.org/ns/share-types",
- },
+ Ref: ref,
+ ArbitraryMetadataKeys: metadataKeys,
}
res, err := client.ListContainer(ctx, req)
if err != nil {
@@ -188,6 +203,23 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string)
}
}
+func requiresExplicitFetching(n *xml.Name) bool {
+ switch n.Space {
+ case _nsDav:
+ return false
+ case _nsOwncloud:
+ switch n.Local {
+ case "favorite", "share-types":
+ return true
+ default:
+ return false
+ }
+ case _nsOCS:
+ return false
+ }
+ return true
+}
+
// from https://github.com/golang/net/blob/e514e69ffb8bc3c76a71ae40de0118d794855992/webdav/xml.go#L178-L205
func readPropfind(r io.Reader) (pf propfindXML, status int, err error) {
c := countingReader{r: r}
@@ -253,6 +285,7 @@ func (s *svc) newPropNS(namespace string, local string, val string) *propertyXML
}
}
+// TODO properly use the space
func (s *svc) newProp(key, val string) *propertyXML {
return &propertyXML{
XMLName: xml.Name{Space: "", Local: key},
@@ -368,8 +401,8 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide
response.Propstat[0].Prop = append(response.Propstat[0].Prop, s.newProp("oc:favorite", "0"))
} else if amd := k.GetMetadata(); amd == nil {
response.Propstat[0].Prop = append(response.Propstat[0].Prop, s.newProp("oc:favorite", "0"))
- } else if v, ok := amd["http://owncloud.org/ns/favorite"]; ok && v != "" {
- response.Propstat[0].Prop = append(response.Propstat[0].Prop, s.newProp("oc:favorite", "1"))
+ } else if v, ok := amd[_propOcFavorite]; ok && v != "" {
+ response.Propstat[0].Prop = append(response.Propstat[0].Prop, s.newProp("oc:favorite", v))
} else {
response.Propstat[0].Prop = append(response.Propstat[0].Prop, s.newProp("oc:favorite", "0"))
}
@@ -387,7 +420,7 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide
size := fmt.Sprintf("%d", md.Size)
for i := range pf.Prop {
switch pf.Prop[i].Space {
- case "http://owncloud.org/ns":
+ case _nsOwncloud:
switch pf.Prop[i].Local {
// TODO(jfd): maybe phoenix and the other clients can just use this id as an opaque string?
// I tested the desktop client and phoenix to annotate which properties are requestted, see below cases
@@ -491,7 +524,7 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide
propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:favorite", "0"))
} else if amd := k.GetMetadata(); amd == nil {
propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:favorite", "0"))
- } else if v, ok := amd["http://owncloud.org/ns/favorite"]; ok && v != "" {
+ } else if v, ok := amd[_propOcFavorite]; ok && v != "" {
propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:favorite", "1"))
} else {
propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:favorite", "0"))
@@ -511,7 +544,7 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide
case "share-types": // desktop
k := md.GetArbitraryMetadata()
amd := k.GetMetadata()
- if amdv, ok := amd[fmt.Sprintf("%s/%s", pf.Prop[i].Space, pf.Prop[i].Local)]; ok {
+ if amdv, ok := amd[metadataKeyOf(&pf.Prop[i])]; ok {
st := fmt.Sprintf("%s", amdv)
propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:share-types", st))
} else {
@@ -546,7 +579,7 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide
default:
propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:"+pf.Prop[i].Local, ""))
}
- case "DAV:":
+ case _nsDav:
switch pf.Prop[i].Local {
case "getetag": // both
if md.Etag != "" {
@@ -586,7 +619,7 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide
default:
propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("d:"+pf.Prop[i].Local, ""))
}
- case "http://open-collaboration-services.org/ns":
+ case _nsOCS:
switch pf.Prop[i].Local {
// ocs:share-permissions indicate clients the maximum permissions that can be granted:
// 1 = read
@@ -614,7 +647,7 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide
propstatNotFound.Prop = append(propstatNotFound.Prop, s.newPropNS(pf.Prop[i].Space, pf.Prop[i].Local, ""))
} else if amd := k.GetMetadata(); amd == nil {
propstatNotFound.Prop = append(propstatNotFound.Prop, s.newPropNS(pf.Prop[i].Space, pf.Prop[i].Local, ""))
- } else if v, ok := amd[fmt.Sprintf("%s/%s", pf.Prop[i].Space, pf.Prop[i].Local)]; ok && v != "" {
+ } else if v, ok := amd[metadataKeyOf(&pf.Prop[i])]; ok && v != "" {
propstatOK.Prop = append(propstatOK.Prop, s.newPropNS(pf.Prop[i].Space, pf.Prop[i].Local, v))
} else {
propstatNotFound.Prop = append(propstatNotFound.Prop, s.newPropNS(pf.Prop[i].Space, pf.Prop[i].Local, ""))
@@ -654,6 +687,10 @@ func (c *countingReader) Read(p []byte) (int, error) {
return n, err
}
+func metadataKeyOf(n *xml.Name) string {
+ return fmt.Sprintf("%s/%s", n.Space, n.Local)
+}
+
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind)
type propfindProps []xml.Name
diff --git a/internal/http/services/owncloud/ocdav/proppatch.go b/internal/http/services/owncloud/ocdav/proppatch.go
index 218bcd5c0f..b0be5ab325 100644
--- a/internal/http/services/owncloud/ocdav/proppatch.go
+++ b/internal/http/services/owncloud/ocdav/proppatch.go
@@ -218,7 +218,7 @@ func (s *svc) formatProppatchResponse(ctx context.Context, acceptedProps []xml.N
func (s *svc) isBooleanProperty(prop string) bool {
// TODO add other properties we know to be boolean?
- return prop == "http://owncloud.org/ns/favorite"
+ return prop == _propOcFavorite
}
func (s *svc) as0or1(val string) string {
@@ -307,9 +307,9 @@ func readProppatch(r io.Reader) (patches []Proppatch, status int, err error) {
for _, op := range pu.SetRemove {
remove := false
switch op.XMLName {
- case xml.Name{Space: "DAV:", Local: "set"}:
+ case xml.Name{Space: _nsDav, Local: "set"}:
// No-op.
- case xml.Name{Space: "DAV:", Local: "remove"}:
+ case xml.Name{Space: _nsDav, Local: "remove"}:
for _, p := range op.Prop {
if len(p.InnerXML) > 0 {
return nil, http.StatusBadRequest, errInvalidProppatch
diff --git a/internal/http/services/owncloud/ocdav/trashbin.go b/internal/http/services/owncloud/ocdav/trashbin.go
index e931bef312..4fa4e3dc1d 100644
--- a/internal/http/services/owncloud/ocdav/trashbin.go
+++ b/internal/http/services/owncloud/ocdav/trashbin.go
@@ -295,7 +295,7 @@ func (h *TrashbinHandler) itemToPropResponse(ctx context.Context, s *svc, pf *pr
size := fmt.Sprintf("%d", item.Size)
for i := range pf.Prop {
switch pf.Prop[i].Space {
- case "http://owncloud.org/ns":
+ case _nsOwncloud:
switch pf.Prop[i].Local {
case "oc:size":
if item.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
@@ -314,7 +314,7 @@ func (h *TrashbinHandler) itemToPropResponse(ctx context.Context, s *svc, pf *pr
default:
propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:"+pf.Prop[i].Local, ""))
}
- case "DAV:":
+ case _nsDav:
switch pf.Prop[i].Local {
case "getcontentlength":
if item.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
diff --git a/internal/http/services/owncloud/ocs/data/capabilities.go b/internal/http/services/owncloud/ocs/data/capabilities.go
index b96bf99b2c..40fd128cd8 100644
--- a/internal/http/services/owncloud/ocs/data/capabilities.go
+++ b/internal/http/services/owncloud/ocs/data/capabilities.go
@@ -99,6 +99,7 @@ type CapabilitiesFiles struct {
BigFileChunking ocsBool `json:"bigfilechunking" xml:"bigfilechunking"`
Undelete ocsBool `json:"undelete" xml:"undelete"`
Versioning ocsBool `json:"versioning" xml:"versioning"`
+ Favorites ocsBool `json:"favorites" xml:"favorites"`
BlacklistedFiles []string `json:"blacklisted_files" xml:"blacklisted_files>element" mapstructure:"blacklisted_files"`
TusSupport *CapabilitiesFilesTusSupport `json:"tus_support" xml:"tus_support" mapstructure:"tus_support"`
}
diff --git a/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go b/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go
index a71a311d15..77932654e0 100644
--- a/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go
+++ b/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go
@@ -102,6 +102,7 @@ func (h *Handler) Init(c *config.Config) {
}
// h.c.Capabilities.Files.Undelete is boolean
// h.c.Capabilities.Files.Versioning is boolean
+ // h.c.Capabilities.Files.Favorites is boolean
// dav
diff --git a/pkg/storage/fs/ocis/metadata.go b/pkg/storage/fs/ocis/metadata.go
index ff167a8b41..47cdbc885c 100644
--- a/pkg/storage/fs/ocis/metadata.go
+++ b/pkg/storage/fs/ocis/metadata.go
@@ -20,20 +20,37 @@ package ocis
import (
"context"
+ "fmt"
"path/filepath"
+ "strconv"
+ "strings"
+ "time"
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/user"
"github.com/pkg/errors"
"github.com/pkg/xattr"
)
+func parseMTime(v string) (t time.Time, err error) {
+ p := strings.SplitN(v, ".", 2)
+ var sec, nsec int64
+ if sec, err = strconv.ParseInt(p[0], 10, 64); err == nil {
+ if len(p) > 1 {
+ nsec, err = strconv.ParseInt(p[1], 10, 64)
+ }
+ }
+ return time.Unix(sec, nsec), err
+}
+
func (fs *ocisfs) SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) (err error) {
n, err := fs.lu.NodeFromResource(ctx, ref)
if err != nil {
return errors.Wrap(err, "ocisfs: error resolving ref")
}
+ sublog := appctx.GetLogger(ctx).With().Interface("node", n).Logger()
if !n.Exists {
err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name))
@@ -52,14 +69,67 @@ func (fs *ocisfs) SetArbitraryMetadata(ctx context.Context, ref *provider.Refere
}
nodePath := n.lu.toInternalPath(n.ID)
+
+ errs := []error{}
+ // TODO should we really continue updating when an error occurs?
+ if md.Metadata != nil {
+ if val, ok := md.Metadata["mtime"]; ok {
+ delete(md.Metadata, "mtime")
+ err := n.SetMtime(ctx, val)
+ if err != nil {
+ errs = append(errs, errors.Wrap(err, "could not set mtime"))
+ }
+ }
+ // TODO(jfd) special handling for atime?
+ // TODO(jfd) allow setting birth time (btime)?
+ // TODO(jfd) any other metadata that is interesting? fileid?
+ // TODO unset when file is updated
+ // TODO unset when folder is updated or add timestamp to etag?
+ if val, ok := md.Metadata["etag"]; ok {
+ delete(md.Metadata, "etag")
+ err := n.SetEtag(ctx, val)
+ if err != nil {
+ errs = append(errs, errors.Wrap(err, "could not set etag"))
+ }
+ }
+ if val, ok := md.Metadata[_favoriteKey]; ok {
+ delete(md.Metadata, _favoriteKey)
+ if u, ok := user.ContextGetUser(ctx); ok {
+ if uid := u.GetId(); uid != nil {
+ if err := n.SetFavorite(uid, val); err != nil {
+ sublog.Error().Err(err).
+ Interface("user", u).
+ Msg("could not set favorite flag")
+ errs = append(errs, errors.Wrap(err, "could not set favorite flag"))
+ }
+ } else {
+ sublog.Error().Interface("user", u).Msg("user has no id")
+ errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "user has no id"))
+ }
+ } else {
+ sublog.Error().Interface("user", u).Msg("error getting user from ctx")
+ errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from ctx"))
+ }
+ }
+ }
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)
+ errs = append(errs, errors.Wrap(err, "ocisfs: could not set metadata attribute "+attrName+" to "+k))
}
}
- return
+
+ switch len(errs) {
+ case 0:
+ return fs.tp.Propagate(ctx, n)
+ case 1:
+ // TODO Propagate if anything changed
+ return errs[0]
+ default:
+ // TODO Propagate if anything changed
+ // TODO how to return multiple errors?
+ return errors.New("multiple errors occurred, see log for details")
+ }
}
func (fs *ocisfs) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) (err error) {
@@ -67,6 +137,7 @@ func (fs *ocisfs) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Refe
if err != nil {
return errors.Wrap(err, "ocisfs: error resolving ref")
}
+ sublog := appctx.GetLogger(ctx).With().Interface("node", n).Logger()
if !n.Exists {
err = errtypes.NotFound(filepath.Join(n.ParentID, n.Name))
@@ -74,7 +145,7 @@ func (fs *ocisfs) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Refe
}
ok, err := fs.p.HasPermission(ctx, n, func(rp *provider.ResourcePermissions) bool {
- // TODO use SetArbitraryMetadata grant to CS3 api, tracked in https://github.com/cs3org/cs3apis/issues/91
+ // TODO use SetArbitraryMetadata grant to CS3 api, tracked in https://github.com/cs3org/cs3apis/issues/91
return rp.InitiateFileUpload
})
switch {
@@ -85,22 +156,57 @@ func (fs *ocisfs) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Refe
}
nodePath := n.lu.toInternalPath(n.ID)
- for i := range keys {
- attrName := metadataPrefix + keys[i]
- if err = xattr.Remove(nodePath, attrName); err != nil {
- // a non-existing attribute will return an error, which we can ignore
- // (using string compare because the error type is syscall.Errno and not wrapped/recognizable)
- if e, ok := err.(*xattr.Error); !ok || !(e.Err.Error() == "no data available" ||
- // darwin
- e.Err.Error() == "attribute not found") {
- appctx.GetLogger(ctx).Error().Err(err).
- Interface("node", n).
- Str("key", keys[i]).
- Msg("could not unset metadata")
+ errs := []error{}
+ for _, k := range keys {
+ switch k {
+ case _favoriteKey:
+ if u, ok := user.ContextGetUser(ctx); ok {
+ // the favorite flag is specific to the user, so we need to incorporate the userid
+ if uid := u.GetId(); uid != nil {
+ fa := fmt.Sprintf("%s%s@%s", favPrefix, uid.GetOpaqueId(), uid.GetIdp())
+ if err := xattr.Remove(nodePath, fa); err != nil {
+ sublog.Error().Err(err).
+ Interface("user", u).
+ Str("key", fa).
+ Msg("could not unset favorite flag")
+ errs = append(errs, errors.Wrap(err, "could not unset favorite flag"))
+ }
+ } else {
+ sublog.Error().
+ Interface("user", u).
+ Msg("user has no id")
+ errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "user has no id"))
+ }
} else {
- err = nil
+ sublog.Error().
+ Interface("user", u).
+ Msg("error getting user from ctx")
+ errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from ctx"))
+ }
+ default:
+ if err = xattr.Remove(nodePath, metadataPrefix+k); err != nil {
+ // a non-existing attribute will return an error, which we can ignore
+ // (using string compare because the error type is syscall.Errno and not wrapped/recognizable)
+ if e, ok := err.(*xattr.Error); !ok || !(e.Err.Error() == "no data available" ||
+ // darwin
+ e.Err.Error() == "attribute not found") {
+ sublog.Error().Err(err).
+ Str("key", k).
+ Msg("could not unset metadata")
+ errs = append(errs, errors.Wrap(err, "could not unset metadata"))
+ }
}
}
}
- return
+ switch len(errs) {
+ case 0:
+ return fs.tp.Propagate(ctx, n)
+ case 1:
+ // TODO Propagate if anything changed
+ return errs[0]
+ default:
+ // TODO Propagate if anything changed
+ // TODO how to return multiple errors?
+ return errors.New("multiple errors occurred, see log for details")
+ }
}
diff --git a/pkg/storage/fs/ocis/node.go b/pkg/storage/fs/ocis/node.go
index 851597f12e..c4ba183c15 100644
--- a/pkg/storage/fs/ocis/node.go
+++ b/pkg/storage/fs/ocis/node.go
@@ -34,7 +34,6 @@ import (
"github.com/cs3org/reva/pkg/appctx"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/mime"
- "github.com/cs3org/reva/pkg/sdk/common"
"github.com/cs3org/reva/pkg/storage/utils/ace"
"github.com/cs3org/reva/pkg/user"
"github.com/pkg/errors"
@@ -45,6 +44,8 @@ import (
const (
_shareTypesKey = "http://owncloud.org/ns/share-types"
_userShareType = "0"
+
+ _favoriteKey = "http://owncloud.org/ns/favorite"
)
// Node represents a node in the tree and provides methods to get a Parent or Child instance
@@ -328,6 +329,97 @@ func (n *Node) PermissionSet(ctx context.Context) *provider.ResourcePermissions
return noPermissions
}
+// calculateEtag returns a hash of fileid + tmtime (or mtime)
+func calculateEtag(nodeID string, tmTime time.Time) (string, error) {
+ h := md5.New()
+ if _, err := io.WriteString(h, nodeID); err != nil {
+ return "", err
+ }
+ if tb, err := tmTime.UTC().MarshalBinary(); err == nil {
+ if _, err := h.Write(tb); err != nil {
+ return "", err
+ }
+ } else {
+ return "", err
+ }
+ return fmt.Sprintf(`"%x"`, h.Sum(nil)), nil
+}
+
+// SetMtime sets the mtime and atime of a node
+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.toInternalPath(n.ID)
+ // updating mtime also updates atime
+ if err := os.Chtimes(nodePath, mt, mt); err != nil {
+ sublog.Error().Err(err).
+ Time("mtime", mt).
+ Msg("could not set mtime")
+ return errors.Wrap(err, "could not set mtime")
+ }
+ } else {
+ sublog.Error().Err(err).
+ Str("mtime", mtime).
+ Msg("could not parse mtime")
+ return errors.Wrap(err, "could not parse mtime")
+ }
+ return nil
+}
+
+// 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.toInternalPath(n.ID)
+ var tmTime time.Time
+ if tmTime, err = n.GetTMTime(); err != nil {
+ // no tmtime, use mtime
+ var fi os.FileInfo
+ if fi, err = os.Lstat(nodePath); err != nil {
+ return
+ }
+ tmTime = fi.ModTime()
+ }
+ var etag string
+ if etag, err = calculateEtag(n.ID, tmTime); err != nil {
+ return
+ }
+
+ // sanitize etag
+ val = fmt.Sprintf("\"%s\"", strings.Trim(val, "\""))
+ if etag == val {
+ sublog.Debug().
+ Str("etag", val).
+ Msg("ignoring request to update identical etag")
+ return nil
+ }
+ // etag is only valid until the calculated etag changes, is part of propagation
+ return xattr.Set(nodePath, tmpEtagAttr, []byte(val))
+}
+
+// SetFavorite sets the favorite for the current user
+// TODO we should not mess with the user here ... the favorites is now a user specific property for a file
+// that cannot be mapped to extended attributes without leaking who has marked a file as a favorite
+// it is a specific case of a tag, which is user individual as well
+// TODO there are different types of tags
+// 1. public that are managed by everyone
+// 2. private tags that are only visible to the user
+// 3. system tags that are only visible to the system
+// 4. group tags that are only visible to a group ...
+// urgh ... well this can be solved using different namespaces
+// 1. public = p:
+// 2. private = u:: for user specific
+// 3. system = s: for system
+// 4. group = g::
+// 5. app? = a:: for apps?
+// 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.toInternalPath(n.ID)
+ // the favorite flag is specific to the user, so we need to incorporate the userid
+ fa := fmt.Sprintf("%s%s@%s", favPrefix, uid.GetOpaqueId(), uid.GetIdp())
+ return xattr.Set(nodePath, fa, []byte(val))
+}
+
// AsResourceInfo return the node as CS3 ResourceInfo
func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissions, mdKeys []string) (ri *provider.ResourceInfo, err error) {
sublog := appctx.GetLogger(ctx).With().Interface("node", n).Logger()
@@ -379,31 +471,19 @@ func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissi
sublog.Debug().Err(err).Msg("could not determine owner")
}
- // 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 = fmt.Sprintf(`"%x"`, string(b))
- } else {
- ri.Etag = fmt.Sprintf(`"%x"`, h.Sum(nil))
+ } else if ri.Etag, err = calculateEtag(n.ID, tmTime); err != nil {
+ sublog.Debug().Err(err).Msg("could not calculate etag")
}
// mtime uses tmtime if present
@@ -414,30 +494,74 @@ func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissi
Nanos: uint32(un % 1000000000),
}
- // TODO only read the requested metadata attributes
- if attrs, err := xattr.List(nodePath); err == nil {
- ri.ArbitraryMetadata = &provider.ArbitraryMetadata{
- Metadata: map[string]string{},
- }
- for i := range attrs {
- if strings.HasPrefix(attrs[i], metadataPrefix) {
- k := strings.TrimPrefix(attrs[i], metadataPrefix)
- if v, err := xattr.Get(nodePath, attrs[i]); err == nil {
- ri.ArbitraryMetadata.Metadata[k] = string(v)
- } else {
- sublog.Error().Err(err).Str("attr", attrs[i]).Msg("could not get attribute value")
+ mdKeysMap := make(map[string]struct{})
+ for _, k := range mdKeys {
+ mdKeysMap[k] = struct{}{}
+ }
+
+ var returnAllKeys bool
+ if _, ok := mdKeysMap["*"]; len(mdKeys) == 0 || ok {
+ returnAllKeys = true
+ }
+
+ metadata := map[string]string{}
+
+ // read favorite flag for the current user
+ if _, ok := mdKeysMap[_favoriteKey]; returnAllKeys || ok {
+ favorite := ""
+ if u, ok := user.ContextGetUser(ctx); ok {
+ // the favorite flag is specific to the user, so we need to incorporate the userid
+ if uid := u.GetId(); uid != nil {
+ fa := fmt.Sprintf("%s%s@%s", favPrefix, uid.GetOpaqueId(), uid.GetIdp())
+ if val, err := xattr.Get(nodePath, fa); err == nil {
+ sublog.Debug().
+ Str("favorite", fa).
+ Msg("found favorite flag")
+ favorite = string(val)
}
+ } else {
+ sublog.Error().Err(errtypes.UserRequired("userrequired")).Msg("user has no id")
}
+ } else {
+ sublog.Error().Err(errtypes.UserRequired("userrequired")).Msg("error getting user from ctx")
}
- } else {
- sublog.Error().Err(err).Msg("could not list attributes")
+ metadata[_favoriteKey] = favorite
}
- if common.FindString(mdKeys, _shareTypesKey) != -1 {
+ // share indicator
+ if _, ok := mdKeysMap[_shareTypesKey]; returnAllKeys || ok {
if n.hasUserShares(ctx) {
- ri.ArbitraryMetadata.Metadata[_shareTypesKey] = _userShareType
+ metadata[_shareTypesKey] = _userShareType
+ }
+ }
+
+ // only read the requested metadata attributes
+ list, err := xattr.List(nodePath)
+ if err != nil {
+ sublog.Error().Err(err).Msg("error getting list of extended attributes")
+ } else {
+ for _, entry := range list {
+ // filter out non-custom properties
+ if !strings.HasPrefix(entry, metadataPrefix) {
+ continue
+ }
+ // only read when key was requested
+ k := entry[len(metadataPrefix):]
+ if _, ok := mdKeysMap[k]; returnAllKeys || ok {
+ if val, err := xattr.Get(nodePath, entry); err == nil {
+ metadata[k] = string(val)
+ } else {
+ sublog.Error().Err(err).
+ Str("entry", entry).
+ Msg("error retrieving xattr metadata")
+ }
+ }
+
}
}
+ ri.ArbitraryMetadata = &provider.ArbitraryMetadata{
+ Metadata: metadata,
+ }
sublog.Debug().
Interface("ri", ri).
diff --git a/pkg/storage/fs/ocis/ocis.go b/pkg/storage/fs/ocis/ocis.go
index 1c3e211511..e1f8e7fd79 100644
--- a/pkg/storage/fs/ocis/ocis.go
+++ b/pkg/storage/fs/ocis/ocis.go
@@ -62,8 +62,9 @@ const (
// grantPrefix is the prefix for sharing related extended attributes
grantPrefix string = ocisPrefix + "grant."
metadataPrefix string = ocisPrefix + "md."
- // TODO implement favorites metadata flag
- //favPrefix string = ocisPrefix + "fav." // favorite flag, per user
+
+ // favorite flag, per user
+ favPrefix string = ocisPrefix + "fav."
// a temporary etag for a folder that is removed when the mtime propagation happens
tmpEtagAttr string = ocisPrefix + "tmp.etag"
diff --git a/tests/acceptance/expected-failures-on-OCIS-storage.txt b/tests/acceptance/expected-failures-on-OCIS-storage.txt
index 78a23b2fa5..c130ad94b7 100644
--- a/tests/acceptance/expected-failures-on-OCIS-storage.txt
+++ b/tests/acceptance/expected-failures-on-OCIS-storage.txt
@@ -115,6 +115,7 @@ apiAuthWebDav/webDavPUTAuth.feature:38
apiCapabilities/capabilitiesWithNormalUser.feature:11
#
# https://github.com/owncloud/ocis-reva/issues/39 REPORT request not implemented
+# https://github.com/cs3org/reva/issues/1394 ocis needs an api to list all files of a user he marked as favorite or that are tagged with a certain tag
# And other missing implementation of favorites
#
apiFavorites/favorites.feature:91
@@ -1087,8 +1088,6 @@ apiWebdavProperties2/getFileProperties.feature:71
#
# https://github.com/owncloud/ocis/issues/567 cannot get share-types webdav property
#
-apiWebdavProperties2/getFileProperties.feature:135
-apiWebdavProperties2/getFileProperties.feature:136
apiWebdavProperties2/getFileProperties.feature:174
apiWebdavProperties2/getFileProperties.feature:175
#
diff --git a/tests/acceptance/expected-failures-on-OWNCLOUD-storage.txt b/tests/acceptance/expected-failures-on-OWNCLOUD-storage.txt
index 9173ef7b2c..49fd19672c 100644
--- a/tests/acceptance/expected-failures-on-OWNCLOUD-storage.txt
+++ b/tests/acceptance/expected-failures-on-OWNCLOUD-storage.txt
@@ -115,6 +115,7 @@ apiAuthWebDav/webDavPUTAuth.feature:38
apiCapabilities/capabilitiesWithNormalUser.feature:11
#
# https://github.com/owncloud/ocis-reva/issues/39 REPORT request not implemented
+# https://github.com/cs3org/reva/issues/1394 ocis needs an api to list all files of a user he marked as favorite or that are tagged with a certain tag
# And other missing implementation of favorites
#
apiFavorites/favorites.feature:91