diff --git a/changelog/unreleased/datatx-createtransfer.md b/changelog/unreleased/datatx-createtransfer.md new file mode 100644 index 0000000000..9fc6e36376 --- /dev/null +++ b/changelog/unreleased/datatx-createtransfer.md @@ -0,0 +1,5 @@ +Enhancement: Create transfer type share + +`transfer-create` creates a share of type transfer. + +https://github.com/cs3org/reva/pull/1725 diff --git a/cmd/reva/ocm-share-list-received.go b/cmd/reva/ocm-share-list-received.go index 83a1122086..851264c7b9 100644 --- a/cmd/reva/ocm-share-list-received.go +++ b/cmd/reva/ocm-share-list-received.go @@ -55,13 +55,13 @@ func ocmShareListReceivedCommand() *command { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Permissions", "Type", - "Grantee.Idp", "Grantee.OpaqueId", "Created", "Updated", "State"}) + "Grantee.Idp", "Grantee.OpaqueId", "Created", "Updated", "State", "ShareType"}) for _, s := range shareRes.Shares { t.AppendRows([]table.Row{ {s.Share.Id.OpaqueId, s.Share.Owner.Idp, s.Share.Owner.OpaqueId, s.Share.ResourceId.String(), s.Share.Permissions.String(), s.Share.Grantee.Type.String(), s.Share.Grantee.GetUserId().Idp, s.Share.Grantee.GetUserId().OpaqueId, time.Unix(int64(s.Share.Ctime.Seconds), 0), - time.Unix(int64(s.Share.Mtime.Seconds), 0), s.State.String()}, + time.Unix(int64(s.Share.Mtime.Seconds), 0), s.State.String(), s.Share.ShareType.String()}, }) } t.Render() diff --git a/cmd/reva/ocm-share-list.go b/cmd/reva/ocm-share-list.go index 9229b87e60..06b8836c9b 100644 --- a/cmd/reva/ocm-share-list.go +++ b/cmd/reva/ocm-share-list.go @@ -83,12 +83,12 @@ func ocmShareListCommand() *command { t := table.NewWriter() t.SetOutputMirror(os.Stdout) t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Permissions", "Type", - "Grantee.Idp", "Grantee.OpaqueId", "Created", "Updated"}) + "ShareType", "Grantee.Idp", "Grantee.OpaqueId", "Created", "Updated"}) for _, s := range shareRes.Shares { t.AppendRows([]table.Row{ {s.Id.OpaqueId, s.Owner.Idp, s.Owner.OpaqueId, s.ResourceId.String(), s.Permissions.String(), - s.Grantee.Type.String(), s.Grantee.GetUserId().Idp, s.Grantee.GetUserId().OpaqueId, + s.Grantee.Type.String(), s.ShareType.String(), s.Grantee.GetUserId().Idp, s.Grantee.GetUserId().OpaqueId, time.Unix(int64(s.Ctime.Seconds), 0), time.Unix(int64(s.Mtime.Seconds), 0)}, }) } diff --git a/cmd/reva/transfer-create.go b/cmd/reva/transfer-create.go index e77ba8df45..308c1b5438 100644 --- a/cmd/reva/transfer-create.go +++ b/cmd/reva/transfer-create.go @@ -19,24 +19,45 @@ package main import ( - "errors" "io" + "os" + "strconv" + "strings" + "time" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" + ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" - tx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/jedib0t/go-pretty/table" + "github.com/pkg/errors" ) func transferCreateCommand() *command { cmd := newCommand("transfer-create") - cmd.Description = func() string { return "create transfer between 2 remotes" } - cmd.Usage = func() string { return "Usage: transfer-create [-flags]" } + cmd.Description = func() string { return "create transfer between 2 sites" } + cmd.Usage = func() string { return "Usage: transfer-create [-flags] " } grantee := cmd.String("grantee", "", "the grantee, receiver of the transfer") + granteeType := cmd.String("granteeType", "user", "the grantee type, one of: user, group") + idp := cmd.String("idp", "", "the idp of the grantee, default to same idp as the user triggering the action") cmd.Action = func(w ...io.Writer) error { - // validate flags + if cmd.NArg() < 1 { + return errors.New("Invalid arguments: " + cmd.Usage()) + } + if *grantee == "" { return errors.New("Grantee cannot be empty: use -grantee flag\n" + cmd.Usage()) } + if *idp == "" { + return errors.New("Idp cannot be empty: use -idp flag\n" + cmd.Usage()) + } + + // the resource to transfer; the path + fn := cmd.Args()[0] ctx := getAuthContext() client, err := getClient() @@ -44,17 +65,107 @@ func transferCreateCommand() *command { return err } - transferRequest := &tx.CreateTransferRequest{} + // check if invitation has been accepted + acceptedUserRes, err := client.GetAcceptedUser(ctx, &invitepb.GetAcceptedUserRequest{ + RemoteUserId: &userpb.UserId{OpaqueId: *grantee, Idp: *idp}, + }) + if err != nil { + return err + } + if acceptedUserRes.Status.Code != rpc.Code_CODE_OK { + return formatError(acceptedUserRes.Status) + } - transferResponse, err := client.CreateTransfer(ctx, transferRequest) + // verify resource stats + statReq := &provider.StatRequest{ + Ref: &provider.Reference{ + Spec: &provider.Reference_Path{ + Path: fn, + }, + }, + } + statRes, err := client.Stat(ctx, statReq) if err != nil { return err } - if transferResponse.Status.Code != rpc.Code_CODE_OK { - return formatError(transferResponse.Status) + if statRes.Status.Code != rpc.Code_CODE_OK { + return formatError(statRes.Status) } + providerInfoResp, err := client.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{ + Domain: *idp, + }) + if err != nil { + return err + } + + resourcePermissions, pint, err := getOCMSharePerm(editorPermission) + if err != nil { + return err + } + + gt := provider.GranteeType_GRANTEE_TYPE_USER + if strings.ToLower(*granteeType) == "group" { + gt = provider.GranteeType_GRANTEE_TYPE_GROUP + } + + createShareReq := &ocm.CreateOCMShareRequest{ + Opaque: &types.Opaque{ + Map: map[string]*types.OpaqueEntry{ + "permissions": &types.OpaqueEntry{ + Decoder: "plain", + Value: []byte(strconv.Itoa(pint)), + }, + "name": &types.OpaqueEntry{ + Decoder: "plain", + Value: []byte(statRes.Info.Path), + }, + "protocol": &types.OpaqueEntry{ + Decoder: "plain", + Value: []byte("datatx"), + }, + }, + }, + ResourceId: statRes.Info.Id, + Grant: &ocm.ShareGrant{ + Grantee: &provider.Grantee{ + Type: gt, + Id: &provider.Grantee_UserId{ + UserId: &userpb.UserId{ + Idp: *idp, + OpaqueId: *grantee, + }, + }, + }, + Permissions: resourcePermissions, + }, + RecipientMeshProvider: providerInfoResp.ProviderInfo, + } + + createShareResponse, err := client.CreateOCMShare(ctx, createShareReq) + if err != nil { + return err + } + if createShareResponse.Status.Code != rpc.Code_CODE_OK { + if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND { + return formatError(statRes.Status) + } + return err + } + + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Permissions", "Type", "Grantee.Idp", "Grantee.OpaqueId", "ShareType", "Created", "Updated"}) + + s := createShareResponse.Share + t.AppendRows([]table.Row{ + {s.Id.OpaqueId, s.Owner.Idp, s.Owner.OpaqueId, s.ResourceId.String(), s.Permissions.String(), + s.Grantee.Type.String(), s.Grantee.GetUserId().Idp, s.Grantee.GetUserId().OpaqueId, s.ShareType.String(), + time.Unix(int64(s.Ctime.Seconds), 0), time.Unix(int64(s.Mtime.Seconds), 0)}, + }) + t.Render() return nil } + return cmd } diff --git a/internal/grpc/services/gateway/gateway.go b/internal/grpc/services/gateway/gateway.go index f91678f6bd..80420e04a0 100644 --- a/internal/grpc/services/gateway/gateway.go +++ b/internal/grpc/services/gateway/gateway.go @@ -63,10 +63,11 @@ type config struct { TransferExpires int64 `mapstructure:"transfer_expires"` TokenManager string `mapstructure:"token_manager"` // ShareFolder is the location where to create shares in the recipient's storage provider. - ShareFolder string `mapstructure:"share_folder"` - HomeMapping string `mapstructure:"home_mapping"` - TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"` - EtagCacheTTL int `mapstructure:"etag_cache_ttl"` + ShareFolder string `mapstructure:"share_folder"` + DataTransfersFolder string `mapstructure:"data_transfers_folder"` + HomeMapping string `mapstructure:"home_mapping"` + TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"` + EtagCacheTTL int `mapstructure:"etag_cache_ttl"` } // sets defaults @@ -77,6 +78,10 @@ func (c *config) init() { c.ShareFolder = strings.Trim(c.ShareFolder, "/") + if c.DataTransfersFolder == "" { + c.DataTransfersFolder = "Data-Transfers" + } + if c.TokenManager == "" { c.TokenManager = "jwt" } diff --git a/internal/grpc/services/gateway/ocmshareprovider.go b/internal/grpc/services/gateway/ocmshareprovider.go index 8ee4935f3b..872009d9c1 100644 --- a/internal/grpc/services/gateway/ocmshareprovider.go +++ b/internal/grpc/services/gateway/ocmshareprovider.go @@ -260,7 +260,7 @@ func (s *svc) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceive panic("gateway: error updating a received share: the share is nil") } - createRefStatus, err := s.createWebdavReference(ctx, share.Share) + createRefStatus, err := s.createOCMReference(ctx, share.Share) return &ocm.UpdateReceivedOCMShareResponse{ Status: createRefStatus, }, err @@ -291,7 +291,7 @@ func (s *svc) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedOCMSh return res, nil } -func (s *svc) createWebdavReference(ctx context.Context, share *ocm.Share) (*rpc.Status, error) { +func (s *svc) createOCMReference(ctx context.Context, share *ocm.Share) (*rpc.Status, error) { log := appctx.GetLogger(ctx) @@ -314,17 +314,39 @@ func (s *svc) createWebdavReference(ctx context.Context, share *ocm.Share) (*rpc return status.NewInternal(ctx, err, "error updating received share"), nil } - // reference path is the home path + some name on the corresponding - // mesh provider (/home/MyShares/x) - // It is the responsibility of the gateway to resolve these references and merge the response back - // from the main request. - refPath := path.Join(homeRes.Path, s.c.ShareFolder, path.Base(share.Name)) - log.Info().Msg("mount path will be:" + refPath) + var refPath, targetURI string + if share.ShareType == ocm.Share_SHARE_TYPE_TRANSFER { + createTransferDir, err := s.CreateContainer(ctx, &provider.CreateContainerRequest{ + Ref: &provider.Reference{ + Spec: &provider.Reference_Path{ + Path: path.Join(homeRes.Path, s.c.DataTransfersFolder), + }, + }, + }) + if err != nil { + return status.NewInternal(ctx, err, "error creating transfers directory"), nil + } + if createTransferDir.Status.Code != rpc.Code_CODE_OK && createTransferDir.Status.Code != rpc.Code_CODE_ALREADY_EXISTS { + err := status.NewErrorFromCode(createTransferDir.Status.GetCode(), "gateway") + return status.NewInternal(ctx, err, "error creating transfers directory"), nil + } - createRefReq := &provider.CreateReferenceRequest{ - Path: refPath, + refPath = path.Join(homeRes.Path, s.c.DataTransfersFolder, path.Base(share.Name)) + targetURI = fmt.Sprintf("datatx://%s@%s?name=%s", token, share.Creator.Idp, share.Name) + } else { + // reference path is the home path + some name on the corresponding + // mesh provider (/home/MyShares/x) + // It is the responsibility of the gateway to resolve these references and merge the response back + // from the main request. + refPath = path.Join(homeRes.Path, s.c.ShareFolder, path.Base(share.Name)) // webdav is the scheme, token@host the opaque part and the share name the query of the URL. - TargetUri: fmt.Sprintf("webdav://%s@%s?name=%s", token, share.Creator.Idp, share.Name), + targetURI = fmt.Sprintf("webdav://%s@%s?name=%s", token, share.Creator.Idp, share.Name) + } + + log.Info().Msg("mount path will be:" + refPath) + createRefReq := &provider.CreateReferenceRequest{ + Path: refPath, + TargetUri: targetURI, } c, err := s.findByPath(ctx, refPath) diff --git a/internal/grpc/services/ocmcore/ocmcore.go b/internal/grpc/services/ocmcore/ocmcore.go index f6bbbf6725..81ae633154 100644 --- a/internal/grpc/services/ocmcore/ocmcore.go +++ b/internal/grpc/services/ocmcore/ocmcore.go @@ -172,7 +172,15 @@ func (s *service) CreateOCMCoreShare(ctx context.Context, req *ocmcore.CreateOCM }, } - share, err := s.sm.Share(ctx, resource, grant, req.Name, nil, "", req.Owner, token, ocm.Share_SHARE_TYPE_REGULAR) + var shareType ocm.Share_ShareType + switch req.Protocol.Name { + case "datatx": + shareType = ocm.Share_SHARE_TYPE_TRANSFER + default: + shareType = ocm.Share_SHARE_TYPE_REGULAR + } + + share, err := s.sm.Share(ctx, resource, grant, req.Name, nil, "", req.Owner, token, shareType) if err != nil { return &ocmcore.CreateOCMCoreShareResponse{ Status: status.NewInternal(ctx, err, "error creating ocm core share"), diff --git a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go index 676da090cc..1b4e8676db 100644 --- a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go +++ b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go @@ -144,7 +144,24 @@ func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareReq }, nil } - share, err := s.sm.Share(ctx, req.ResourceId, req.Grant, name, req.RecipientMeshProvider, permissions, nil, "", ocm.Share_SHARE_TYPE_REGULAR) + // discover share type + sharetype := ocm.Share_SHARE_TYPE_REGULAR + protocol, ok := req.Opaque.Map["protocol"] + if ok { + switch protocol.Decoder { + case "plain": + if string(protocol.Value) == "datatx" { + sharetype = ocm.Share_SHARE_TYPE_TRANSFER + } + default: + err := errors.New("protocol decoder not recognized") + return &ocm.CreateOCMShareResponse{ + Status: status.NewInternal(ctx, err, "error creating share"), + }, nil + } + } + + share, err := s.sm.Share(ctx, req.ResourceId, req.Grant, name, req.RecipientMeshProvider, permissions, nil, "", sharetype) if err != nil { return &ocm.CreateOCMShareResponse{ Status: status.NewInternal(ctx, err, "error creating share"), diff --git a/internal/grpc/services/storageprovider/storageprovider.go b/internal/grpc/services/storageprovider/storageprovider.go index bede723ced..7785771fcb 100644 --- a/internal/grpc/services/storageprovider/storageprovider.go +++ b/internal/grpc/services/storageprovider/storageprovider.go @@ -460,7 +460,7 @@ func (s *service) CreateContainer(ctx context.Context, req *provider.CreateConta case errtypes.IsNotFound: st = status.NewNotFound(ctx, "path not found when creating container") case errtypes.AlreadyExists: - st = status.NewInternal(ctx, err, "error: container already exists") + st = status.NewAlreadyExists(ctx, err, "container already exists") case errtypes.PermissionDenied: st = status.NewPermissionDenied(ctx, err, "permission denied") default: diff --git a/pkg/ocm/share/manager/json/json.go b/pkg/ocm/share/manager/json/json.go index b37e0f5450..7a4e9d867a 100644 --- a/pkg/ocm/share/manager/json/json.go +++ b/pkg/ocm/share/manager/json/json.go @@ -269,20 +269,40 @@ func (m *mgr) Share(ctx context.Context, md *provider.ResourceId, g *ocm.ShareGr } if isOwnersMeshProvider { + token, ok := tokenpkg.ContextGetToken(ctx) + if !ok { + return nil, errors.New("Could not get token from context") + } + var protocol []byte + if st == ocm.Share_SHARE_TYPE_TRANSFER { + protocol, err = json.Marshal( + map[string]interface{}{ + "name": "datatx", + "options": map[string]string{ + "permissions": pm, + "token": token, + }, + }, + ) + if err != nil { + err = errors.Wrap(err, "error marshalling protocol data") + return nil, err + } - // Call the remote provider's CreateOCMCoreShare method - protocol, err := json.Marshal( - map[string]interface{}{ - "name": "webdav", - "options": map[string]string{ - "permissions": pm, - "token": tokenpkg.ContextMustGetToken(ctx), + } else { + protocol, err = json.Marshal( + map[string]interface{}{ + "name": "webdav", + "options": map[string]string{ + "permissions": pm, + "token": tokenpkg.ContextMustGetToken(ctx), + }, }, - }, - ) - if err != nil { - err = errors.Wrap(err, "error marshalling protocol data") - return nil, err + ) + if err != nil { + err = errors.Wrap(err, "error marshalling protocol data") + return nil, err + } } requestBody := url.Values{ diff --git a/pkg/rgrpc/status/status.go b/pkg/rgrpc/status/status.go index c8c7dc0f31..5be9c670d0 100644 --- a/pkg/rgrpc/status/status.go +++ b/pkg/rgrpc/status/status.go @@ -124,6 +124,17 @@ func NewUnimplemented(ctx context.Context, err error, msg string) *rpc.Status { } } +// NewAlreadyExists returns a Status with CODE_ALREADY_EXISTS and logs the msg. +func NewAlreadyExists(ctx context.Context, err error, msg string) *rpc.Status { + log := appctx.GetLogger(ctx).With().CallerWithSkipFrameCount(3).Logger() + log.Error().Err(err).Msg(msg) + return &rpc.Status{ + Code: rpc.Code_CODE_ALREADY_EXISTS, + Message: msg, + Trace: getTrace(ctx), + } +} + // NewInvalidArg returns a Status with CODE_INVALID_ARGUMENT. func NewInvalidArg(ctx context.Context, msg string) *rpc.Status { return &rpc.Status{Code: rpc.Code_CODE_INVALID_ARGUMENT, diff --git a/pkg/storage/utils/localfs/localfs.go b/pkg/storage/utils/localfs/localfs.go index d27b4bb91e..3ed3873aba 100644 --- a/pkg/storage/utils/localfs/localfs.go +++ b/pkg/storage/utils/localfs/localfs.go @@ -48,16 +48,17 @@ import ( // Config holds the configuration details for the local fs. type Config struct { - Root string `mapstructure:"root"` - DisableHome bool `mapstructure:"disable_home"` - UserLayout string `mapstructure:"user_layout"` - ShareFolder string `mapstructure:"share_folder"` - Uploads string `mapstructure:"uploads"` - DataDirectory string `mapstructure:"data_directory"` - RecycleBin string `mapstructure:"recycle_bin"` - Versions string `mapstructure:"versions"` - Shadow string `mapstructure:"shadow"` - References string `mapstructure:"references"` + Root string `mapstructure:"root"` + DisableHome bool `mapstructure:"disable_home"` + UserLayout string `mapstructure:"user_layout"` + ShareFolder string `mapstructure:"share_folder"` + DataTransfersFolder string `mapstructure:"data_transfers_folder"` + Uploads string `mapstructure:"uploads"` + DataDirectory string `mapstructure:"data_directory"` + RecycleBin string `mapstructure:"recycle_bin"` + Versions string `mapstructure:"versions"` + Shadow string `mapstructure:"shadow"` + References string `mapstructure:"references"` } func (c *Config) init() { @@ -73,6 +74,10 @@ func (c *Config) init() { c.ShareFolder = "/MyShares" } + if c.DataTransfersFolder == "" { + c.DataTransfersFolder = "/Data-Transfers" + } + // ensure share folder always starts with slash c.ShareFolder = path.Join("/", c.ShareFolder) @@ -244,6 +249,10 @@ func (fs *localfs) isShareFolder(ctx context.Context, p string) bool { return strings.HasPrefix(p, fs.conf.ShareFolder) } +func (fs *localfs) isDataTransfersFolder(ctx context.Context, p string) bool { + return strings.HasPrefix(p, fs.conf.DataTransfersFolder) +} + func (fs *localfs) isShareFolderRoot(ctx context.Context, p string) bool { return path.Clean(p) == fs.conf.ShareFolder } @@ -518,12 +527,16 @@ func (fs *localfs) UpdateGrant(ctx context.Context, ref *provider.Reference, g * } func (fs *localfs) CreateReference(ctx context.Context, path string, targetURI *url.URL) error { - if !fs.isShareFolder(ctx, path) { - return errtypes.PermissionDenied("localfs: cannot create references outside the share folder") + var fn string + switch { + case fs.isShareFolder(ctx, path): + fn = fs.wrapReferences(ctx, path) + case fs.isDataTransfersFolder(ctx, path): + fn = fs.wrap(ctx, path) + default: + return errtypes.PermissionDenied("localfs: cannot create references outside the share folder and data transfers folder") } - fn := fs.wrapReferences(ctx, path) - err := os.MkdirAll(fn, 0700) if err != nil { if os.IsNotExist(err) {