From 9ffcd414f24d883b79ddfb25794ba433b029bf08 Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Sat, 30 Apr 2022 20:42:41 +0200 Subject: [PATCH 1/5] fix space shares and storage ids --- internal/grpc/services/gateway/usershareprovider.go | 4 +++- internal/grpc/services/storageprovider/storageprovider.go | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/grpc/services/gateway/usershareprovider.go b/internal/grpc/services/gateway/usershareprovider.go index edb577f915..d29af8f887 100644 --- a/internal/grpc/services/gateway/usershareprovider.go +++ b/internal/grpc/services/gateway/usershareprovider.go @@ -35,6 +35,7 @@ import ( "github.com/cs3org/reva/v2/pkg/share" "github.com/cs3org/reva/v2/pkg/storage/utils/grants" rtrace "github.com/cs3org/reva/v2/pkg/trace" + "github.com/cs3org/reva/v2/pkg/utils/resourceid" "github.com/pkg/errors" ) @@ -778,7 +779,8 @@ func refIsSpaceRoot(ref *provider.ResourceId) bool { if ref.StorageId == "" || ref.OpaqueId == "" { return false } - if ref.StorageId != ref.OpaqueId { + sid, _ := resourceid.StorageIDUnwrap(ref.GetStorageId()) + if sid != ref.OpaqueId { return false } return true diff --git a/internal/grpc/services/storageprovider/storageprovider.go b/internal/grpc/services/storageprovider/storageprovider.go index 1e3ccee676..245d916e6e 100644 --- a/internal/grpc/services/storageprovider/storageprovider.go +++ b/internal/grpc/services/storageprovider/storageprovider.go @@ -506,6 +506,8 @@ func (s *service) CreateStorageSpace(ctx context.Context, req *provider.CreateSt }, nil } + resp.StorageSpace.Id.OpaqueId = resourceid.StorageIDWrap(resp.StorageSpace.Id.GetOpaqueId(), s.conf.MountID) + resp.StorageSpace.Root.StorageId = resourceid.StorageIDWrap(resp.StorageSpace.Root.GetStorageId(), s.conf.MountID) return resp, nil } @@ -582,6 +584,7 @@ func (s *service) UpdateStorageSpace(ctx context.Context, req *provider.UpdateSt return nil, err } res.StorageSpace.Id.OpaqueId = resourceid.StorageIDWrap(res.StorageSpace.Id.GetOpaqueId(), s.conf.MountID) + res.StorageSpace.Root.StorageId = resourceid.StorageIDWrap(res.StorageSpace.Root.GetStorageId(), s.conf.MountID) return res, nil } From 21dc859d89a30cfe9d33d8e1ea21b058819a672c Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Sat, 30 Apr 2022 23:39:16 +0200 Subject: [PATCH 2/5] Revert "Concurrently stat shares in ocs service (#2787)" This reverts commit 7f0132a75a36ee5ad345182b2958465f03775084. --- changelog/unreleased/ocs-concurrent-stat.md | 3 - .../handlers/apps/sharing/shares/public.go | 87 ++++---- .../handlers/apps/sharing/shares/shares.go | 189 ++++++++---------- .../ocs/handlers/apps/sharing/shares/user.go | 89 +++------ 4 files changed, 151 insertions(+), 217 deletions(-) delete mode 100644 changelog/unreleased/ocs-concurrent-stat.md diff --git a/changelog/unreleased/ocs-concurrent-stat.md b/changelog/unreleased/ocs-concurrent-stat.md deleted file mode 100644 index 962c722d29..0000000000 --- a/changelog/unreleased/ocs-concurrent-stat.md +++ /dev/null @@ -1,3 +0,0 @@ -Enhancement: Concurrently resolve shares in ocs HTTP service - -https://github.com/cs3org/reva/pull/2787 \ No newline at end of file diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go index 39e34bc7b1..a56444daaa 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/public.go @@ -19,14 +19,11 @@ package shares import ( - "context" "encoding/json" "fmt" "net/http" "strconv" - "sync" - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" @@ -183,68 +180,52 @@ func (h *Handler) listPublicShares(r *http.Request, filters []*link.ListPublicSh log := appctx.GetLogger(ctx) ocsDataPayload := make([]*conversions.ShareData, 0) - client, err := pool.GetGatewayServiceClient(h.gatewayAddr) - if err != nil { - return ocsDataPayload, nil, err - } - - req := link.ListPublicSharesRequest{ - Filters: filters, - } + // TODO(refs) why is this guard needed? Are we moving towards a gateway only for service discovery? without a gateway this is dead code. + if h.gatewayAddr != "" { + client, err := pool.GetGatewayServiceClient(h.gatewayAddr) + if err != nil { + return ocsDataPayload, nil, err + } - res, err := client.ListPublicShares(ctx, &req) - if err != nil { - return ocsDataPayload, nil, err - } - if res.Status.Code != rpc.Code_CODE_OK { - return ocsDataPayload, res.Status, nil - } + req := link.ListPublicSharesRequest{ + Filters: filters, + } - var wg sync.WaitGroup - workers := 50 - input := make(chan *link.PublicShare, len(res.Share)) - output := make(chan *conversions.ShareData, len(res.Share)) + res, err := client.ListPublicShares(ctx, &req) + if err != nil { + return ocsDataPayload, nil, err + } + if res.Status.Code != rpc.Code_CODE_OK { + return ocsDataPayload, res.Status, nil + } - for i := 0; i < workers; i++ { - wg.Add(1) - go func(ctx context.Context, client gateway.GatewayAPIClient, input chan *link.PublicShare, output chan *conversions.ShareData, wg *sync.WaitGroup) { - defer wg.Done() + for _, share := range res.GetShare() { + info, status, err := h.getResourceInfoByID(ctx, client, share.ResourceId) + if err != nil || status.Code != rpc.Code_CODE_OK { + log.Debug().Interface("share", share).Interface("status", status).Err(err).Msg("could not stat share, skipping") + continue + } - for share := range input { - info, status, err := h.getResourceInfoByID(ctx, client, share.ResourceId) - if err != nil || status.Code != rpc.Code_CODE_OK { - log.Debug().Interface("share", share).Interface("status", status).Err(err).Msg("could not stat share, skipping") - return - } + sData := conversions.PublicShare2ShareData(share, r, h.publicURL) - sData := conversions.PublicShare2ShareData(share, r, h.publicURL) + sData.Name = share.DisplayName - sData.Name = share.DisplayName + if err := h.addFileInfo(ctx, sData, info); err != nil { + log.Debug().Interface("share", share).Interface("info", info).Err(err).Msg("could not add file info, skipping") + continue + } + h.mapUserIds(ctx, client, sData) - if err := h.addFileInfo(ctx, sData, info); err != nil { - log.Debug().Interface("share", share).Interface("info", info).Err(err).Msg("could not add file info, skipping") - return - } - h.mapUserIds(ctx, client, sData) + log.Debug().Interface("share", share).Interface("info", info).Interface("shareData", share).Msg("mapped") - log.Debug().Interface("share", share).Interface("info", info).Interface("shareData", sData).Msg("mapped") - output <- sData - } - }(ctx, client, input, output, &wg) - } + ocsDataPayload = append(ocsDataPayload, sData) - for _, share := range res.Share { - input <- share - } - close(input) - wg.Wait() - close(output) + } - for s := range output { - ocsDataPayload = append(ocsDataPayload, s) + return ocsDataPayload, nil, nil } - return ocsDataPayload, nil, nil + return ocsDataPayload, nil, errors.New("bad request") } func (h *Handler) isPublicShare(r *http.Request, oid string) bool { diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go index c1d96250d5..bbfd776133 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go @@ -28,7 +28,6 @@ import ( "path" "strconv" "strings" - "sync" "text/template" "time" @@ -800,121 +799,101 @@ func (h *Handler) listSharesWithMe(w http.ResponseWriter, r *http.Request) { shares := make([]*conversions.ShareData, 0, len(lrsRes.GetShares())) - var wg sync.WaitGroup - workers := 50 - input := make(chan *collaboration.ReceivedShare, len(lrsRes.GetShares())) - output := make(chan *conversions.ShareData, len(lrsRes.GetShares())) + // TODO(refs) filter out "invalid" shares + for _, rs := range lrsRes.GetShares() { + if stateFilter != ocsStateUnknown && rs.GetState() != stateFilter { + continue + } + var info *provider.ResourceInfo + if pinfo != nil { + // check if the shared resource matches the path resource + if !utils.ResourceIDEqual(rs.Share.ResourceId, pinfo.Id) { + // try next share + continue + } + // we can reuse the stat info + info = pinfo + } else { + var status *rpc.Status + // FIXME the ResourceID is the id of the resource, but we want the id of the mount point so we can fetch that path, well we have the mountpoint path in the receivedshare + // first stat mount point + mountID := &provider.ResourceId{ + StorageId: utils.ShareStorageProviderID, + OpaqueId: rs.Share.Id.OpaqueId, + } + info, status, err = h.getResourceInfoByID(ctx, client, mountID) + if err != nil || status.Code != rpc.Code_CODE_OK { + // fallback to unmounted resource + info, status, err = h.getResourceInfoByID(ctx, client, rs.Share.ResourceId) + if err != nil || status.Code != rpc.Code_CODE_OK { + h.logProblems(status, err, "could not stat, skipping") + continue + } + } + } - for i := 0; i < workers; i++ { - wg.Add(1) - go func(ctx context.Context, client gateway.GatewayAPIClient, input chan *collaboration.ReceivedShare, output chan *conversions.ShareData, wg *sync.WaitGroup) { - defer wg.Done() + data, err := conversions.CS3Share2ShareData(r.Context(), rs.Share) + if err != nil { + log.Debug().Interface("share", rs.Share).Interface("shareData", data).Err(err).Msg("could not CS3Share2ShareData, skipping") + continue + } - for rs := range input { - if stateFilter != ocsStateUnknown && rs.GetState() != stateFilter { - return - } - var info *provider.ResourceInfo - if pinfo != nil { - // check if the shared resource matches the path resource - if !utils.ResourceIDEqual(rs.Share.ResourceId, pinfo.Id) { - // try next share - return - } - // we can reuse the stat info - info = pinfo - } else { - var status *rpc.Status - // FIXME the ResourceID is the id of the resource, but we want the id of the mount point so we can fetch that path, well we have the mountpoint path in the receivedshare - // first stat mount point - mountID := &provider.ResourceId{ - StorageId: utils.ShareStorageProviderID, - OpaqueId: rs.Share.Id.OpaqueId, - } - info, status, err = h.getResourceInfoByID(ctx, client, mountID) - if err != nil || status.Code != rpc.Code_CODE_OK { - // fallback to unmounted resource - info, status, err = h.getResourceInfoByID(ctx, client, rs.Share.ResourceId) - if err != nil || status.Code != rpc.Code_CODE_OK { - h.logProblems(status, err, "could not stat, skipping") - return - } - } - } + data.State = mapState(rs.GetState()) - data, err := conversions.CS3Share2ShareData(r.Context(), rs.Share) - if err != nil { - log.Debug().Interface("share", rs.Share).Interface("shareData", data).Err(err).Msg("could not CS3Share2ShareData, skipping") - return - } + if err := h.addFileInfo(ctx, data, info); err != nil { + log.Debug().Interface("received_share", rs).Interface("info", info).Interface("shareData", data).Err(err).Msg("could not add file info, skipping") + continue + } + h.mapUserIds(r.Context(), client, data) - data.State = mapState(rs.GetState()) + if data.State == ocsStateAccepted { + // only accepted shares can be accessed when jailing users into their home. + // in this case we cannot stat shared resources that are outside the users home (/home), + // the path (/users/u-u-i-d/foo) will not be accessible - if err := h.addFileInfo(ctx, data, info); err != nil { - log.Debug().Interface("received_share", rs).Interface("info", info).Interface("shareData", data).Err(err).Msg("could not add file info, skipping") - return - } - h.mapUserIds(r.Context(), client, data) - - if data.State == ocsStateAccepted { - // only accepted shares can be accessed when jailing users into their home. - // in this case we cannot stat shared resources that are outside the users home (/home), - // the path (/users/u-u-i-d/foo) will not be accessible - - // in a global namespace we can access the share using the full path - // in a jailed namespace we have to point to the mount point in the users /Shares jail - // - needed for oc10 hot migration - // or use the /dav/spaces/ endpoint? - - // list /Shares and match fileids with list of received shares - // - only works for a /Shares folder jail - // - does not work for freely mountable shares as in oc10 because we would need to iterate over the whole tree, there is no listing of mountpoints, yet - - // can we return the mountpoint when the gateway resolves the listing of shares? - // - no, the gateway only sees the same list any has the same options as the ocs service - // - we would need to have a list of mountpoints for the shares -> owncloudstorageprovider for hot migration migration - - // best we can do for now is stat the /Shares jail if it is set and return those paths - - // if we are in a jail and the current share has been accepted use the stat from the share jail - // Needed because received shares can be jailed in a folder in the users home - - if h.sharePrefix != "/" { - // if we have share jail infos use them to build the path - if rs.MountPoint != nil && rs.MountPoint.Path != "" { - // override path with info from share jail - data.FileTarget = path.Join(h.sharePrefix, rs.MountPoint.Path) - data.Path = path.Join(h.sharePrefix, rs.MountPoint.Path) - } else { - data.FileTarget = path.Join(h.sharePrefix, path.Base(info.Path)) - data.Path = path.Join(h.sharePrefix, path.Base(info.Path)) - } - } else { - data.FileTarget = info.Path - data.Path = info.Path - } - } else { - // not accepted shares need their Path jailed to make the testsuite happy + // in a global namespace we can access the share using the full path + // in a jailed namespace we have to point to the mount point in the users /Shares jail + // - needed for oc10 hot migration + // or use the /dav/spaces/ endpoint? - if h.sharePrefix != "/" { - data.Path = path.Join("/", path.Base(info.Path)) - } + // list /Shares and match fileids with list of received shares + // - only works for a /Shares folder jail + // - does not work for freely mountable shares as in oc10 because we would need to iterate over the whole tree, there is no listing of mountpoints, yet + + // can we return the mountpoint when the gateway resolves the listing of shares? + // - no, the gateway only sees the same list any has the same options as the ocs service + // - we would need to have a list of mountpoints for the shares -> owncloudstorageprovider for hot migration migration + + // best we can do for now is stat the /Shares jail if it is set and return those paths + // if we are in a jail and the current share has been accepted use the stat from the share jail + // Needed because received shares can be jailed in a folder in the users home + + if h.sharePrefix != "/" { + // if we have a mount point use it to build the path + if rs.MountPoint != nil && rs.MountPoint.Path != "" { + // override path with info from share jail + data.FileTarget = path.Join(h.sharePrefix, rs.MountPoint.Path) + data.Path = path.Join(h.sharePrefix, rs.MountPoint.Path) + } else { + data.FileTarget = path.Join(h.sharePrefix, path.Base(info.Path)) + data.Path = path.Join(h.sharePrefix, path.Base(info.Path)) } - output <- data + } else { + data.FileTarget = info.Path + data.Path = info.Path } - }(ctx, client, input, output, &wg) - } + } else { + // not accepted shares need their Path jailed to make the testsuite happy - for _, share := range lrsRes.GetShares() { - input <- share - } - close(input) - wg.Wait() - close(output) + if h.sharePrefix != "/" { + data.Path = path.Join("/", path.Base(info.Path)) + } + + } - for s := range output { - shares = append(shares, s) + shares = append(shares, data) + log.Debug().Msgf("share: %+v", *data) } response.WriteOCSSuccess(w, r, shares) diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go index e482810e32..335e0b9448 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go @@ -19,11 +19,8 @@ package shares import ( - "context" "net/http" - "sync" - 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" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" @@ -193,65 +190,45 @@ func (h *Handler) listUserShares(r *http.Request, filters []*collaboration.Filte } ocsDataPayload := make([]*conversions.ShareData, 0) - client, err := h.getClient() - if err != nil { - return ocsDataPayload, nil, err - } - - // do list shares request. filtered - lsUserSharesResponse, err := client.ListShares(ctx, &lsUserSharesRequest) - if err != nil { - return ocsDataPayload, nil, err - } - if lsUserSharesResponse.Status.Code != rpc.Code_CODE_OK { - return ocsDataPayload, lsUserSharesResponse.Status, nil - } - - var wg sync.WaitGroup - workers := 50 - input := make(chan *collaboration.Share, len(lsUserSharesResponse.Shares)) - output := make(chan *conversions.ShareData, len(lsUserSharesResponse.Shares)) - - for i := 0; i < workers; i++ { - wg.Add(1) - go func(ctx context.Context, client gateway.GatewayAPIClient, input chan *collaboration.Share, output chan *conversions.ShareData, wg *sync.WaitGroup) { - defer wg.Done() - - // build OCS response payload - for s := range input { - data, err := conversions.CS3Share2ShareData(ctx, s) - if err != nil { - log.Debug().Interface("share", s).Interface("shareData", data).Err(err).Msg("could not CS3Share2ShareData, skipping") - return - } + if h.gatewayAddr != "" { + // get a connection to the users share provider + client, err := h.getClient() + if err != nil { + return ocsDataPayload, nil, err + } - info, status, err := h.getResourceInfoByID(ctx, client, s.ResourceId) - if err != nil || status.Code != rpc.Code_CODE_OK { - log.Debug().Interface("share", s).Interface("status", status).Interface("shareData", data).Err(err).Msg("could not stat share, skipping") - return - } + // do list shares request. filtered + lsUserSharesResponse, err := client.ListShares(ctx, &lsUserSharesRequest) + if err != nil { + return ocsDataPayload, nil, err + } + if lsUserSharesResponse.Status.Code != rpc.Code_CODE_OK { + return ocsDataPayload, lsUserSharesResponse.Status, nil + } - if err := h.addFileInfo(ctx, data, info); err != nil { - log.Debug().Interface("share", s).Interface("info", info).Interface("shareData", data).Err(err).Msg("could not add file info, skipping") - return - } - h.mapUserIds(ctx, client, data) + // build OCS response payload + for _, s := range lsUserSharesResponse.Shares { + data, err := conversions.CS3Share2ShareData(ctx, s) + if err != nil { + log.Debug().Interface("share", s).Interface("shareData", data).Err(err).Msg("could not CS3Share2ShareData, skipping") + continue + } - log.Debug().Interface("share", s).Interface("info", info).Interface("shareData", data).Msg("mapped") - output <- data + info, status, err := h.getResourceInfoByID(ctx, client, s.ResourceId) + if err != nil || status.Code != rpc.Code_CODE_OK { + log.Debug().Interface("share", s).Interface("status", status).Interface("shareData", data).Err(err).Msg("could not stat share, skipping") + continue } - }(ctx, client, input, output, &wg) - } - for _, share := range lsUserSharesResponse.Shares { - input <- share - } - close(input) - wg.Wait() - close(output) + if err := h.addFileInfo(ctx, data, info); err != nil { + log.Debug().Interface("share", s).Interface("info", info).Interface("shareData", data).Err(err).Msg("could not add file info, skipping") + continue + } + h.mapUserIds(ctx, client, data) - for s := range output { - ocsDataPayload = append(ocsDataPayload, s) + log.Debug().Interface("share", s).Interface("info", info).Interface("shareData", data).Msg("mapped") + ocsDataPayload = append(ocsDataPayload, data) + } } return ocsDataPayload, nil, nil From db79d547118cad6ab0a7fb3030900a8066907579 Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Sun, 1 May 2022 12:08:15 +0200 Subject: [PATCH 3/5] add changelog --- changelog/unreleased/fix-space-shares.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog/unreleased/fix-space-shares.md diff --git a/changelog/unreleased/fix-space-shares.md b/changelog/unreleased/fix-space-shares.md new file mode 100644 index 0000000000..894325a689 --- /dev/null +++ b/changelog/unreleased/fix-space-shares.md @@ -0,0 +1,5 @@ +Bugfix: Fix the resource id handling for space shares + +Adapt the space shares to the new id format. + +https://github.com/cs3org/reva/pull/2802 From 831b177b67592a052acea07fef5be4746299c6fc Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Sun, 1 May 2022 12:26:28 +0200 Subject: [PATCH 4/5] simplify logic --- internal/grpc/services/gateway/usershareprovider.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/grpc/services/gateway/usershareprovider.go b/internal/grpc/services/gateway/usershareprovider.go index d29af8f887..85e1d2519d 100644 --- a/internal/grpc/services/gateway/usershareprovider.go +++ b/internal/grpc/services/gateway/usershareprovider.go @@ -780,10 +780,7 @@ func refIsSpaceRoot(ref *provider.ResourceId) bool { return false } sid, _ := resourceid.StorageIDUnwrap(ref.GetStorageId()) - if sid != ref.OpaqueId { - return false - } - return true + return sid == ref.OpaqueId } func shareIsSpaceRoot(key *collaboration.ShareKey) bool { From 49e5e02dd98b8b879fcce92156747972a395183b Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Mon, 2 May 2022 09:03:30 +0200 Subject: [PATCH 5/5] fix nextcloud mock --- pkg/storage/fs/nextcloud/nextcloud_server_mock.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/storage/fs/nextcloud/nextcloud_server_mock.go b/pkg/storage/fs/nextcloud/nextcloud_server_mock.go index b6d06cc44f..06b71528f4 100644 --- a/pkg/storage/fs/nextcloud/nextcloud_server_mock.go +++ b/pkg/storage/fs/nextcloud/nextcloud_server_mock.go @@ -65,8 +65,8 @@ var responses = map[string]Response{ `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/CreateHome `: {200, ``, serverStateHome}, `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/CreateHome {}`: {200, ``, serverStateHome}, - `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/CreateStorageSpace {"owner":{"id":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"username":"einstein"},"type":"personal","name":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c"}`: {200, `{"status":{"code":1}}`, serverStateHome}, - `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/CreateStorageSpace {"owner":{"id":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"username":"einstein"},"type":"personal"}`: {200, `{"status":{"code":1}}`, serverStateHome}, + `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/CreateStorageSpace {"owner":{"id":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"username":"einstein"},"type":"personal","name":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c"}`: {200, `{"status":{"code":1},"storage_space":{"id":{"opaque_id":"some-opaque-storage-space-id"},"owner":{"id":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}},"root":{"storage_id":"some-storage-ud","opaque_id":"some-opaque-root-id"},"name":"My Storage Space","quota":{"quota_max_bytes":456,"quota_max_files":123},"space_type":"home","mtime":{"seconds":1234567890}}}`, serverStateHome}, + `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/CreateStorageSpace {"owner":{"id":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1},"username":"einstein"},"type":"personal"}`: {200, `{"status":{"code":1},"storage_space":{"id":{"opaque_id":"some-opaque-storage-space-id"},"owner":{"id":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}},"root":{"storage_id":"some-storage-ud","opaque_id":"some-opaque-root-id"},"name":"My Storage Space","quota":{"quota_max_bytes":456,"quota_max_files":123},"space_type":"home","mtime":{"seconds":1234567890}}}`, serverStateHome}, `POST /apps/sciencemesh/~f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c/api/storage/CreateReference {"path":"/Shares/reference","url":"scheme://target"}`: {200, `[]`, serverStateReference},