diff --git a/changelog/unreleased/ocis-quota.md b/changelog/unreleased/ocis-quota.md new file mode 100644 index 0000000000..0d58ad70c8 --- /dev/null +++ b/changelog/unreleased/ocis-quota.md @@ -0,0 +1,5 @@ +Enhancement: quota querying and tree accounting + +The ocs api now returns the user quota for the users home storage. Furthermore, the ocis storage driver now reads the quota from the extended attributes of the user home or root node and implements tree size accounting. Finally, ocdav PROPFINDS now handle the `DAV:quota-used-bytes` and `DAV:quote-available-bytes` properties. + +https://github.com/cs3org/reva/pull/1405 \ No newline at end of file diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index bb52f48c6b..dc565ecfed 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -1762,9 +1762,20 @@ func (s *svc) PurgeRecycle(ctx context.Context, req *gateway.PurgeRecycleRequest return res, nil } -func (s *svc) GetQuota(ctx context.Context, _ *gateway.GetQuotaRequest) (*provider.GetQuotaResponse, error) { - res := &provider.GetQuotaResponse{ - Status: status.NewUnimplemented(ctx, nil, "GetQuota not yet implemented"), +func (s *svc) GetQuota(ctx context.Context, req *gateway.GetQuotaRequest) (*provider.GetQuotaResponse, error) { + c, err := s.find(ctx, req.Ref) + if err != nil { + return &provider.GetQuotaResponse{ + Status: status.NewStatusFromErrType(ctx, "GetQuota ref="+req.Ref.String(), err), + }, nil + } + + res, err := c.GetQuota(ctx, &provider.GetQuotaRequest{ + Opaque: req.GetOpaque(), + //Ref: req.GetRef(), // TODO send which storage space ... or root + }) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling GetQuota") } return res, nil } diff --git a/internal/grpc/services/storageprovider/storageprovider.go b/internal/grpc/services/storageprovider/storageprovider.go index ae950d49c4..4e64793047 100644 --- a/internal/grpc/services/storageprovider/storageprovider.go +++ b/internal/grpc/services/storageprovider/storageprovider.go @@ -1062,8 +1062,8 @@ func (s *service) GetQuota(ctx context.Context, req *provider.GetQuotaRequest) ( res := &provider.GetQuotaResponse{ Status: status.NewOK(ctx), - TotalBytes: uint64(total), - UsedBytes: uint64(used), + TotalBytes: total, + UsedBytes: used, } return res, nil } diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go index cfc9f3293d..98b815c7a2 100644 --- a/internal/http/services/owncloud/ocdav/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind.go @@ -52,6 +52,10 @@ const ( // RFC1123 time that mimics oc10. time.RFC1123 would end in "UTC", see https://github.com/golang/go/issues/13781 RFC1123 = "Mon, 02 Jan 2006 15:04:05 GMT" + + //_propQuotaUncalculated = "-1" + _propQuotaUnknown = "-2" + //_propQuotaUnlimited = "-3" ) // ns is the namespace that is prefixed to the path in the cs3 namespace @@ -89,22 +93,6 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string) return } - ref := &provider.Reference{ - Spec: &provider.Reference_Path{Path: fn}, - } - req := &provider.StatRequest{Ref: ref} - res, err := client.Stat(ctx, req) - if err != nil { - sublog.Error().Err(err).Msgf("error sending a grpc stat request to ref: %v", ref) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if res.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, res.Status) - return - } - metadataKeys := []string{} if pf.Allprop != nil { // TODO this changes the behavior and returns all properties if allprops has been set, @@ -120,6 +108,24 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string) } } } + ref := &provider.Reference{ + Spec: &provider.Reference_Path{Path: fn}, + } + req := &provider.StatRequest{ + Ref: ref, + ArbitraryMetadataKeys: metadataKeys, + } + res, err := client.Stat(ctx, req) + if err != nil { + sublog.Error().Err(err).Interface("req", req).Msg("error sending a grpc stat request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if res.Status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, res.Status) + return + } info := res.Info infos := []*provider.ResourceInfo{info} @@ -217,10 +223,17 @@ 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 + switch n.Local { + case "quota-available-bytes", "quota-used-bytes": + // A PROPFIND request SHOULD NOT return DAV:quota-available-bytes and DAV:quota-used-bytes + // from https://www.rfc-editor.org/rfc/rfc4331.html#section-2 + return true + default: + return false + } case _nsOwncloud: switch n.Local { - case "favorite", "share-types", "checksums": + case "favorite", "share-types", "checksums", "size": return true default: return false @@ -334,11 +347,24 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide } var ls *link.PublicShare - if md.Opaque != nil && md.Opaque.Map != nil && md.Opaque.Map["link-share"] != nil && md.Opaque.Map["link-share"].Decoder == "json" { - ls = &link.PublicShare{} - err := json.Unmarshal(md.Opaque.Map["link-share"].Value, ls) - if err != nil { - sublog.Error().Err(err).Msg("could not unmarshal link json") + + // -1 indicates uncalculated + // -2 indicates unknown (default) + // -3 indicates unlimited + quota := _propQuotaUnknown + size := fmt.Sprintf("%d", md.Size) + // TODO refactor helper functions: GetOpaqueJSONEncoded(opaque, key string, *struct) err, GetOpaquePlainEncoded(opaque, key) value, err + // or use ok like pattern and return bool? + if md.Opaque != nil && md.Opaque.Map != nil { + if md.Opaque.Map["link-share"] != nil && md.Opaque.Map["link-share"].Decoder == "json" { + ls = &link.PublicShare{} + err := json.Unmarshal(md.Opaque.Map["link-share"].Value, ls) + if err != nil { + sublog.Error().Err(err).Msg("could not unmarshal link json") + } + } + if md.Opaque.Map["quota"] != nil && md.Opaque.Map["quota"].Decoder == "plain" { + quota = string(md.Opaque.Map["quota"].Value) } } @@ -389,12 +415,15 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide } // always return size, well nearly always ... public link shares are a little weird - size := fmt.Sprintf("%d", md.Size) if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { propstatOK.Prop = append(propstatOK.Prop, s.newPropRaw("d:resourcetype", "")) if ls == nil { propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:size", size)) } + // A PROPFIND request SHOULD NOT return DAV:quota-available-bytes and DAV:quota-used-bytes + // from https://www.rfc-editor.org/rfc/rfc4331.html#section-2 + //propstatOK.Prop = append(propstatOK.Prop, s.newProp("d:quota-used-bytes", size)) + //propstatOK.Prop = append(propstatOK.Prop, s.newProp("d:quota-available-bytes", quota)) } else { propstatOK.Prop = append(propstatOK.Prop, s.newProp("d:resourcetype", ""), @@ -458,7 +487,6 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide // TODO return other properties ... but how do we put them in a namespace? } else { // otherwise return only the requested properties - size := fmt.Sprintf("%d", md.Size) for i := range pf.Prop { switch pf.Prop[i].Space { case _nsOwncloud: @@ -539,7 +567,8 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:public-link-expiration", "")) case "size": // phoenix only // TODO we cannot find out if md.Size is set or not because ints in go default to 0 - // oc:size is also available on folders + // TODO what is the difference to d:quota-used-bytes (which only exists for collections)? + // oc:size is available on files and folders and behaves like d:getcontentlength or d:quota-used-bytes respectively if ls == nil { propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:size", size)) } else { @@ -690,6 +719,22 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide } else { propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("d:getlastmodified", "")) } + case "quota-used-bytes": // RFC 4331 + if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + // always returns the current usage, + // in oc10 there seems to be a bug that makes the size in webdav differ from the one in the user properties, not taking shares into account + // in ocis we plan to always mak the quota a property of the storage space + propstatOK.Prop = append(propstatOK.Prop, s.newProp("d:quota-used-bytes", size)) + } else { + propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("d:quota-used-bytes", "")) + } + case "quota-available-bytes": // RFC 4331 + if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + // oc10 returns -3 for unlimited, -2 for unknown, -1 for uncalculated + propstatOK.Prop = append(propstatOK.Prop, s.newProp("d:quota-available-bytes", quota)) + } else { + propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("d:quota-available-bytes", "")) + } default: propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("d:"+pf.Prop[i].Local, "")) } @@ -763,7 +808,12 @@ func (c *countingReader) Read(p []byte) (int, error) { } func metadataKeyOf(n *xml.Name) string { - return fmt.Sprintf("%s/%s", n.Space, n.Local) + switch { + case n.Space == _nsDav && n.Local == "quota-available-bytes": + return "quota" + default: + return fmt.Sprintf("%s/%s", n.Space, n.Local) + } } // http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for propfind) diff --git a/internal/http/services/owncloud/ocs/handlers/cloud/cloud.go b/internal/http/services/owncloud/ocs/handlers/cloud/cloud.go index bb2364f1e3..8b099b7b0c 100644 --- a/internal/http/services/owncloud/ocs/handlers/cloud/cloud.go +++ b/internal/http/services/owncloud/ocs/handlers/cloud/cloud.go @@ -37,11 +37,12 @@ type Handler struct { } // Init initializes this and any contained handlers -func (h *Handler) Init(c *config.Config) { +func (h *Handler) Init(c *config.Config) error { h.UserHandler = new(user.Handler) - h.UsersHandler = new(users.Handler) h.CapabilitiesHandler = new(capabilities.Handler) h.CapabilitiesHandler.Init(c) + h.UsersHandler = new(users.Handler) + return h.UsersHandler.Init(c) } // Handler routes the cloud endpoints diff --git a/internal/http/services/owncloud/ocs/handlers/cloud/users/users.go b/internal/http/services/owncloud/ocs/handlers/cloud/users/users.go index fcf4b638c7..9922110415 100644 --- a/internal/http/services/owncloud/ocs/handlers/cloud/users/users.go +++ b/internal/http/services/owncloud/ocs/handlers/cloud/users/users.go @@ -22,13 +22,28 @@ import ( "fmt" "net/http" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/internal/http/services/owncloud/ocdav" + "github.com/cs3org/reva/internal/http/services/owncloud/ocs/config" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" + "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/rhttp/router" ctxuser "github.com/cs3org/reva/pkg/user" ) -// The UsersHandler renders user data for the user id given in the url path +// Handler renders user data for the user id given in the url path type Handler struct { + gatewayAddr string +} + +// Init initializes this and any contained handlers +func (h *Handler) Init(c *config.Config) error { + h.gatewayAddr = c.GatewaySvc + return nil } func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -44,7 +59,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } if user != u.Username { - // FIXME allow fetching other users info? + // FIXME allow fetching other users info? only for admins response.WriteOCSError(w, r, http.StatusForbidden, "user id mismatch", fmt.Errorf("%s tried to access %s user info endpoint", u.Id.OpaqueId, user)) return } @@ -53,19 +68,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { head, r.URL.Path = router.ShiftPath(r.URL.Path) switch head { case "": - response.WriteOCSSuccess(w, r, &Users{ - // FIXME query storages? cache a summary? - // TODO use list of storages to allow clients to resolve quota status - Quota: &Quota{ - Free: 2840756224000, - Used: 5059416668, - Total: 2845815640668, - Relative: 0.18, - Definition: "default", - }, - DisplayName: u.DisplayName, - Email: u.Mail, - }) + h.handleUsers(w, r, u) return case "groups": response.WriteOCSSuccess(w, r, &Groups{}) @@ -100,3 +103,59 @@ type Users struct { type Groups struct { Groups []string `json:"groups" xml:"groups>element"` } + +func (h *Handler) handleUsers(w http.ResponseWriter, r *http.Request, u *userpb.User) { + ctx := r.Context() + sublog := appctx.GetLogger(r.Context()) + + gc, err := pool.GetGatewayServiceClient(h.gatewayAddr) + if err != nil { + sublog.Error().Err(err).Msg("error getting gateway client") + w.WriteHeader(http.StatusInternalServerError) + return + } + + getHomeRes, err := gc.GetHome(ctx, &provider.GetHomeRequest{}) + if err != nil { + sublog.Error().Err(err).Msg("error calling GetHome") + w.WriteHeader(http.StatusInternalServerError) + return + } + if getHomeRes.Status.Code != rpc.Code_CODE_OK { + ocdav.HandleErrorStatus(sublog, w, getHomeRes.Status) + return + } + + getQuotaRes, err := gc.GetQuota(ctx, &gateway.GetQuotaRequest{ + Ref: &provider.Reference{ + Spec: &provider.Reference_Path{ + Path: getHomeRes.Path, + }, + }, + }) + if err != nil { + sublog.Error().Err(err).Msg("error calling GetQuota") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if getQuotaRes.Status.Code != rpc.Code_CODE_OK { + ocdav.HandleErrorStatus(sublog, w, getQuotaRes.Status) + return + } + + response.WriteOCSSuccess(w, r, &Users{ + // ocs can only return the home storage quota + Quota: &Quota{ + Free: int64(getQuotaRes.TotalBytes - getQuotaRes.UsedBytes), + Used: int64(getQuotaRes.UsedBytes), + // TODO support negative values or flags for the quota to carry special meaning: -1 = uncalculated, -2 = unknown, -3 = unlimited + // for now we can only report total and used + Total: int64(getQuotaRes.TotalBytes), + Relative: float32(float64(getQuotaRes.UsedBytes) / float64(getQuotaRes.TotalBytes)), + Definition: "default", + }, + DisplayName: u.DisplayName, + Email: u.Mail, + }) +} diff --git a/internal/http/services/owncloud/ocs/v1.go b/internal/http/services/owncloud/ocs/v1.go index 635ab74de1..f22f4d5392 100644 --- a/internal/http/services/owncloud/ocs/v1.go +++ b/internal/http/services/owncloud/ocs/v1.go @@ -37,15 +37,14 @@ type V1Handler struct { } func (h *V1Handler) init(c *config.Config) error { + h.ConfigHandler = new(configHandler.Handler) + h.ConfigHandler.Init(c) h.AppsHandler = new(apps.Handler) if err := h.AppsHandler.Init(c); err != nil { return err } h.CloudHandler = new(cloud.Handler) - h.CloudHandler.Init(c) - h.ConfigHandler = new(configHandler.Handler) - h.ConfigHandler.Init(c) - return nil + return h.CloudHandler.Init(c) } // Handler handles requests diff --git a/pkg/eosclient/eosbinary/eosbinary.go b/pkg/eosclient/eosbinary/eosbinary.go index b49ce42606..17679a21f9 100644 --- a/pkg/eosclient/eosbinary/eosbinary.go +++ b/pkg/eosclient/eosbinary/eosbinary.go @@ -794,19 +794,19 @@ func (c *Client) parseQuota(path, raw string) (*eosclient.QuotaInfo, error) { if strings.HasPrefix(path, space) { maxBytesString := m["maxlogicalbytes"] usedBytesString := m["usedlogicalbytes"] - maxBytes, _ := strconv.ParseInt(maxBytesString, 10, 64) - usedBytes, _ := strconv.ParseInt(usedBytesString, 10, 64) + maxBytes, _ := strconv.ParseUint(maxBytesString, 10, 64) + usedBytes, _ := strconv.ParseUint(usedBytesString, 10, 64) maxInodesString := m["maxfiles"] usedInodesString := m["usedfiles"] - maxInodes, _ := strconv.ParseInt(maxInodesString, 10, 64) - usedInodes, _ := strconv.ParseInt(usedInodesString, 10, 64) + maxInodes, _ := strconv.ParseUint(maxInodesString, 10, 64) + usedInodes, _ := strconv.ParseUint(usedInodesString, 10, 64) qi := &eosclient.QuotaInfo{ - AvailableBytes: int(maxBytes), - UsedBytes: int(usedBytes), - AvailableInodes: int(maxInodes), - UsedInodes: int(usedInodes), + AvailableBytes: maxBytes, + UsedBytes: usedBytes, + AvailableInodes: maxInodes, + UsedInodes: usedInodes, } return qi, nil } diff --git a/pkg/eosclient/eosclient.go b/pkg/eosclient/eosclient.go index 502630260d..1425ed5ac7 100644 --- a/pkg/eosclient/eosclient.go +++ b/pkg/eosclient/eosclient.go @@ -96,9 +96,10 @@ type DeletedEntry struct { } // QuotaInfo reports the available bytes and inodes for a particular user. +// eos reports all quota values are unsigned long, see https://github.com/cern-eos/eos/blob/93515df8c0d5a858982853d960bec98f983c1285/mgm/Quota.hh#L135 type QuotaInfo struct { - AvailableBytes, UsedBytes int - AvailableInodes, UsedInodes int + AvailableBytes, UsedBytes uint64 + AvailableInodes, UsedInodes uint64 } // SetQuotaInfo encapsulates the information needed to diff --git a/pkg/storage/fs/ocis/node.go b/pkg/storage/fs/ocis/node.go index f79a07e0ed..6e7d2854bd 100644 --- a/pkg/storage/fs/ocis/node.go +++ b/pkg/storage/fs/ocis/node.go @@ -27,6 +27,7 @@ import ( "io" "os" "path/filepath" + "strconv" "strings" "time" @@ -50,6 +51,11 @@ const ( _favoriteKey = "http://owncloud.org/ns/favorite" _checksumsKey = "http://owncloud.org/ns/checksums" + _quotaKey = "quota" + + _quotaUncalculated = "-1" + _quotaUnknown = "-2" + _quotaUnlimited = "-3" ) // Node represents a node in the tree and provides methods to get a Parent or Child instance @@ -470,6 +476,15 @@ func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissi Target: string(target), PermissionSet: rp, } + if nodeType == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + ts, err := n.GetTreeSize() + if err == nil { + ri.Size = ts + } else { + ri.Size = 0 // make dirs always return 0 if it is unknown + sublog.Debug().Err(err).Msg("could not read treesize") + } + } if ri.Owner, err = n.Owner(); err != nil { sublog.Debug().Err(err).Msg("could not determine owner") @@ -547,6 +562,26 @@ func (n *Node) AsResourceInfo(ctx context.Context, rp *provider.ResourcePermissi readChecksumIntoOpaque(ctx, nodePath, storageprovider.XSAdler32, ri) } + // quota + if _, ok := mdKeysMap[_quotaKey]; (nodeType == provider.ResourceType_RESOURCE_TYPE_CONTAINER) && returnAllKeys || ok { + var quotaPath string + if n.lu.Options.EnableHome { + if r, err := n.lu.HomeNode(ctx); err == nil { + quotaPath = n.lu.toInternalPath(r.ID) + readQuotaIntoOpaque(ctx, quotaPath, ri) + } else { + sublog.Error().Err(err).Msg("error determining home node for quota") + } + } else { + if r, err := n.lu.RootNode(ctx); err == nil { + quotaPath = n.lu.toInternalPath(r.ID) + readQuotaIntoOpaque(ctx, quotaPath, ri) + } else { + sublog.Error().Err(err).Msg("error determining root node for quota") + } + } + } + // only read the requested metadata attributes attrs, err := xattr.List(nodePath) if err != nil { @@ -620,6 +655,85 @@ func readChecksumIntoOpaque(ctx context.Context, nodePath, algo string, ri *prov } } +// quota is always stored on the root node +func readQuotaIntoOpaque(ctx context.Context, nodePath string, ri *provider.ResourceInfo) { + v, err := xattr.Get(nodePath, quotaAttr) + switch { + case err == nil: + // make sure we have a proper signed int + // we use the same magic numbers to indicate: + // -1 = uncalculated + // -2 = unknown + // -3 = unlimited + if _, err := strconv.ParseInt(string(v), 10, 64); err == nil { + if ri.Opaque == nil { + ri.Opaque = &types.Opaque{ + Map: map[string]*types.OpaqueEntry{}, + } + } + ri.Opaque.Map[_quotaKey] = &types.OpaqueEntry{ + Decoder: "plain", + Value: v, + } + } else { + appctx.GetLogger(ctx).Error().Err(err).Str("nodepath", nodePath).Str("quota", string(v)).Msg("malformed quota") + } + case isNoData(err): + appctx.GetLogger(ctx).Debug().Err(err).Str("nodepath", nodePath).Msg("quota not set") + case isNotFound(err): + appctx.GetLogger(ctx).Error().Err(err).Str("nodepath", nodePath).Msg("file not found when reading quota") + default: + appctx.GetLogger(ctx).Error().Err(err).Str("nodepath", nodePath).Msg("could not read quota") + } +} + +// CalculateTreeSize will sum up the size of all children of a node +func (n *Node) CalculateTreeSize(ctx context.Context) (uint64, error) { + var size uint64 + // TODO check if this is a dir? + nodePath := n.lu.toInternalPath(n.ID) + + f, err := os.Open(nodePath) + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Str("nodepath", nodePath).Msg("could not open dir") + return 0, err + } + defer f.Close() + + names, err := f.Readdirnames(0) + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Str("nodepath", nodePath).Msg("could not read dirnames") + return 0, err + } + for i := range names { + cPath := filepath.Join(nodePath, names[i]) + info, err := os.Stat(cPath) + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Str("childpath", cPath).Msg("could not stat child entry") + continue // continue after an error + } + if !info.IsDir() { + size += uint64(info.Size()) + } else { + // read from attr + var b []byte + // xattr.Get will follow the symlink + if b, err = xattr.Get(cPath, treesizeAttr); err != nil { + // TODO recursively descend and recalculate treesize + continue // continue after an error + } + csize, err := strconv.ParseUint(string(b), 10, 64) + if err != nil { + // TODO recursively descend and recalculate treesize + continue // continue after an error + } + size += csize + } + } + return size, err + +} + // HasPropagation checks if the propagation attribute exists and is set to "1" func (n *Node) HasPropagation() (propagation bool) { if b, err := xattr.Get(n.lu.toInternalPath(n.ID), propagationAttr); err == nil { @@ -642,6 +756,20 @@ func (n *Node) SetTMTime(t time.Time) (err error) { return xattr.Set(n.lu.toInternalPath(n.ID), treeMTimeAttr, []byte(t.UTC().Format(time.RFC3339Nano))) } +// GetTreeSize reads the treesize from the extended attributes +func (n *Node) GetTreeSize() (treesize uint64, err error) { + var b []byte + if b, err = xattr.Get(n.lu.toInternalPath(n.ID), treesizeAttr); err != nil { + return + } + return strconv.ParseUint(string(b), 10, 64) +} + +// SetTreeSize writes the treesize to the extended attributes +func (n *Node) SetTreeSize(ts uint64) (err error) { + return xattr.Set(n.lu.toInternalPath(n.ID), treesizeAttr, []byte(strconv.FormatUint(ts, 10))) +} + // SetChecksum writes the checksum with the given checksum type to the extended attributes func (n *Node) SetChecksum(csType string, h hash.Hash) (err error) { return xattr.Set(n.lu.toInternalPath(n.ID), checksumPrefix+csType, h.Sum(nil)) diff --git a/pkg/storage/fs/ocis/ocis.go b/pkg/storage/fs/ocis/ocis.go index b3024b51ca..ff643cf94b 100644 --- a/pkg/storage/fs/ocis/ocis.go +++ b/pkg/storage/fs/ocis/ocis.go @@ -24,7 +24,9 @@ import ( "net/url" "os" "path/filepath" + "strconv" "strings" + "syscall" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" @@ -84,7 +86,11 @@ const ( // 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" + // stored as uint64, little endian + treesizeAttr string = ocisPrefix + "treesize" + + // the quota for the storage space / tree, regardless who accesses it + quotaAttr string = ocisPrefix + "quota" ) func init() { @@ -183,8 +189,56 @@ func (fs *ocisfs) Shutdown(ctx context.Context) error { return nil } -func (fs *ocisfs) GetQuota(ctx context.Context) (int, int, error) { - return 0, 0, nil +// TODO Document in the cs3 should we return quota or free space? +func (fs *ocisfs) GetQuota(ctx context.Context) (uint64, uint64, error) { + var node *Node + var err error + if node, err = fs.lu.HomeOrRootNode(ctx); err != nil { + return 0, 0, err + } + + if !node.Exists { + err = errtypes.NotFound(filepath.Join(node.ParentID, node.Name)) + return 0, 0, err + } + + rp, err := fs.p.AssemblePermissions(ctx, node) + switch { + case err != nil: + return 0, 0, errtypes.InternalError(err.Error()) + case !rp.GetQuota: + return 0, 0, errtypes.PermissionDenied(node.ID) + } + + ri, err := node.AsResourceInfo(ctx, rp, []string{"treesize", "quota"}) + if err != nil { + return 0, 0, err + } + + quotaStr := _quotaUnknown + if ri.Opaque != nil && ri.Opaque.Map != nil && ri.Opaque.Map["quota"] != nil && ri.Opaque.Map["quota"].Decoder == "plain" { + quotaStr = string(ri.Opaque.Map["quota"].Value) + } + stat := syscall.Statfs_t{} + err = syscall.Statfs(fs.lu.toInternalPath(node.ID), &stat) + if err != nil { + return 0, 0, err + } + + total := ri.Size + (stat.Bavail * uint64(stat.Bsize)) // used treesize + available space + + switch { + case quotaStr == _quotaUncalculated, quotaStr == _quotaUnknown, quotaStr == _quotaUnlimited: + // best we can do is return current total + // TODO indicate unlimited total? -> in opaque data? + default: + if quota, err := strconv.ParseUint(quotaStr, 10, 64); err == nil { + if total > quota { + total = quota + } + } + } + return total, ri.Size, nil } // CreateHome creates a new root node that has no parent id diff --git a/pkg/storage/fs/ocis/tree.go b/pkg/storage/fs/ocis/tree.go index 742304dcba..0fed417286 100644 --- a/pkg/storage/fs/ocis/tree.go +++ b/pkg/storage/fs/ocis/tree.go @@ -31,7 +31,6 @@ import ( "github.com/google/uuid" "github.com/pkg/errors" "github.com/pkg/xattr" - "github.com/rs/zerolog/log" ) // Tree manages a hierarchical tree @@ -301,12 +300,12 @@ func (t *Tree) Delete(ctx context.Context, n *Node) (err error) { // Propagate propagates changes to the root of the tree func (t *Tree) Propagate(ctx context.Context, n *Node) (err error) { + sublog := appctx.GetLogger(ctx).With().Interface("node", n).Logger() if !t.lu.Options.TreeTimeAccounting && !t.lu.Options.TreeSizeAccounting { // no propagation enabled - log.Debug().Msg("propagation disabled") + sublog.Debug().Msg("propagation disabled") return } - log := appctx.GetLogger(ctx) // is propagation enabled for the parent node? @@ -318,16 +317,20 @@ func (t *Tree) Propagate(ctx context.Context, n *Node) (err error) { // use a sync time and don't rely on the mtime of the current node, as the stat might not change when a rename happened too quickly sTime := time.Now().UTC() + // we loop until we reach the root for err == nil && n.ID != root.ID { - log.Debug().Interface("node", n).Msg("propagating") + sublog.Debug().Msg("propagating") + // make n the parent or break the loop if n, err = n.Parent(); err != nil { break } + sublog = sublog.With().Interface("node", n).Logger() + // TODO none, sync and async? if !n.HasPropagation() { - log.Debug().Interface("node", n).Str("attr", propagationAttr).Msg("propagation attribute not set or unreadable, not propagating") + sublog.Debug().Str("attr", propagationAttr).Msg("propagation attribute not set or unreadable, not propagating") // if the attribute is not set treat it as false / none / no propagation return nil } @@ -341,20 +344,16 @@ func (t *Tree) Propagate(ctx context.Context, n *Node) (err error) { switch { case err != nil: // missing attribute, or invalid format, overwrite - log.Debug().Err(err). - Interface("node", n). - Msg("could not read tmtime attribute, overwriting") + sublog.Debug().Err(err).Msg("could not read tmtime attribute, overwriting") updateSyncTime = true case tmTime.Before(sTime): - log.Debug(). - Interface("node", n). + sublog.Debug(). Time("tmtime", tmTime). Time("stime", sTime). Msg("parent tmtime is older than node mtime, updating") updateSyncTime = true default: - log.Debug(). - Interface("node", n). + sublog.Debug(). Time("tmtime", tmTime). Time("stime", sTime). Dur("delta", sTime.Sub(tmTime)). @@ -364,24 +363,60 @@ func (t *Tree) Propagate(ctx context.Context, n *Node) (err error) { if updateSyncTime { // update the tree time of the parent node if err = n.SetTMTime(sTime); err != nil { - log.Error().Err(err).Interface("node", n).Time("tmtime", sTime).Msg("could not update tmtime of parent node") - return + sublog.Error().Err(err).Time("tmtime", sTime).Msg("could not update tmtime of parent node") + } else { + sublog.Debug().Time("tmtime", sTime).Msg("updated tmtime of parent node") } - log.Debug().Interface("node", n).Time("tmtime", sTime).Msg("updated tmtime of parent node") } if err := n.UnsetTempEtag(); err != nil { - log.Error().Err(err).Interface("node", n).Msg("could not remove temporary etag attribute") + sublog.Error().Err(err).Msg("could not remove temporary etag attribute") } } - // TODO size accounting + // size accounting + if t.lu.Options.TreeSizeAccounting { + // update the treesize if it differs from the current size + updateTreeSize := false + + var treeSize, calculatedTreeSize uint64 + calculatedTreeSize, err = n.CalculateTreeSize(ctx) + if err != nil { + continue + } + + treeSize, err = n.GetTreeSize() + switch { + case err != nil: + // missing attribute, or invalid format, overwrite + sublog.Debug().Err(err).Msg("could not read treesize attribute, overwriting") + updateTreeSize = true + case treeSize != calculatedTreeSize: + sublog.Debug(). + Uint64("treesize", treeSize). + Uint64("calculatedTreeSize", calculatedTreeSize). + Msg("parent treesize is different then calculated treesize, updating") + updateTreeSize = true + default: + sublog.Debug(). + Uint64("treesize", treeSize). + Uint64("calculatedTreeSize", calculatedTreeSize). + Msg("parent size matches calculated size, not updating") + } + if updateTreeSize { + // update the tree time of the parent node + if err = n.SetTreeSize(calculatedTreeSize); err != nil { + sublog.Error().Err(err).Uint64("calculatedTreeSize", calculatedTreeSize).Msg("could not update treesize of parent node") + } else { + sublog.Debug().Uint64("calculatedTreeSize", calculatedTreeSize).Msg("updated treesize of parent node") + } + } + } } if err != nil { - log.Error().Err(err).Interface("node", n).Msg("error propagating") - return + sublog.Error().Err(err).Msg("error propagating") } return } diff --git a/pkg/storage/fs/owncloud/owncloud.go b/pkg/storage/fs/owncloud/owncloud.go index 7625b0ca77..19efc2f8a5 100644 --- a/pkg/storage/fs/owncloud/owncloud.go +++ b/pkg/storage/fs/owncloud/owncloud.go @@ -1102,7 +1102,7 @@ func (fs *ocfs) UpdateGrant(ctx context.Context, ref *provider.Reference, g *pro return fs.propagate(ctx, ip) } -func (fs *ocfs) GetQuota(ctx context.Context) (int, int, error) { +func (fs *ocfs) GetQuota(ctx context.Context) (uint64, uint64, error) { return 0, 0, nil } diff --git a/pkg/storage/fs/s3/s3.go b/pkg/storage/fs/s3/s3.go index 356aba8447..7e4f101175 100644 --- a/pkg/storage/fs/s3/s3.go +++ b/pkg/storage/fs/s3/s3.go @@ -262,7 +262,7 @@ func (fs *s3FS) UpdateGrant(ctx context.Context, ref *provider.Reference, g *pro return errtypes.NotSupported("s3: operation not supported") } -func (fs *s3FS) GetQuota(ctx context.Context) (int, int, error) { +func (fs *s3FS) GetQuota(ctx context.Context) (uint64, uint64, error) { return 0, 0, nil } diff --git a/pkg/storage/fs/s3ng/s3ng.go b/pkg/storage/fs/s3ng/s3ng.go index e42bb197fa..108d6a6404 100644 --- a/pkg/storage/fs/s3ng/s3ng.go +++ b/pkg/storage/fs/s3ng/s3ng.go @@ -139,7 +139,7 @@ func (fs *s3ngfs) Shutdown(ctx context.Context) error { return nil } -func (fs *s3ngfs) GetQuota(ctx context.Context) (int, int, error) { +func (fs *s3ngfs) GetQuota(ctx context.Context) (uint64, uint64, error) { return 0, 0, nil } diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 0ed8cf8f53..a986eb4b18 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -51,7 +51,7 @@ type FS interface { RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error UpdateGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error) - GetQuota(ctx context.Context) (int, int, error) + GetQuota(ctx context.Context) (uint64, uint64, error) CreateReference(ctx context.Context, path string, targetURI *url.URL) error Shutdown(ctx context.Context) error SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index 68f9a67786..08f38bd17c 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -756,7 +756,7 @@ func (fs *eosfs) listShareFolderRoot(ctx context.Context, p string) (finfos []*p return finfos, nil } -func (fs *eosfs) GetQuota(ctx context.Context) (int, int, error) { +func (fs *eosfs) GetQuota(ctx context.Context) (uint64, uint64, error) { u, err := getUser(ctx) if err != nil { return 0, 0, errors.Wrap(err, "eos: no user in ctx") diff --git a/pkg/storage/utils/localfs/localfs.go b/pkg/storage/utils/localfs/localfs.go index 6ee2b6a0c6..3b61580788 100644 --- a/pkg/storage/utils/localfs/localfs.go +++ b/pkg/storage/utils/localfs/localfs.go @@ -29,6 +29,7 @@ import ( "path" "strconv" "strings" + "syscall" "time" grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1" @@ -517,8 +518,18 @@ func (fs *localfs) UpdateGrant(ctx context.Context, ref *provider.Reference, g * return fs.AddGrant(ctx, ref, g) } -func (fs *localfs) GetQuota(ctx context.Context) (int, int, error) { - return 0, 0, nil +func (fs *localfs) GetQuota(ctx context.Context) (uint64, uint64, error) { + // TODO quota of which storage space? + // we could use the logged in user, but when a user has access to multiple storages this falls short + // for now return quota of root + stat := syscall.Statfs_t{} + err := syscall.Statfs(fs.conf.Root, &stat) + if err != nil { + return 0, 0, err + } + total := stat.Blocks * uint64(stat.Bsize) // Total data blocks in filesystem + used := (stat.Blocks - stat.Bavail) * uint64(stat.Bsize) // Free blocks available to unprivileged user + return total, used, nil } func (fs *localfs) CreateReference(ctx context.Context, path string, targetURI *url.URL) error { diff --git a/tests/acceptance/expected-failures-on-OCIS-storage.md b/tests/acceptance/expected-failures-on-OCIS-storage.md index d5c23866a4..b068aae637 100644 --- a/tests/acceptance/expected-failures-on-OCIS-storage.md +++ b/tests/acceptance/expected-failures-on-OCIS-storage.md @@ -1,7 +1,7 @@ ## Scenarios from ownCloud10 core API tests that are expected to fail with OCIS storage ### File -Basic file management like up and download, move, copy, properties, trash, versions and chunking. +Basic file management like up and download, move, copy, properties, quota, trash, versions and chunking. #### [Implement Trashbin Feature for ocis storage](https://github.com/owncloud/product/issues/209) @@ -326,9 +326,6 @@ Scenario Outline: try to create a folder with a name of an existing file ### [Different webdav properties from core](https://github.com/owncloud/ocis/issues/1302) - [apiWebdavProperties2/getFileProperties.feature:327](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties2/getFileProperties.feature#L327) - [apiWebdavProperties2/getFileProperties.feature:328](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties2/getFileProperties.feature#L328) -Scenario Outline: Propfind the size of a folder using webdav api `Property "oc:size" found with value "10", expected "#^0$#" or "#^0$#"` -- [apiWebdavProperties2/getFileProperties.feature:376](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties2/getFileProperties.feature#L376) -- [apiWebdavProperties2/getFileProperties.feature:377](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties2/getFileProperties.feature#L377) Scenario Outline: Propfind the permissions on a file using webdav api `Property "oc:permissions" found with value "DNVWR", expected "/RM{0,1}DNVW/"` - [apiWebdavProperties2/getFileProperties.feature:441](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties2/getFileProperties.feature#L441) - [apiWebdavProperties2/getFileProperties.feature:442](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties2/getFileProperties.feature#L442) @@ -1186,6 +1183,7 @@ File and sync features in a shared scenario - [apiSharePublicLink2/uploadToPublicLinkShare.feature:66](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiSharePublicLink2/uploadToPublicLinkShare.feature#L66) #### [Set quota over settings](https://github.com/owncloud/ocis/issues/1290) +_requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ - [apiSharePublicLink2/uploadToPublicLinkShare.feature:148](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiSharePublicLink2/uploadToPublicLinkShare.feature#L148) - [apiSharePublicLink2/uploadToPublicLinkShare.feature:158](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiSharePublicLink2/uploadToPublicLinkShare.feature#L158) - [apiSharePublicLink2/uploadToPublicLinkShare.feature:167](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiSharePublicLink2/uploadToPublicLinkShare.feature#L167) @@ -1393,6 +1391,7 @@ Scenario Outline: delete a folder when there is a default folder for received sh - [apiWebdavProperties1/copyFile.feature:475](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L475) #### [quota query](https://github.com/owncloud/ocis/issues/1313) +_requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ - [apiMain/quota.feature:41](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/quota.feature#L41) Scenario: Uploading a file in received folder having enough quota - [apiMain/quota.feature:54](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/quota.feature#L54) Scenario: Uploading a file in received folder having insufficient quota - [apiMain/quota.feature:68](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/quota.feature#L68) Scenario: Overwriting a file in received folder having enough quota @@ -1425,6 +1424,7 @@ Scenario Outline: Retrieving folder quota when quota is set and a file was recei - [apiWebdavProperties2/getFileProperties.feature:233](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties2/getFileProperties.feature#L233) #### [changing user quota gives ocs status 103 / Cannot set quota](https://github.com/owncloud/product/issues/247) +_requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ - [apiShareOperationsToShares/uploadToShare.feature:162](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareOperationsToShares/uploadToShare.feature#L162) - [apiShareOperationsToShares/uploadToShare.feature:163](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareOperationsToShares/uploadToShare.feature#L163) - [apiShareOperationsToShares/uploadToShare.feature:181](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareOperationsToShares/uploadToShare.feature#L181) @@ -1837,7 +1837,7 @@ User and group management features - [apiProvisioning-v2/editUser.feature:47](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiProvisioning-v2/editUser.feature#L47) #### [quota query](https://github.com/owncloud/ocis/issues/1313) -_getting and setting quota_ +_requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ - [apiMain/quota.feature:9](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/quota.feature#L9) Scenario: Uploading a file as owner having enough quota - [apiMain/quota.feature:16](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/quota.feature#L16) Scenario: Uploading a file as owner having insufficient quota - [apiMain/quota.feature:23](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/quota.feature#L23) Scenario: Overwriting a file as owner having enough quota @@ -1850,6 +1850,7 @@ Scenario Outline: Retrieving folder quota when quota is set - [apiWebdavProperties1/getQuota.feature:28](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/getQuota.feature#L28) #### [changing user quota gives ocs status 103 / Cannot set quota](https://github.com/owncloud/product/issues/247) +_requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ - [apiProvisioning-v1/editUser.feature:56](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiProvisioning-v1/editUser.feature#L56) - [apiProvisioning-v1/editUser.feature:122](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiProvisioning-v1/editUser.feature#L122) - [apiProvisioning-v2/editUser.feature:56](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiProvisioning-v2/editUser.feature#L56) diff --git a/tests/acceptance/expected-failures-on-OWNCLOUD-storage.md b/tests/acceptance/expected-failures-on-OWNCLOUD-storage.md index 23653bd494..a29ec9188f 100644 --- a/tests/acceptance/expected-failures-on-OWNCLOUD-storage.md +++ b/tests/acceptance/expected-failures-on-OWNCLOUD-storage.md @@ -1,7 +1,7 @@ ## Scenarios from ownCloud10 core API tests that are expected to fail with owncloud storage ### File -Basic file management like up and download, move, copy, properties, trash, versions and chunking. +Basic file management like up and download, move, copy, properties, quota, trash, versions and chunking. #### [Implement Trashbin Feature for ocis storage](https://github.com/owncloud/product/issues/209) - [apiWebdavEtagPropagation2/restoreFromTrash.feature:48](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavEtagPropagation2/restoreFromTrash.feature#L48) @@ -1204,6 +1204,7 @@ The following scenarios fail on OWNCLOUD storage but not on OCIS storage: - [apiSharePublicLink2/uploadToPublicLinkShare.feature:66](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiSharePublicLink2/uploadToPublicLinkShare.feature#L66) #### [Set quota over settings](https://github.com/owncloud/ocis/issues/1290) +_requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ - [apiSharePublicLink2/uploadToPublicLinkShare.feature:148](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiSharePublicLink2/uploadToPublicLinkShare.feature#L148) - [apiSharePublicLink2/uploadToPublicLinkShare.feature:158](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiSharePublicLink2/uploadToPublicLinkShare.feature#L158) - [apiSharePublicLink2/uploadToPublicLinkShare.feature:167](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiSharePublicLink2/uploadToPublicLinkShare.feature#L167) @@ -1505,6 +1506,7 @@ Scenario Outline: delete a folder when there is a default folder for received sh - [apiWebdavProperties1/copyFile.feature:475](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/copyFile.feature#L475) #### [quota query](https://github.com/owncloud/ocis/issues/1313) +_requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ - [apiMain/quota.feature:41](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/quota.feature#L41) Scenario: Uploading a file in received folder having enough quota - [apiMain/quota.feature:54](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/quota.feature#L54) Scenario: Uploading a file in received folder having insufficient quota - [apiMain/quota.feature:68](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/quota.feature#L68) Scenario: Overwriting a file in received folder having enough quota @@ -1542,6 +1544,7 @@ The following scenarios fail on OWNCLOUD storage but not on OCIS storage: - [apiWebdavProperties2/getFileProperties.feature:233](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties2/getFileProperties.feature#L233) #### [changing user quota gives ocs status 103 / Cannot set quota](https://github.com/owncloud/product/issues/247) +_requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ - [apiShareOperationsToShares/uploadToShare.feature:162](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareOperationsToShares/uploadToShare.feature#L162) - [apiShareOperationsToShares/uploadToShare.feature:163](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareOperationsToShares/uploadToShare.feature#L163) - [apiShareOperationsToShares/uploadToShare.feature:181](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareOperationsToShares/uploadToShare.feature#L181) @@ -1963,7 +1966,7 @@ special character username not valid - [apiProvisioning-v2/editUser.feature:47](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiProvisioning-v2/editUser.feature#L47) #### [quota query](https://github.com/owncloud/ocis/issues/1313) -_getting and setting quota_ +_requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ - [apiMain/quota.feature:9](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/quota.feature#L9) Scenario: Uploading a file as owner having enough quota - [apiMain/quota.feature:16](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/quota.feature#L16) Scenario: Uploading a file as owner having insufficient quota - [apiMain/quota.feature:23](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiMain/quota.feature#L23) Scenario: Overwriting a file as owner having enough quota @@ -1976,6 +1979,7 @@ _getting and setting quota_ - [apiWebdavProperties1/getQuota.feature:28](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiWebdavProperties1/getQuota.feature#L28) #### [changing user quota gives ocs status 103 / Cannot set quota](https://github.com/owncloud/product/issues/247) +_requires a [CS3 user provisioning api that can update the quota for a user](https://github.com/cs3org/cs3apis/pull/95#issuecomment-772780683)_ - [apiProvisioning-v1/editUser.feature:56](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiProvisioning-v1/editUser.feature#L56) - [apiProvisioning-v1/editUser.feature:122](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiProvisioning-v1/editUser.feature#L122) - [apiProvisioning-v2/editUser.feature:56](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiProvisioning-v2/editUser.feature#L56)