diff --git a/changelog/unreleased/spaces-readme.md b/changelog/unreleased/spaces-readme.md new file mode 100644 index 0000000000..3ae609d032 --- /dev/null +++ b/changelog/unreleased/spaces-readme.md @@ -0,0 +1,6 @@ +Enhancement: support for updating space + +This PR adds support for updating spaces to libregraph, specifically the description and thumbnail of a space. +Additionally, the projects catalogue now directly implements the methods of the spaces registry. + +https://github.com/cs3org/reva/pull/5260 \ No newline at end of file diff --git a/go.mod b/go.mod index 6d0043f916..ce07af715b 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/coreos/go-oidc/v3 v3.14.1 github.com/creasty/defaults v1.8.0 github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e - github.com/cs3org/go-cs3apis v0.0.0-20250626104136-a0b31b323c48 + github.com/cs3org/go-cs3apis v0.0.0-20250811135935-5147e5c98678 github.com/dgraph-io/ristretto v0.2.0 github.com/dolthub/go-mysql-server v0.14.0 github.com/gdexlab/go-render v1.0.1 @@ -42,7 +42,7 @@ require ( github.com/nats-io/nats.go v1.43.0 github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.38.0 - github.com/owncloud/libre-graph-api-go v1.0.5-0.20240425090020-dba6d1507c38 + github.com/owncloud/libre-graph-api-go v1.0.5-0.20250217093259-fa3804be6c27 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.22.0 github.com/rs/cors v1.11.1 diff --git a/go.sum b/go.sum index 33d2ae4a4f..2bbc12ca35 100644 --- a/go.sum +++ b/go.sum @@ -895,8 +895,8 @@ github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYK github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJffz4pz0o1WuQxJ28+5x5JgaHD8= github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4= -github.com/cs3org/go-cs3apis v0.0.0-20250626104136-a0b31b323c48 h1:ia8WkfK+9quVDRZzEXmjV9vmdwVbVyX26uVTIJlJm0o= -github.com/cs3org/go-cs3apis v0.0.0-20250626104136-a0b31b323c48/go.mod h1:DedpcqXl193qF/08Y04IO0PpxyyMu8+GrkD6kWK2MEQ= +github.com/cs3org/go-cs3apis v0.0.0-20250811135935-5147e5c98678 h1:MVTpLBAmZMvDY+5TW1LYCSlitKikwX0/Sor8LSqeCJ4= +github.com/cs3org/go-cs3apis v0.0.0-20250811135935-5147e5c98678/go.mod h1:DedpcqXl193qF/08Y04IO0PpxyyMu8+GrkD6kWK2MEQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -1405,8 +1405,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/owncloud/libre-graph-api-go v1.0.5-0.20240425090020-dba6d1507c38 h1:Ld9bPh0c4y1H22mhiWZBw4AoupWjg8L0WLKX0hfbJho= -github.com/owncloud/libre-graph-api-go v1.0.5-0.20240425090020-dba6d1507c38/go.mod h1:yXI+rmE8yYx+ZsGVrnCpprw/gZMcxjwntnX2y2+VKxY= +github.com/owncloud/libre-graph-api-go v1.0.5-0.20250217093259-fa3804be6c27 h1:ID8s5lGBntmrlI6TbDAjTzRyHucn3bVM2wlW+HBplv4= +github.com/owncloud/libre-graph-api-go v1.0.5-0.20250217093259-fa3804be6c27/go.mod h1:+gT+x62AS9u2Farh9wE2uYmgdvTg0MQgsSI62D+xoRg= github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= diff --git a/internal/grpc/services/spacesregistry/spacesregistry.go b/internal/grpc/services/spacesregistry/spacesregistry.go index fce6ac31f3..d3a523ed65 100644 --- a/internal/grpc/services/spacesregistry/spacesregistry.go +++ b/internal/grpc/services/spacesregistry/spacesregistry.go @@ -181,10 +181,15 @@ func (s *service) listSpacesByType(ctx context.Context, user *userpb.User, space sp = append(sp, space) } } else if spaceType == spaces.SpaceTypeProject { - projects, err := s.projects.ListProjects(ctx, user) + resp, err := s.projects.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{}) if err != nil { return nil, err } + if resp.Status.Code != rpcv1beta1.Code_CODE_OK { + return nil, fmt.Errorf("%s: %s", resp.Status.Code.String(), resp.Status.Message) + } + + projects := resp.StorageSpaces if err := s.decorateProjects(ctx, projects); err != nil { return nil, err } @@ -305,7 +310,7 @@ func (s *service) userSpace(ctx context.Context, user *userpb.User) (*provider.S } func (s *service) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) { - return nil, errors.New("not yet implemented") + return s.projects.UpdateStorageSpace(ctx, req) } func (s *service) DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) (*provider.DeleteStorageSpaceResponse, error) { diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index 754d7b47da..e9f38115b1 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -194,23 +194,41 @@ func (h *DavHandler) Handler(s *svc) http.Handler { r = r.WithContext(ctx) h.TrashbinHandler.Handler(s).ServeHTTP(w, r) default: - // path is of type: space_id/relative/path/from/space - // the space_id is the base64 encode of the path where - // the space is located + // there are two possible types of path + // 1) path is of type: space_id/relative/path/from/space + // the space_id is the base64 encode of the path where + // the space is located + // 2) path is of type: resource_id/relative/path + // here, resource_id is the encoded resource id of a folder + // i.e. in the form of storage$space_id!inode + // and the relative path is relative to this folder _, base, ok := spaces.DecodeStorageSpaceID(head) - if !ok { - w.WriteHeader(http.StatusBadRequest) - return + if ok { + // this is case (1) + ctx = context.WithValue(ctx, ctxSpaceID, head) + fullPath := filepath.Join(base, r.URL.Path) + // like this, we can use the existing DAV handler + // we replace the space id with it's actual path in the URL + r.URL.Path = fullPath + + // We support doing a PUT and a PROPFIND on a resource ID (eg eos$space!inode/file.txt) + } else if r.Method == http.MethodPut || r.Method == MethodPropfind { + // If it's not a space ID, we try to parse it as a resource ID, i.e. case (2) + var storageId, itemId string + storageId, base, itemId, ok = spaces.DecodeResourceID(head) + if !ok { + w.WriteHeader(http.StatusBadRequest) + return + } + + spaceId := spaces.EncodeSpaceID(base) + ctx = context.WithValue(ctx, ctxSpaceID, spaceId) + ctx = context.WithValue(ctx, ctxStorageId, storageId) + ctx = context.WithValue(ctx, ctxResourceOpaqueId, itemId) } - fullPath := filepath.Join(base, r.URL.Path) - r.URL.Path = fullPath - - ctx = context.WithValue(ctx, ctxSpaceID, head) - ctx = context.WithValue(ctx, ctxSpaceFullPath, fullPath) ctx = context.WithValue(ctx, ctxSpacePath, base) - ctx = context.WithValue(ctx, ctxSpaceRelativePath, r.URL.Path) r = r.WithContext(ctx) h.SpacesHandler.Handler(s).ServeHTTP(w, r) } diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index 0344bd9a32..f560a9e2f2 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -53,10 +53,10 @@ const ( ctxKeyBaseURI ctxKey = iota ctxSpaceID ctxSpacePath - ctxSpaceFullPath - ctxSpaceRelativePath ctxOCM ctxPublicLink + ctxStorageId + ctxResourceOpaqueId ) var ( diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go index 2a962b9442..149d0d5850 100644 --- a/internal/http/services/owncloud/ocdav/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind.go @@ -72,9 +72,18 @@ const ( // ns is the namespace that is prefixed to the path in the cs3 namespace. func (s *svc) handlePathPropfind(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() + fn := path.Join(ns, r.URL.Path) + ref := &provider.Reference{} - sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() + // We check if the PROPFIND was made to a resource ID instead of a path + if r, ok := requestWasMadeToResourceId(ctx, fn); ok { + ref = r + } else { + ref.Path = fn + } + + sublog := appctx.GetLogger(ctx).With().Any("ref", ref).Logger() pf, status, err := readPropfind(r.Body) if err != nil { @@ -83,8 +92,6 @@ func (s *svc) handlePathPropfind(w http.ResponseWriter, r *http.Request, ns stri return } - ref := &provider.Reference{Path: fn} - parentInfo, resourceInfos, ok := s.getResourceInfos(ctx, w, r, pf, ref, false, sublog) if !ok { // getResourceInfos handles responses in case of an error so we can just return here. diff --git a/internal/http/services/owncloud/ocdav/put.go b/internal/http/services/owncloud/ocdav/put.go index 1594fd64eb..b0d24c22d9 100644 --- a/internal/http/services/owncloud/ocdav/put.go +++ b/internal/http/services/owncloud/ocdav/put.go @@ -111,11 +111,18 @@ func isContentRange(r *http.Request) bool { func (s *svc) handlePathPut(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() + fn := path.Join(ns, r.URL.Path) + ref := &provider.Reference{} - sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() + // We check if the PUT was made to a resource ID instead of a path + if r, ok := requestWasMadeToResourceId(ctx, fn); ok { + ref = r + } else { + ref.Path = fn + } - ref := &provider.Reference{Path: fn} + sublog := appctx.GetLogger(ctx).With().Any("ref", ref).Logger() s.handlePut(ctx, w, r, ref, sublog) } diff --git a/internal/http/services/owncloud/ocdav/spaces.go b/internal/http/services/owncloud/ocdav/spaces.go index 5a659072f4..47461d8533 100644 --- a/internal/http/services/owncloud/ocdav/spaces.go +++ b/internal/http/services/owncloud/ocdav/spaces.go @@ -22,8 +22,10 @@ import ( "context" "fmt" "net/http" + "path" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v3/pkg/rhttp/router" "github.com/cs3org/reva/v3/pkg/utils" @@ -130,3 +132,21 @@ func (s *svc) lookUpStorageSpaceReference(ctx context.Context, spaceID string, r Path: utils.MakeRelativePath(relativePath), }, lSSRes.Status, nil } + +func requestWasMadeToResourceId(ctx context.Context, fn string) (ref *provider.Reference, ok bool) { + if opaqueId := ctx.Value(ctxResourceOpaqueId); opaqueId != nil { + storageId := ctx.Value(ctxStorageId) + if storageId != nil { + ref := &provider.Reference{ + // We make the path relative + Path: path.Join(".", fn), + ResourceId: &provider.ResourceId{ + StorageId: storageId.(string), + OpaqueId: opaqueId.(string), + }, + } + return ref, true + } + } + return nil, false +} diff --git a/internal/http/services/owncloud/ocgraph/drives.go b/internal/http/services/owncloud/ocgraph/drives.go index 8cbf69e21d..7cc33e3175 100644 --- a/internal/http/services/owncloud/ocgraph/drives.go +++ b/internal/http/services/owncloud/ocgraph/drives.go @@ -35,12 +35,15 @@ import ( userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" - providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v3/pkg/appctx" "github.com/cs3org/reva/v3/pkg/rhttp/router" "github.com/cs3org/reva/v3/pkg/spaces" "github.com/cs3org/reva/v3/pkg/utils/list" + gomime "github.com/glpatcern/go-mime" + "github.com/go-chi/chi/v5" libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/pkg/errors" ) @@ -78,7 +81,7 @@ func (s *svc) listMySpaces(w http.ResponseWriter, r *http.Request) { return } - res, err := gw.ListStorageSpaces(ctx, &providerpb.ListStorageSpacesRequest{ + res, err := gw.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{ Filters: filters, }) if err != nil { @@ -93,8 +96,8 @@ func (s *svc) listMySpaces(w http.ResponseWriter, r *http.Request) { } me := appctx.ContextMustGetUser(ctx) - spaces = list.Map(res.StorageSpaces, func(space *providerpb.StorageSpace) *libregraph.Drive { - return s.cs3StorageSpaceToDrive(me, space) + spaces = list.Map(res.StorageSpaces, func(space *provider.StorageSpace) *libregraph.Drive { + return s.cs3StorageSpaceToDrive(ctx, me, space) }) } @@ -175,10 +178,11 @@ func (s *svc) convertShareToSpace(rsi *gateway.ReceivedShareResourceInfo) *libre Size: libregraph.PtrInt64(int64(rsi.ResourceInfo.Size)), }, }, + Special: []libregraph.DriveItem{}, } } -func generateCs3StorageSpaceFilters(request *godata.GoDataRequest) ([]*providerpb.ListStorageSpacesRequest_Filter, error) { +func generateCs3StorageSpaceFilters(request *godata.GoDataRequest) ([]*provider.ListStorageSpacesRequest_Filter, error) { var filters spaces.ListStorageSpaceFilter if request.Query.Filter != nil { if request.Query.Filter.Tree.Token.Value == "eq" { @@ -188,7 +192,7 @@ func generateCs3StorageSpaceFilters(request *godata.GoDataRequest) ([]*providerp filters = filters.BySpaceType(spaceType) case "id": id := strings.Trim(request.Query.Filter.Tree.Children[1].Token.Value, "'") - filters = filters.ByID(&providerpb.StorageSpaceId{OpaqueId: id}) + filters = filters.ByID(&provider.StorageSpaceId{OpaqueId: id}) } } else { err := errors.Errorf("unsupported filter operand: %s", request.Query.Filter.Tree.Token.Value) @@ -198,16 +202,57 @@ func generateCs3StorageSpaceFilters(request *godata.GoDataRequest) ([]*providerp return filters.List(), nil } -func (s *svc) cs3StorageSpaceToDrive(user *userpb.User, space *providerpb.StorageSpace) *libregraph.Drive { +func (s *svc) cs3StorageSpaceToDrive(ctx context.Context, user *userpb.User, space *provider.StorageSpace) *libregraph.Drive { + log := appctx.GetLogger(ctx) + drive := &libregraph.Drive{ DriveAlias: libregraph.PtrString(space.RootInfo.Path[1:]), Id: libregraph.PtrString(space.Id.OpaqueId), Name: space.Name, DriveType: libregraph.PtrString(space.SpaceType), + Special: []libregraph.DriveItem{}, } drive.Root = &libregraph.DriveItem{} + if space.ReadmeId != "" || space.ThumbnailId != "" { + gw, err := s.getClient() + // If an error occurs, we just don't set the readme / thumbnail + if err == nil { + if space.ReadmeId != "" { + res, err := gw.Stat(ctx, &provider.StatRequest{ + Ref: &provider.Reference{ + Path: space.ReadmeId, + }, + }) + if err == nil && res.Status.Code == rpcv1beta1.Code_CODE_OK { + item := s.ResourceInfoToDriveItem(res.Info, "readme") + drive.Special = append(drive.Special, item) + } else { + log.Error().Err(err).Str("spaceid", space.Id.OpaqueId).Any("status", res.Status).Msg("Failed to stat space README") + } + } + if space.ThumbnailId != "" { + res, err := gw.Stat(ctx, &provider.StatRequest{ + Ref: &provider.Reference{ + Path: space.ThumbnailId, + }, + }) + if err == nil && res.Status.Code == rpcv1beta1.Code_CODE_OK { + drive.Special = append(drive.Special, s.ResourceInfoToDriveItem(res.Info, "image")) + } else { + log.Error().Err(err).Str("spaceid", space.Id.OpaqueId).Any("status", res.Status).Msg("Failed to stat space thumbnail") + } + } + } else { + log.Error().Err(err).Any("spaceID", space.Id).Msg("Failed to get gateway client") + } + } + + if space.Description != "" { + drive.Description = libregraph.PtrString(space.Description) + } + if space.SpaceType != "personal" { drive.Root = &libregraph.DriveItem{ Id: libregraph.PtrString(space.Id.OpaqueId), @@ -275,8 +320,8 @@ func (s *svc) getSpace(w http.ResponseWriter, r *http.Request) { return } - stat, err := gw.Stat(ctx, &providerpb.StatRequest{ - Ref: &providerpb.Reference{ + stat, err := gw.Stat(ctx, &provider.StatRequest{ + Ref: &provider.Reference{ ResourceId: shareRes.Share.Share.ResourceId, }, }) @@ -298,12 +343,12 @@ func (s *svc) getSpace(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(space) return } else { - listRes, err := gw.ListStorageSpaces(ctx, &providerpb.ListStorageSpacesRequest{ - Filters: []*providerpb.ListStorageSpacesRequest_Filter{ + listRes, err := gw.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{ + Filters: []*provider.ListStorageSpacesRequest_Filter{ { - Type: providerpb.ListStorageSpacesRequest_Filter_TYPE_ID, - Term: &providerpb.ListStorageSpacesRequest_Filter_Id{ - Id: &providerpb.StorageSpaceId{ + Type: provider.ListStorageSpacesRequest_Filter_TYPE_ID, + Term: &provider.ListStorageSpacesRequest_Filter_Id{ + Id: &provider.StorageSpaceId{ OpaqueId: spaceID, }, }, @@ -324,7 +369,7 @@ func (s *svc) getSpace(w http.ResponseWriter, r *http.Request) { spaces := listRes.StorageSpaces if len(spaces) == 1 { user := appctx.ContextMustGetUser(ctx) - space := s.cs3StorageSpaceToDrive(user, spaces[0]) + space := s.cs3StorageSpaceToDrive(ctx, user, spaces[0]) _ = json.NewEncoder(w).Encode(space) return } @@ -333,6 +378,120 @@ func (s *svc) getSpace(w http.ResponseWriter, r *http.Request) { handleError(ctx, errors.New("space not found"), http.StatusNotFound, w) } +func (s *svc) patchSpace(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := appctx.GetLogger(ctx) + + update := &libregraph.DriveUpdate{} + dec := json.NewDecoder(r.Body) + dec.DisallowUnknownFields() + if err := dec.Decode(update); err != nil { + log.Error().Err(err).Interface("Body", r.Body).Msg("failed unmarshalling request body") + w.WriteHeader(http.StatusBadRequest) + return + } + + if update.Name == nil { + handleError(ctx, errors.New("patching a space requires the space name"), http.StatusBadRequest, w) + return + } + + gw, err := s.getClient() + if err != nil { + log.Error().Err(err).Msg("error getting grpc client") + handleError(ctx, err, http.StatusInternalServerError, w) + return + } + + spaceId := chi.URLParam(r, "space-id") + updateRequest := &provider.UpdateStorageSpaceRequest{ + StorageSpace: &provider.StorageSpace{ + Id: &provider.StorageSpaceId{ + OpaqueId: spaceId, + }, + Name: *update.Name, + }} + + if len(update.Special) > 0 { + updateData := update.Special[0] + if updateData.Id == nil || updateData.SpecialFolder == nil { + handleError(ctx, errors.New("Unsupported update type"), http.StatusBadRequest, w) + return + } + + storage, _, id, ok := spaces.DecodeResourceID(*updateData.Id) + if !ok { + handleError(ctx, errors.New("ID not in an understandable format"), http.StatusBadRequest, w) + return + } + + statRes, err := gw.Stat(ctx, &provider.StatRequest{ + Ref: &provider.Reference{ + ResourceId: &provider.ResourceId{ + StorageId: storage, + OpaqueId: id, + }, + }, + }) + + if err != nil || statRes.Status == nil || statRes.Status.Code != rpcv1beta1.Code_CODE_OK { + log.Error().Err(err).Any("res", statRes.Status).Msg("error statting provided special resource") + handleError(ctx, err, http.StatusInternalServerError, w) + return + } + + switch *updateData.SpecialFolder.Name { + case "readme": + updateRequest.Field = &provider.UpdateStorageSpaceRequest_UpdateField{ + Field: &provider.UpdateStorageSpaceRequest_UpdateField_Metadata{ + Metadata: &provider.SpaceMetadata{ + Type: provider.SpaceMetadata_TYPE_README, + Id: statRes.Info.Path, + }, + }, + } + case "image": + updateRequest.Field = &provider.UpdateStorageSpaceRequest_UpdateField{ + Field: &provider.UpdateStorageSpaceRequest_UpdateField_Metadata{ + Metadata: &provider.SpaceMetadata{ + Type: provider.SpaceMetadata_TYPE_THUMBNAIL, + Id: statRes.Info.Path, + }, + }, + } + default: + handleError(ctx, errors.New("Unsupported update type"), http.StatusBadRequest, w) + return + } + } else if update.Description != nil { + updateRequest.Field = &provider.UpdateStorageSpaceRequest_UpdateField{ + Field: &provider.UpdateStorageSpaceRequest_UpdateField_Description{ + Description: *update.Description, + }, + } + } else { + handleError(ctx, errors.New("Unsupported update type"), http.StatusBadRequest, w) + return + } + + res, err := gw.UpdateStorageSpace(ctx, updateRequest) + + if err != nil { + handleError(ctx, errors.New("failed to update storage space"), http.StatusInternalServerError, w) + return + } + + if res.Status.Code != rpcv1beta1.Code_CODE_OK { + log.Error().Interface("response", res).Msg("error updating public share") + handleError(ctx, errors.New("failed to update storage space"), http.StatusInternalServerError, w) + return + } + + user := appctx.ContextMustGetUser(ctx) + space := s.cs3StorageSpaceToDrive(ctx, user, res.StorageSpace) + _ = json.NewEncoder(w).Encode(space) +} + func isShareJail(spaceID string) bool { return spaceID == ShareJailID } @@ -347,7 +506,7 @@ func fullURL(base, path string) string { return full } -func cs3PermissionsToLibreGraph(user *userpb.User, perms *providerpb.ResourcePermissions) []libregraph.Permission { +func cs3PermissionsToLibreGraph(user *userpb.User, perms *provider.ResourcePermissions) []libregraph.Permission { var p libregraph.Permission // we need to map the permissions to the roles switch { @@ -378,3 +537,31 @@ func cs3PermissionsToLibreGraph(user *userpb.User, perms *providerpb.ResourcePer } return []libregraph.Permission{p} } + +func (s *svc) ResourceInfoToDriveItem(r *provider.ResourceInfo, special string) libregraph.DriveItem { + + item := libregraph.DriveItem{ + Id: libregraph.PtrString(spaces.EncodeResourceID(r.Id)), + ETag: libregraph.PtrString(r.Etag), + Name: libregraph.PtrString(r.Name), + Size: libregraph.PtrInt64(int64(r.Size)), + WebDavUrl: libregraph.PtrString(fullURL(s.c.WebDavBase, r.Path)), + } + + if len(strings.Split(r.Path, ".")) > 1 { + mimetype := gomime.TypeByExtension(filepath.Ext(r.Path)) + if mimetype != "" { + item.File = &libregraph.OpenGraphFile{ + MimeType: libregraph.PtrString(mimetype), + } + } + } + + if special != "" { + item.SpecialFolder = &libregraph.SpecialFolder{ + Name: libregraph.PtrString(special), + } + } + + return item +} diff --git a/internal/http/services/owncloud/ocgraph/ocgraph.go b/internal/http/services/owncloud/ocgraph/ocgraph.go index 24ed23ff69..eb62c93079 100644 --- a/internal/http/services/owncloud/ocgraph/ocgraph.go +++ b/internal/http/services/owncloud/ocgraph/ocgraph.go @@ -103,6 +103,7 @@ func (s *svc) initRouter() { }) r.Route("/drives", func(r chi.Router) { r.Get("/{space-id}", s.getSpace) + r.Patch("/{space-id}", s.patchSpace) }) r.Route("/users", func(r chi.Router) { r.Get("/", s.listUsers) @@ -111,6 +112,7 @@ func (s *svc) initRouter() { r.Get("/", s.listGroups) }) }) + s.router.Route("/v1beta1", func(r chi.Router) { r.Route("/me", func(r chi.Router) { r.Route("/drives", func(r chi.Router) { diff --git a/internal/http/services/owncloud/ocgraph/users.go b/internal/http/services/owncloud/ocgraph/users.go index 8605acc855..df0be969f2 100644 --- a/internal/http/services/owncloud/ocgraph/users.go +++ b/internal/http/services/owncloud/ocgraph/users.go @@ -64,9 +64,9 @@ func (s UserSelectableProperty) Valid() bool { func (s *svc) getMe(w http.ResponseWriter, r *http.Request) { user := appctx.ContextMustGetUser(r.Context()) me := &libregraph.User{ - DisplayName: &user.DisplayName, + DisplayName: user.DisplayName, Mail: &user.Mail, - OnPremisesSamAccountName: &user.Username, + OnPremisesSamAccountName: user.Username, Id: &user.Id.OpaqueId, } _ = json.NewEncoder(w).Encode(me) @@ -174,8 +174,8 @@ func mapToLibregraphUsers(users []*userpb.User, selection []UserSelectableProper lgUser = libregraph.User{ Id: &u.Id.OpaqueId, Mail: &u.Mail, - OnPremisesSamAccountName: &u.Username, - DisplayName: &u.DisplayName, + OnPremisesSamAccountName: u.Username, + DisplayName: u.DisplayName, } } else { for _, prop := range selection { @@ -197,11 +197,11 @@ func appendPropToLgUser(u *userpb.User, lgUser libregraph.User, prop UserSelecta case propUserId: lgUser.Id = &u.Id.OpaqueId case propUserDisplayName: - lgUser.DisplayName = &u.DisplayName + lgUser.DisplayName = u.DisplayName case propUserMail: lgUser.Mail = &u.Mail case propUserOnPremisesSamAccountName: - lgUser.OnPremisesSamAccountName = &u.Username + lgUser.OnPremisesSamAccountName = u.Username } return lgUser } @@ -217,7 +217,7 @@ func sortUsers(ctx context.Context, users []libregraph.User, sortKey string) ([] switch UserSelectableProperty(sortKey) { case propUserDisplayName: slices.SortFunc(users, func(a, b libregraph.User) int { - return cmp.Compare(*a.DisplayName, *b.DisplayName) + return cmp.Compare(a.DisplayName, b.DisplayName) }) case propUserId: slices.SortFunc(users, func(a, b libregraph.User) int { @@ -229,7 +229,7 @@ func sortUsers(ctx context.Context, users []libregraph.User, sortKey string) ([] }) case propUserOnPremisesSamAccountName: slices.SortFunc(users, func(a, b libregraph.User) int { - return cmp.Compare(*a.OnPremisesSamAccountName, *b.OnPremisesSamAccountName) + return cmp.Compare(a.OnPremisesSamAccountName, b.OnPremisesSamAccountName) }) } return users, nil diff --git a/pkg/projects/manager/memory/memory.go b/pkg/projects/manager/memory/memory.go index b64024ca4c..3d059a135e 100644 --- a/pkg/projects/manager/memory/memory.go +++ b/pkg/projects/manager/memory/memory.go @@ -20,14 +20,17 @@ package memory import ( "context" + "errors" "slices" + "github.com/cs3org/reva/v3/pkg/appctx" "github.com/cs3org/reva/v3/pkg/projects" "github.com/cs3org/reva/v3/pkg/projects/manager/registry" "github.com/cs3org/reva/v3/pkg/spaces" "github.com/cs3org/reva/v3/pkg/utils/cfg" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" conversions "github.com/cs3org/reva/v3/internal/http/services/owncloud/ocs/conversions" ) @@ -66,8 +69,17 @@ func NewWithConfig(ctx context.Context, c *Config) (projects.Catalogue, error) { return &service{c: c}, nil } -func (s *service) ListProjects(ctx context.Context, user *userpb.User) ([]*provider.StorageSpace, error) { +func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSpacesRequest) (*provider.ListStorageSpacesResponse, error) { projects := []*provider.StorageSpace{} + user, ok := appctx.ContextGetUser(ctx) + if !ok { + return &provider.ListStorageSpacesResponse{ + Status: &rpcv1beta1.Status{ + Code: rpcv1beta1.Code_CODE_UNAUTHENTICATED, + Message: "must provide a user for listing storage spaces", + }, + }, nil + } for _, space := range s.c.Spaces { if perms, ok := projectBelongToUser(user, &space); ok { projects = append(projects, &provider.StorageSpace{ @@ -88,7 +100,24 @@ func (s *service) ListProjects(ctx context.Context, user *userpb.User) ([]*provi }) } } - return projects, nil + return &provider.ListStorageSpacesResponse{ + StorageSpaces: projects, + Status: &rpcv1beta1.Status{ + Code: rpcv1beta1.Code_CODE_OK, + }, + }, nil +} + +func (s *service) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) { + return nil, errors.New("Unsupported") +} + +func (s *service) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { + return nil, errors.New("Unsupported") +} + +func (s *service) DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) (*provider.DeleteStorageSpaceResponse, error) { + return nil, errors.New("Unsupported") } func projectBelongToUser(user *userpb.User, project *SpaceDescription) (*provider.ResourcePermissions, bool) { diff --git a/pkg/projects/manager/sql/sql.go b/pkg/projects/manager/sql/sql.go index 76eea0aa99..1c0195de29 100644 --- a/pkg/projects/manager/sql/sql.go +++ b/pkg/projects/manager/sql/sql.go @@ -26,8 +26,10 @@ import ( "github.com/ReneKroon/ttlcache/v2" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v3/internal/http/services/owncloud/ocs/conversions" + "github.com/cs3org/reva/v3/pkg/appctx" "github.com/cs3org/reva/v3/pkg/projects" "github.com/cs3org/reva/v3/pkg/projects/manager/registry" "github.com/cs3org/reva/v3/pkg/spaces" @@ -71,9 +73,19 @@ type Project struct { Path string Name string `gorm:"size:255;uniqueIndex:i_name"` Owner string `gorm:"size:255"` - Readers string - Writers string - Admins string + // Readers e-group ID + Readers string + // Writers e-group ID + Writers string + // Admins e-group ID + Admins string + // Called description in libregraph API + // Called subtitle in front-end + Description string + // Path of readme.md + ReadmePath string + // Path of the thumbnail file + ThumbnailPath string } func New(ctx context.Context, m map[string]any) (projects.Catalogue, error) { @@ -119,9 +131,19 @@ func New(ctx context.Context, m map[string]any) (projects.Catalogue, error) { return mgr, nil } -func (m *mgr) ListProjects(ctx context.Context, user *userpb.User) ([]*provider.StorageSpace, error) { +func (m *mgr) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSpacesRequest) (*provider.ListStorageSpacesResponse, error) { var fetchedProjects []*Project + user, ok := appctx.ContextGetUser(ctx) + if !ok { + return &provider.ListStorageSpacesResponse{ + Status: &rpcv1beta1.Status{ + Code: rpcv1beta1.Code_CODE_UNAUTHENTICATED, + Message: "must provide a user for listing storage spaces", + }, + }, nil + } + if res, err := m.cache.Get(cacheKey); err == nil && res != nil { fetchedProjects = res.([]*Project) } else { @@ -135,30 +157,107 @@ func (m *mgr) ListProjects(ctx context.Context, user *userpb.User) ([]*provider. projects := []*provider.StorageSpace{} for _, p := range fetchedProjects { - if perms, ok := projectBelongToUser(user, p); ok { - projects = append(projects, &provider.StorageSpace{ - Id: &provider.StorageSpaceId{ - OpaqueId: spaces.EncodeStorageSpaceID(p.StorageID, p.Path), - }, - Owner: &userpb.User{ - Id: &userpb.UserId{ - OpaqueId: p.Owner, - }, - }, - Name: p.Name, - SpaceType: spaces.SpaceTypeProject.AsString(), - RootInfo: &provider.ResourceInfo{ - Path: p.Path, - PermissionSet: perms, - }, - }) + if perms, ok := projectBelongsToUser(user, p); ok { + projects = append(projects, projectToStorageSpace(p, perms)) } } - return projects, nil + return &provider.ListStorageSpacesResponse{ + StorageSpaces: projects, + Status: &rpcv1beta1.Status{ + Code: rpcv1beta1.Code_CODE_OK, + }, + }, nil } -func projectBelongToUser(user *userpb.User, p *Project) (*provider.ResourcePermissions, bool) { +func (m *mgr) GetStorageSpace(ctx context.Context, name string) (*provider.StorageSpace, error) { + var fetchedProject *Project + + user, ok := appctx.ContextGetUser(ctx) + if !ok { + return nil, errors.New("must provide a user for fetching storage spaces") + } + + query := m.db.Model(&Project{}).Where("name = ?", name) + res := query.First(fetchedProject) + if res.Error != nil { + return nil, res.Error + } + + if perms, ok := projectBelongsToUser(user, fetchedProject); ok { + return projectToStorageSpace(fetchedProject, perms), nil + } + return nil, fmt.Errorf("no project named %s belonging to which user has access was found", name) +} + +func (m *mgr) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) { + log := appctx.GetLogger(ctx) + if req == nil || req.StorageSpace == nil || req.StorageSpace.Id == nil { + log.Error().Msg("UpdateStorageSpace called without valid request") + return &provider.UpdateStorageSpaceResponse{ + Status: &rpcv1beta1.Status{ + Code: rpcv1beta1.Code_CODE_INVALID, + }, + }, errors.New("Must provide an ID when updating a storage space") + } + log.Debug().Any("space", req.StorageSpace).Any("update", req.Field).Msg("Updating storage space") + + if req.Field == nil { + return &provider.UpdateStorageSpaceResponse{ + Status: &rpcv1beta1.Status{ + Code: rpcv1beta1.Code_CODE_INVALID, + }, + }, errors.New("No field given to update") + } + + var res *gorm.DB + + switch req.Field.Field.(type) { + case *provider.UpdateStorageSpaceRequest_UpdateField_Description: + res = m.db.Model(&Project{}). + Where("name = ?", req.StorageSpace.Name). + Update("description", req.Field.GetDescription()) + case *provider.UpdateStorageSpaceRequest_UpdateField_Metadata: + switch req.Field.GetMetadata().Type { + case provider.SpaceMetadata_TYPE_README: + res = m.db.Model(&Project{}). + Where("name = ?", req.StorageSpace.Name). + Update("readme_path", req.Field.GetMetadata().Id) + case provider.SpaceMetadata_TYPE_THUMBNAIL: + res = m.db.Model(&Project{}). + Where("name = ?", req.StorageSpace.Name). + Update("thumbnail_path", req.Field.GetMetadata().Id) + } + default: + return nil, errors.New("Unsupported update type") + } + + if res.Error != nil { + return nil, res.Error + } + + space, err := m.GetStorageSpace(ctx, req.StorageSpace.Name) + if err != nil { + return nil, err + } + + return &provider.UpdateStorageSpaceResponse{ + Status: &rpcv1beta1.Status{ + Code: rpcv1beta1.Code_CODE_OK, + }, + StorageSpace: space, + }, nil +} + +func (m *mgr) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { + return nil, errors.New("Unsupported") +} + +func (m *mgr) DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) (*provider.DeleteStorageSpaceResponse, error) { + return nil, errors.New("Unsupported") +} + +func projectBelongsToUser(user *userpb.User, p *Project) (*provider.ResourcePermissions, bool) { if user.Id.OpaqueId == p.Owner { return conversions.NewManagerRole().CS3ResourcePermissions(), true } @@ -173,3 +272,25 @@ func projectBelongToUser(user *userpb.User, p *Project) (*provider.ResourcePermi } return nil, false } + +func projectToStorageSpace(p *Project, perms *provider.ResourcePermissions) *provider.StorageSpace { + return &provider.StorageSpace{ + Id: &provider.StorageSpaceId{ + OpaqueId: spaces.EncodeStorageSpaceID(p.StorageID, p.Path), + }, + Owner: &userpb.User{ + Id: &userpb.UserId{ + OpaqueId: p.Owner, + }, + }, + Name: p.Name, + SpaceType: spaces.SpaceTypeProject.AsString(), + RootInfo: &provider.ResourceInfo{ + Path: p.Path, + PermissionSet: perms, + }, + Description: p.Description, + ThumbnailId: p.ThumbnailPath, + ReadmeId: p.ReadmePath, + } +} diff --git a/pkg/projects/manager/sql/sql_test.go b/pkg/projects/manager/sql/sql_test.go index 9cc2c69127..89c1fc744d 100644 --- a/pkg/projects/manager/sql/sql_test.go +++ b/pkg/projects/manager/sql/sql_test.go @@ -28,6 +28,7 @@ import ( userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v3/internal/http/services/owncloud/ocs/conversions" + "github.com/cs3org/reva/v3/pkg/appctx" projects_catalogue "github.com/cs3org/reva/v3/pkg/projects" "github.com/cs3org/reva/v3/pkg/spaces" ) @@ -267,13 +268,14 @@ func TestListProjects(t *testing.T) { catmgr.db.Create(&proj) } - got, err := catalogue.ListProjects(ctx, tt.user) + ctx = appctx.ContextSetUser(ctx, tt.user) + got, err := catalogue.ListStorageSpaces(ctx, &provider.ListStorageSpacesRequest{}) if err != nil { t.Fatalf("not expected error while listing projects: %+v", err) } - if !reflect.DeepEqual(got, tt.expected) { - t.Fatalf("projects' list do not match. got=%#v expected=%#v", got, tt.expected) + if !reflect.DeepEqual(got.StorageSpaces, tt.expected) { + t.Fatalf("projects' list do not match. got=%#v expected=%#v", got.StorageSpaces, tt.expected) } err = teardown(t) diff --git a/pkg/projects/projects.go b/pkg/projects/projects.go index db64d0831c..7ca696b4c2 100644 --- a/pkg/projects/projects.go +++ b/pkg/projects/projects.go @@ -21,11 +21,13 @@ package projects import ( "context" - userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" ) // Catalogue is the interface that stores the project spaces. type Catalogue interface { - ListProjects(ctx context.Context, user *userpb.User) ([]*provider.StorageSpace, error) + CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) + ListStorageSpaces(ctx context.Context, req *provider.ListStorageSpacesRequest) (*provider.ListStorageSpacesResponse, error) + UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) + DeleteStorageSpace(ctx context.Context, req *provider.DeleteStorageSpaceRequest) (*provider.DeleteStorageSpaceResponse, error) } diff --git a/pkg/spaces/utils.go b/pkg/spaces/utils.go index 591bdc9b8b..6ee0ddd47d 100644 --- a/pkg/spaces/utils.go +++ b/pkg/spaces/utils.go @@ -90,15 +90,15 @@ func EncodeResourceID(r *provider.ResourceId) string { // Decode resourceID returns the components of the space ID. // The resource ID is expected to be in the form of $base32()!. -func DecodeResourceID(raw string) (storageID, path, itemID string, ok bool) { +func DecodeResourceID(raw string) (storageID, spacePath, itemID string, ok bool) { // The input is expected to be in the form of $base32()! s := strings.SplitN(raw, "!", 2) if len(s) != 2 { return "", "", "", false } itemID = s[1] - storageID, path, ok = DecodeStorageSpaceID(s[0]) - return storageID, path, itemID, ok + storageID, spacePath, ok = DecodeStorageSpaceID(s[0]) + return storageID, spacePath, itemID, ok } // ParseResourceID converts the encoded resource id in a CS3API ResourceId.