diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go index 5fc6aaf15c..7bd4953848 100644 --- a/internal/http/services/owncloud/ocdav/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind.go @@ -251,8 +251,17 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *storage response.Propstat[0].Prop = append(response.Propstat[0].Prop, s.newProp("oc:checksums", value)) } - // TODO: read favorite via separate call? that would be expensive? I hope it is in the md - response.Propstat[0].Prop = append(response.Propstat[0].Prop, s.newProp("oc:favorite", "0")) + // favorites from arbitrary metadata + if k := md.GetArbitraryMetadata(); k == nil { + 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 { + response.Propstat[0].Prop = append(response.Propstat[0].Prop, s.newProp("oc:favorite", "0")) + } + // TODO return other properties ... but how do we put them in a namespace? } else { // otherwise return only the requested properties propstatOK := propstatXML{ @@ -300,9 +309,18 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *storage propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:owner-id", "")) } case "favorite": // phoenix only - // can be 0 or 1 + // TODO: can be 0 or 1?, in oc10 it is present or not // TODO: read favorite via separate call? that would be expensive? I hope it is in the md - propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:favorite", "0")) + // TODO: this boolean favorite property is so horribly wrong ... either it is presont, or it is not ... unless ... it is possible to have a non binary value ... we need to double check + if k := md.GetArbitraryMetadata(); k == nil { + 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 != "" { + propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:favorite", "1")) + } else { + propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:favorite", "0")) + } case "checksums": // desktop if md.Checksum != nil { // TODO(jfd): the actual value is an abomination like this: diff --git a/internal/http/services/owncloud/ocdav/proppatch.go b/internal/http/services/owncloud/ocdav/proppatch.go index 36b38ce223..4ca163bc16 100644 --- a/internal/http/services/owncloud/ocdav/proppatch.go +++ b/internal/http/services/owncloud/ocdav/proppatch.go @@ -24,32 +24,181 @@ import ( "fmt" "io" "net/http" + "path" + "strings" + "go.opencensus.io/trace" + + rpcpb "github.com/cs3org/go-cs3apis/cs3/rpc" + storageproviderv0alphapb "github.com/cs3org/go-cs3apis/cs3/storageprovider/v0alpha" "github.com/cs3org/reva/pkg/appctx" "github.com/pkg/errors" ) func (s *svc) doProppatch(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "proppatch") + defer span.End() log := appctx.GetLogger(ctx) - //fn := path.Join(ns, r.URL.Path) - _, status, err := readProppatch(r.Body) + fn := path.Join(ns, r.URL.Path) + + pp, status, err := readProppatch(r.Body) if err != nil { log.Error().Err(err).Msg("error reading proppatch") w.WriteHeader(status) return } - _, err = s.getClient() + c, err := s.getClient() if err != nil { log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } - // TODO(jfd): implement properties - w.WriteHeader(http.StatusNotImplemented) + mkeys := []string{} + + pf := &propfindXML{ + Prop: propfindProps{}, + } + rreq := &storageproviderv0alphapb.UnsetArbitraryMetadataRequest{ + Ref: &storageproviderv0alphapb.Reference{ + Spec: &storageproviderv0alphapb.Reference_Path{Path: fn}, + }, + ArbitraryMetadataKeys: []string{}, + } + sreq := &storageproviderv0alphapb.SetArbitraryMetadataRequest{ + Ref: &storageproviderv0alphapb.Reference{ + Spec: &storageproviderv0alphapb.Reference_Path{Path: fn}, + }, + ArbitraryMetadata: &storageproviderv0alphapb.ArbitraryMetadata{ + Metadata: map[string]string{}, + }, + } + for i := range pp { + if len(pp[i].Props) < 1 { + continue + } + for j := range pp[i].Props { + pf.Prop = append(pf.Prop, pp[i].Props[j].XMLName) + // don't use path.Join. It removes the double slash! concatenate with a / + key := fmt.Sprintf("%s/%s", pp[i].Props[j].XMLName.Space, pp[i].Props[j].XMLName.Local) + value := string(pp[i].Props[j].InnerXML) + remove := pp[i].Remove + // boolean flags may be "set" to false as well + if s.isBooleanProperty(key) { + // Make boolean properties either "0" or "1" + value = s.as0or1(value) + if value == "0" { + remove = true + } + } + if remove { + rreq.ArbitraryMetadataKeys = append(rreq.ArbitraryMetadataKeys, key) + } else { + sreq.ArbitraryMetadata.Metadata[key] = value + } + mkeys = append(mkeys, key) + } + // what do we need to unset + if len(rreq.ArbitraryMetadataKeys) > 0 { + res, err := c.UnsetArbitraryMetadata(ctx, rreq) + if err != nil { + log.Error().Err(err).Msg("error sending a grpc UnsetArbitraryMetadata request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if res.Status.Code != rpcpb.Code_CODE_OK { + if res.Status.Code == rpcpb.Code_CODE_NOT_FOUND { + log.Warn().Str("path", fn).Msg("resource not found") + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusInternalServerError) + return + } + } + if len(sreq.ArbitraryMetadata.Metadata) > 0 { + res, err := c.SetArbitraryMetadata(ctx, sreq) + if err != nil { + log.Error().Err(err).Msg("error sending a grpc SetArbitraryMetadata request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if res.Status.Code != rpcpb.Code_CODE_OK { + if res.Status.Code == rpcpb.Code_CODE_NOT_FOUND { + log.Warn().Str("path", fn).Msg("resource not found") + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusInternalServerError) + return + } + } + } + + req := &storageproviderv0alphapb.StatRequest{ + Ref: &storageproviderv0alphapb.Reference{ + Spec: &storageproviderv0alphapb.Reference_Path{Path: fn}, + }, + ArbitraryMetadataKeys: mkeys, + } + res, err := c.Stat(ctx, req) + if err != nil { + log.Error().Err(err).Msg("error sending a grpc stat request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if res.Status.Code != rpcpb.Code_CODE_OK { + if res.Status.Code == rpcpb.Code_CODE_NOT_FOUND { + log.Warn().Str("path", fn).Msg("resource not found") + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusInternalServerError) + return + } + + info := res.Info + infos := []*storageproviderv0alphapb.ResourceInfo{info} + + propRes, err := s.formatPropfind(ctx, pf, infos, ns) + if err != nil { + log.Error().Err(err).Msg("error formatting propfind") + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set("DAV", "1, 3, extended-mkcol") + w.Header().Set("Content-Type", "application/xml; charset=utf-8") + w.WriteHeader(http.StatusMultiStatus) + if _, err := w.Write([]byte(propRes)); err != nil { + log.Err(err).Msg("error writing response") + } +} + +func (s *svc) isBooleanProperty(prop string) bool { + // TODO add other properties we know to be boolean? + return prop == "http://owncloud.org/ns/favorite" +} + +func (s *svc) as0or1(val string) string { + switch strings.TrimSpace(val) { + case "false": + return "0" + case "": + return "0" + case "0": + return "0" + case "no": + return "0" + case "off": + return "0" + } + return "1" } // Proppatch describes a property update instruction as defined in RFC 4918. diff --git a/pkg/storage/fs/owncloud/owncloud.go b/pkg/storage/fs/owncloud/owncloud.go index d09d79bc91..9c8a27a74a 100644 --- a/pkg/storage/fs/owncloud/owncloud.go +++ b/pkg/storage/fs/owncloud/owncloud.go @@ -140,6 +140,7 @@ const ( sharePrefix string = "user.oc.acl." trashOriginPrefix string = "user.oc.o" mdPrefix string = "user.oc.md." // arbitrary metadada + favPrefix string = "user.oc.fav." // favorite flag, per user etagPrefix string = "user.oc.etag." // allow overriding a calculated etag with one from the extended attributes ) @@ -324,7 +325,7 @@ func (fs *ocFS) getRecyclePath(ctx context.Context) (string, error) { err := errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from ctx") return "", err } - return path.Join(fs.c.DataDirectory, u.Username, "files_trashbin/files"), nil + return path.Join(fs.c.DataDirectory, u.GetUsername(), "files_trashbin/files"), nil } func (fs *ocFS) removeNamespace(ctx context.Context, np string) string { @@ -374,6 +375,27 @@ func (fs *ocFS) convertToResourceInfo(ctx context.Context, fi os.FileInfo, np st etag = string(val) } + // TODO how do we tell the storage providen/driver which arbitrary metadata to retrieve? an analogy to webdav allprops or a list of requested properties + 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(np, fa); err == nil { + appctx.GetLogger(ctx).Debug(). + Str("np", np). + Str("favorite", string(val)). + Str("username", u.GetUsername()). + Msg("found favorite flag") + favorite = string(val) + } + } else { + appctx.GetLogger(ctx).Error().Err(errtypes.UserRequired("userrequired")).Msg("user has no id") + } + } else { + appctx.GetLogger(ctx).Error().Err(errtypes.UserRequired("userrequired")).Msg("error getting user from ctx") + } + return &storageproviderv0alphapb.ResourceInfo{ Id: &storageproviderv0alphapb.ResourceId{OpaqueId: id}, Path: fn, @@ -387,6 +409,11 @@ func (fs *ocFS) convertToResourceInfo(ctx context.Context, fi os.FileInfo, np st Seconds: uint64(fi.ModTime().Unix()), // TODO read nanos from where? Nanos: fi.MTimeNanos, }, + ArbitraryMetadata: &storageproviderv0alphapb.ArbitraryMetadata{ + Metadata: map[string]string{ + "http://owncloud.org/ns/favorite": favorite, + }, + }, } } func getResourceType(isDir bool) storageproviderv0alphapb.ResourceType { @@ -874,26 +901,25 @@ func (fs *ocFS) SetArbitraryMetadata(ctx context.Context, ref *storageproviderv0 return errors.Wrap(err, "ocFS: error stating "+np) } - var mtime time.Time - var mtimeErr, etagErr, mdErr error + errs := []error{} if md.Metadata != nil { if val, ok := md.Metadata["mtime"]; ok { - if mtime, mtimeErr = parseMTime(val); mtimeErr == nil { + if mtime, err := parseMTime(val); err == nil { // updating mtime also updates atime - if mtimeErr = os.Chtimes(np, mtime, mtime); mtimeErr != nil { - log.Error().Err(mtimeErr). + if err := os.Chtimes(np, mtime, mtime); err != nil { + log.Error().Err(err). Str("np", np). Time("mtime", mtime). Msg("could not set mtime") - err = errors.Wrapf(err, "could not set mtime") + errs = append(errs, errors.Wrap(err, "could not set mtime")) } } else { - log.Error().Err(mtimeErr). + log.Error().Err(err). Str("np", np). Str("val", val). Msg("could not parse mtime") - err = errors.Wrapf(err, "could not parse mtime") + errs = append(errs, errors.Wrap(err, "could not parse mtime")) } // remove from metadata delete(md.Metadata, "mtime") @@ -911,29 +937,82 @@ func (fs *ocFS) SetArbitraryMetadata(ctx context.Context, ref *storageproviderv0 } else // etag is only valid until the calculated etag changes // TODO(jfd) cleanup in a batch job - if etagErr = xattr.Set(np, etagPrefix+etag, []byte(val)); etagErr != nil { - - log.Error().Err(etagErr). + if err := xattr.Set(np, etagPrefix+etag, []byte(val)); err != nil { + log.Error().Err(err). Str("np", np). Str("calcetag", etag). Str("etag", val). Msg("could not set etag") - err = errors.Wrapf(err, "could not set etag") + errs = append(errs, errors.Wrap(err, "could not set etag")) } delete(md.Metadata, "etag") } + if val, ok := md.Metadata["http://owncloud.org/ns/favorite"]; ok { + // 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 maganed 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 + 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.Set(np, fa, []byte(val)); err != nil { + log.Error().Err(err). + Str("np", np). + Interface("user", u). + Str("key", fa). + Msg("could not set favorite flag") + errs = append(errs, errors.Wrap(err, "could not set favorite flag")) + } + } else { + log.Error(). + Str("np", np). + Interface("user", u). + Msg("user has no id") + errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "user has no id")) + } + } else { + log.Error(). + Str("np", np). + Interface("user", u). + Msg("error getting user from ctx") + errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "error getting user from ctx")) + } + // remove from metadata + delete(md.Metadata, "http://owncloud.org/ns/favorite") + } } for k, v := range md.Metadata { - if mdErr = xattr.Set(np, mdPrefix+k, []byte(v)); mdErr != nil { - log.Error().Err(etagErr). + if err := xattr.Set(np, mdPrefix+k, []byte(v)); err != nil { + log.Error().Err(err). Str("np", np). Str("key", k). Str("val", v). Msg("could not set metadata") - err = errors.Wrapf(mdErr, "could not set metadata") + errs = append(errs, errors.Wrap(err, "could not set metadata")) } } - return err + switch len(errs) { + case 0: + return nil + case 1: + return errs[0] + default: + // TODO how to return multiple errors? + return errors.New("multiple errors occurred, see log for details") + } } func parseMTime(v string) (t time.Time, err error) { @@ -963,16 +1042,56 @@ func (fs *ocFS) UnsetArbitraryMetadata(ctx context.Context, ref *storageprovider return errors.Wrap(err, "ocFS: error stating "+np) } + errs := []error{} for _, k := range keys { - if err = xattr.Remove(np, mdPrefix+k); err != nil { - log.Error().Err(err). - Str("np", np). - Str("key", k). - Msg("could not set metadata") - err = errors.Wrapf(err, "could not unset metadata") + switch k { + case "http://owncloud.org/ns/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 err := xattr.Remove(np, fa); err != nil { + log.Error().Err(err). + Str("np", np). + Interface("user", u). + Str("key", fa). + Msg("could not unset favorite flag") + errs = append(errs, errors.Wrap(err, "could not unset favorite flag")) + } + } else { + log.Error(). + Str("np", np). + Interface("user", u). + Msg("user has no id") + errs = append(errs, errors.Wrap(errtypes.UserRequired("userrequired"), "user has no id")) + } + } else { + log.Error(). + Str("np", np). + 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(np, mdPrefix+k); err != nil { + log.Error().Err(err). + Str("np", np). + Str("key", k). + Msg("could not unset metadata") + errs = append(errs, errors.Wrap(err, "could not unset metadata")) + } } } - return err + + switch len(errs) { + case 0: + return nil + case 1: + return errs[0] + default: + // TODO how to return multiple errors? + return errors.New("multiple errors occurred, see log for details") + } } // Delete is actually only a move to trash @@ -1375,7 +1494,7 @@ func (fs *ocFS) RestoreRecycleItem(ctx context.Context, key string) error { } else { origin = path.Clean(string(v)) } - tgt := path.Join(fs.getInternalPath(ctx, path.Join("/", u.Username, origin)), strings.TrimSuffix(path.Base(src), suffix)) + tgt := path.Join(fs.getInternalPath(ctx, path.Join("/", u.GetUsername(), origin)), strings.TrimSuffix(path.Base(src), suffix)) // move back to original location if err := os.Rename(src, tgt); err != nil { log.Error().Err(err).Str("path", src).Msg("could not restore item")