diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 6755f07a791..d719eab8279 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -131,23 +131,23 @@ func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSp if id != nil { // query that specific story provider - parts := strings.SplitN(id.OpaqueId, "!", 2) - if len(parts) != 2 { - return &provider.ListStorageSpacesResponse{ - Status: status.NewInvalidArg(ctx, "space id must be separated by !"), - }, nil - } + parts := strings.SplitN(id.OpaqueId, "!", 2) + if len(parts) != 2 { + return &provider.ListStorageSpacesResponse{ + Status: status.NewInvalidArg(ctx, "space id must be separated by !"), + }, nil + } res, err := c.GetStorageProviders(ctx, ®istry.GetStorageProvidersRequest{ Ref: &provider.Reference{ResourceId: &provider.ResourceId{ StorageId: parts[0], // FIXME REFERENCE the StorageSpaceId is a storageid + an opaqueid - OpaqueId: parts[1], + OpaqueId: parts[1], }}, }) - if err != nil { - return &provider.ListStorageSpacesResponse{ + if err != nil { + return &provider.ListStorageSpacesResponse{ Status: status.NewStatusFromErrType(ctx, "ListStorageSpaces filters: req "+req.String(), err), - }, nil - } + }, nil + } if res.Status.Code != rpc.Code_CODE_OK { return &provider.ListStorageSpacesResponse{ Status: res.Status, @@ -158,8 +158,8 @@ func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSp // get list of all storage providers res, err := c.ListStorageProviders(ctx, ®istry.ListStorageProvidersRequest{}) - if err != nil { - return &provider.ListStorageSpacesResponse{ + if err != nil { + return &provider.ListStorageSpacesResponse{ Status: status.NewStatusFromErrType(ctx, "error listing providers", err), }, nil } @@ -209,8 +209,8 @@ func (s *svc) ListStorageSpaces(ctx context.Context, req *provider.ListStorageSp return &provider.ListStorageSpacesResponse{ Status: status.NewOK(ctx), StorageSpaces: spaces, - }, nil - } + }, nil +} func (s *svc) listStorageSpacesOnProvider(ctx context.Context, req *provider.ListStorageSpacesRequest, res *[]*provider.StorageSpace, p *registry.ProviderInfo, e *error, wg *sync.WaitGroup) { defer wg.Done() @@ -291,6 +291,11 @@ func (s *svc) getHome(_ context.Context) string { func (s *svc) InitiateFileDownload(ctx context.Context, req *provider.InitiateFileDownloadRequest) (*gateway.InitiateFileDownloadResponse, error) { log := appctx.GetLogger(ctx) + + if utils.IsRelativeReference(req.Ref) { + return s.initiateFileDownload(ctx, req) + } + p, st := s.getPath(ctx, req.Ref) if st.Code != rpc.Code_CODE_OK { return &gateway.InitiateFileDownloadResponse{ @@ -501,6 +506,9 @@ func (s *svc) initiateFileDownload(ctx context.Context, req *provider.InitiateFi func (s *svc) InitiateFileUpload(ctx context.Context, req *provider.InitiateFileUploadRequest) (*gateway.InitiateFileUploadResponse, error) { log := appctx.GetLogger(ctx) + if utils.IsRelativeReference(req.Ref) { + return s.initiateFileUpload(ctx, req) + } p, st := s.getPath(ctx, req.Ref) if st.Code != rpc.Code_CODE_OK { return &gateway.InitiateFileUploadResponse{ @@ -723,6 +731,11 @@ func (s *svc) GetPath(ctx context.Context, req *provider.GetPathRequest) (*provi func (s *svc) CreateContainer(ctx context.Context, req *provider.CreateContainerRequest) (*provider.CreateContainerResponse, error) { log := appctx.GetLogger(ctx) + + if utils.IsRelativeReference(req.Ref) { + return s.createContainer(ctx, req) + } + p, st := s.getPath(ctx, req.Ref) if st.Code != rpc.Code_CODE_OK { return &provider.CreateContainerResponse{ @@ -1167,14 +1180,19 @@ func (s *svc) stat(ctx context.Context, req *provider.StatRequest) (*provider.St } resPath := req.Ref.GetPath() - if len(providers) == 1 && (resPath == "" || strings.HasPrefix(resPath, providers[0].ProviderPath)) { + if len(providers) == 1 && (utils.IsRelativeReference(req.Ref) || resPath == "" || strings.HasPrefix(resPath, providers[0].ProviderPath)) { c, err := s.getStorageProviderClient(ctx, providers[0]) if err != nil { return &provider.StatResponse{ Status: status.NewInternal(ctx, err, "error connecting to storage provider="+providers[0].Address), }, nil } - return c.Stat(ctx, req) + rsp, err := c.Stat(ctx, req) + if err != nil || rsp.Status.Code != rpc.Code_CODE_OK { + return rsp, err + } + + return rsp, nil } infoFromProviders := make([]*provider.ResourceInfo, len(providers)) @@ -1222,12 +1240,16 @@ func (s *svc) statOnProvider(ctx context.Context, req *provider.StatRequest, res return } - resPath := path.Clean(req.Ref.GetPath()) - newPath := req.Ref.GetPath() - if resPath != "" && !strings.HasPrefix(resPath, p.ProviderPath) { - newPath = p.ProviderPath + if utils.IsAbsoluteReference(req.Ref) { + resPath := path.Clean(req.Ref.GetPath()) + newPath := req.Ref.GetPath() + if resPath != "" && !strings.HasPrefix(resPath, p.ProviderPath) { + newPath = p.ProviderPath + } + req.Ref = &provider.Reference{Path: newPath} } - r, err := c.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{Path: newPath}}) + + r, err := c.Stat(ctx, req) if err != nil { *e = errors.Wrap(err, "gateway: error calling ListContainer") return @@ -1239,6 +1261,11 @@ func (s *svc) statOnProvider(ctx context.Context, req *provider.StatRequest, res } func (s *svc) Stat(ctx context.Context, req *provider.StatRequest) (*provider.StatResponse, error) { + + if utils.IsRelativeReference(req.Ref) { + return s.stat(ctx, req) + } + p, st := s.getPath(ctx, req.Ref, req.ArbitraryMetadataKeys...) if st.Code != rpc.Code_CODE_OK { return &provider.StatResponse{ @@ -1538,6 +1565,7 @@ func (s *svc) listContainer(ctx context.Context, req *provider.ListContainerRequ infos := []*provider.ResourceInfo{} indirects := make(map[string][]*provider.ResourceInfo) + trimPrefix := utils.IsAbsoluteReference(req.Ref) for i := range providers { if errors[i] != nil { return &provider.ListContainerResponse{ @@ -1545,7 +1573,7 @@ func (s *svc) listContainer(ctx context.Context, req *provider.ListContainerRequ }, nil } for _, inf := range infoFromProviders[i] { - if parent := path.Dir(inf.Path); resPath != "" && resPath != parent { + if parent := path.Dir(inf.Path); trimPrefix && resPath != "." && resPath != parent { parts := strings.Split(strings.TrimPrefix(inf.Path, resPath), "/") p := path.Join(resPath, parts[1]) indirects[p] = append(indirects[p], inf) @@ -1583,12 +1611,16 @@ func (s *svc) listContainerOnProvider(ctx context.Context, req *provider.ListCon return } - resPath := path.Clean(req.Ref.GetPath()) - newPath := req.Ref.GetPath() - if resPath != "" && !strings.HasPrefix(resPath, p.ProviderPath) { - newPath = p.ProviderPath + if utils.IsAbsoluteReference(req.Ref) { + resPath := path.Clean(req.Ref.GetPath()) + newPath := req.Ref.GetPath() + if resPath != "" && !strings.HasPrefix(resPath, p.ProviderPath) { + newPath = p.ProviderPath + } + req.Ref = &provider.Reference{Path: newPath} } - r, err := c.ListContainer(ctx, &provider.ListContainerRequest{Ref: &provider.Reference{Path: newPath}}) + + r, err := c.ListContainer(ctx, req) if err != nil { *e = errors.Wrap(err, "gateway: error calling ListContainer") return @@ -1598,6 +1630,11 @@ func (s *svc) listContainerOnProvider(ctx context.Context, req *provider.ListCon func (s *svc) ListContainer(ctx context.Context, req *provider.ListContainerRequest) (*provider.ListContainerResponse, error) { log := appctx.GetLogger(ctx) + + if utils.IsRelativeReference(req.Ref) { + return s.listContainer(ctx, req) + } + p, st := s.getPath(ctx, req.Ref, req.ArbitraryMetadataKeys...) if st.Code != rpc.Code_CODE_OK { return &provider.ListContainerResponse{ @@ -1786,7 +1823,7 @@ func (s *svc) getPath(ctx context.Context, ref *provider.Reference, keys ...stri return res.Info.Path, res.Status } - if ref.Path != "" { + if ref.ResourceId == nil && strings.HasPrefix(ref.Path, "/") { return ref.Path, &rpc.Status{Code: rpc.Code_CODE_OK} } return "", &rpc.Status{Code: rpc.Code_CODE_INTERNAL} diff --git a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go index b53f5adf070..8dba95b7d33 100644 --- a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go +++ b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go @@ -577,7 +577,7 @@ func (s *service) unwrap(ctx context.Context, ref *provider.Reference) (token st return "", "", errtypes.BadRequest("need absolute path ref: got " + ref.String()) } - if ref.GetPath() == "" { + if !strings.HasPrefix(ref.GetPath(), "/") { // abort, no valid id nor path return "", "", errtypes.BadRequest("invalid ref: " + ref.String()) } diff --git a/internal/grpc/services/storageprovider/storageprovider.go b/internal/grpc/services/storageprovider/storageprovider.go index 0fe2a3600f6..2981c26847e 100644 --- a/internal/grpc/services/storageprovider/storageprovider.go +++ b/internal/grpc/services/storageprovider/storageprovider.go @@ -38,6 +38,7 @@ import ( "github.com/cs3org/reva/pkg/rgrpc/status" "github.com/cs3org/reva/pkg/storage" "github.com/cs3org/reva/pkg/storage/fs/registry" + "github.com/cs3org/reva/pkg/utils" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "go.opencensus.io/trace" @@ -196,7 +197,7 @@ func registerMimeTypes(mimes map[string]string) { } func (s *service) SetArbitraryMetadata(ctx context.Context, req *provider.SetArbitraryMetadataRequest) (*provider.SetArbitraryMetadataResponse, error) { - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) if err != nil { err := errors.Wrap(err, "storageprovidersvc: error unwrapping path") return &provider.SetArbitraryMetadataResponse{ @@ -226,7 +227,7 @@ func (s *service) SetArbitraryMetadata(ctx context.Context, req *provider.SetArb } func (s *service) UnsetArbitraryMetadata(ctx context.Context, req *provider.UnsetArbitraryMetadataRequest) (*provider.UnsetArbitraryMetadataResponse, error) { - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) if err != nil { err := errors.Wrap(err, "storageprovidersvc: error unwrapping path") return &provider.UnsetArbitraryMetadataResponse{ @@ -262,40 +263,49 @@ func (s *service) InitiateFileDownload(ctx context.Context, req *provider.Initia // For example, https://data-server.example.org/home/docs/myfile.txt // or ownclouds://data-server.example.org/home/docs/myfile.txt log := appctx.GetLogger(ctx) + u := *s.dataServerURL - newRef, err := s.unwrap(ctx, req.Ref) - if err != nil { - return &provider.InitiateFileDownloadResponse{ - Status: status.NewInternal(ctx, err, "error unwrapping path"), - }, nil + log.Info().Str("data-server", u.String()).Interface("ref", req.Ref).Msg("file download") + + protocol := &provider.FileDownloadProtocol{Expose: s.conf.ExposeDataServer} + + if utils.IsRelativeReference(req.Ref) { + protocol.Protocol = "spaces" + u.Path = path.Join(u.Path, "spaces", req.Ref.ResourceId.StorageId+"!"+req.Ref.ResourceId.OpaqueId, req.Ref.Path) + } else { + ctx, newRef, err := s.unwrap(ctx, req.Ref) + if err != nil { + return &provider.InitiateFileDownloadResponse{ + Status: status.NewInternal(ctx, err, "error unwrapping path"), + }, nil + } + // Currently, we only support the simple protocol for GET requests + // Once we have multiple protocols, this would be moved to the fs layer + protocol.Protocol = "simple" + u.Path = path.Join(u.Path, "simple", newRef.GetPath()) } - // Currently, we only support the simple protocol for GET requests - // Once we have multiple protocols, this would be moved to the fs layer - u.Path = path.Join(u.Path, "simple", newRef.GetPath()) + protocol.DownloadEndpoint = u.String() - log.Info().Str("data-server", u.String()).Str("fn", req.Ref.GetPath()).Msg("file download") - res := &provider.InitiateFileDownloadResponse{ - Protocols: []*provider.FileDownloadProtocol{ - &provider.FileDownloadProtocol{ - Protocol: "simple", - DownloadEndpoint: u.String(), - Expose: s.conf.ExposeDataServer, - }, - }, - Status: status.NewOK(ctx), - } - return res, nil + return &provider.InitiateFileDownloadResponse{ + Protocols: []*provider.FileDownloadProtocol{protocol}, + Status: status.NewOK(ctx), + }, nil } func (s *service) InitiateFileUpload(ctx context.Context, req *provider.InitiateFileUploadRequest) (*provider.InitiateFileUploadResponse, error) { // TODO(labkode): same considerations as download log := appctx.GetLogger(ctx) - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) if err != nil { - return &provider.InitiateFileUploadResponse{ - Status: status.NewInternal(ctx, err, "error unwrapping path"), - }, nil + switch err.(type) { + case errtypes.IsNotFound: + newRef = req.Ref + default: + return &provider.InitiateFileUploadResponse{ + Status: status.NewInternal(ctx, err, "error unwrapping path"), + }, nil + } } if newRef.GetPath() == "/" { return &provider.InitiateFileUploadResponse{ @@ -483,14 +493,42 @@ func (s *service) DeleteStorageSpace(ctx context.Context, req *provider.DeleteSt } func (s *service) CreateContainer(ctx context.Context, req *provider.CreateContainerRequest) (*provider.CreateContainerResponse, error) { - newRef, err := s.unwrap(ctx, req.Ref) + var err error + var parentRef *provider.Reference + var name string + switch { + case utils.IsRelativeReference(req.Ref): + req.Ref.Path = path.Dir(req.Ref.Path) + name = path.Base(req.Ref.Path) + case utils.IsAbsoluteReference(req.Ref): + ctx, ref, err := s.unwrap(ctx, req.Ref) + if err != nil { + return &provider.CreateContainerResponse{ + Status: status.NewInternal(ctx, err, "error unwrapping path"), + }, nil + } + parentRef = &provider.Reference{Path: path.Dir(ref.GetPath())} + name = path.Base(ref.GetPath()) + default: + return &provider.CreateContainerResponse{ + Status: status.NewInvalidArg(ctx, "invalid reference, name required"), + }, nil + } + var st *rpc.Status if err != nil { + switch err.(type) { + case errtypes.IsNotFound: + st = status.NewNotFound(ctx, "path not found when unwrapping") + case errtypes.PermissionDenied: + st = status.NewPermissionDenied(ctx, err, "permission denied") + default: + st = status.NewInternal(ctx, err, "error unwrapping: "+req.String()) + } return &provider.CreateContainerResponse{ - Status: status.NewInternal(ctx, err, "error unwrapping path"), + Status: st, }, nil } - - if err := s.storage.CreateDir(ctx, newRef.GetPath()); err != nil { + if err := s.storage.CreateDir(ctx, parentRef, name); err != nil { var st *rpc.Status switch err.(type) { case errtypes.IsNotFound: @@ -514,7 +552,7 @@ func (s *service) CreateContainer(ctx context.Context, req *provider.CreateConta } func (s *service) Delete(ctx context.Context, req *provider.DeleteRequest) (*provider.DeleteResponse, error) { - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) if err != nil { return &provider.DeleteResponse{ Status: status.NewInternal(ctx, err, "error unwrapping path"), @@ -548,17 +586,22 @@ func (s *service) Delete(ctx context.Context, req *provider.DeleteRequest) (*pro } func (s *service) Move(ctx context.Context, req *provider.MoveRequest) (*provider.MoveResponse, error) { - sourceRef, err := s.unwrap(ctx, req.Source) + ctx, sourceRef, err := s.unwrap(ctx, req.Source) if err != nil { return &provider.MoveResponse{ Status: status.NewInternal(ctx, err, "error unwrapping source path"), }, nil } - targetRef, err := s.unwrap(ctx, req.Destination) + ctx, targetRef, err := s.unwrap(ctx, req.Destination) if err != nil { - return &provider.MoveResponse{ - Status: status.NewInternal(ctx, err, "error unwrapping destination path"), - }, nil + switch err.(type) { + case errtypes.IsNotFound: + targetRef = req.Destination + default: + return &provider.MoveResponse{ + Status: status.NewInternal(ctx, err, "error unwrapping destination path"), + }, nil + } } if err := s.storage.Move(ctx, sourceRef, targetRef); err != nil { @@ -590,16 +633,24 @@ func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provide trace.StringAttribute("ref", req.Ref.String()), ) - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) + var st *rpc.Status if err != nil { + switch err.(type) { + case errtypes.IsNotFound: + st = status.NewNotFound(ctx, "path not found when unwrapping") + case errtypes.PermissionDenied: + st = status.NewPermissionDenied(ctx, err, "permission denied") + default: + st = status.NewInternal(ctx, err, "error unwrapping: "+req.String()) + } return &provider.StatResponse{ - Status: status.NewInternal(ctx, err, "error unwrapping path"), + Status: st, }, nil } md, err := s.storage.GetMD(ctx, newRef, req.ArbitraryMetadataKeys) if err != nil { - var st *rpc.Status switch err.(type) { case errtypes.IsNotFound: st = status.NewNotFound(ctx, "path not found when stating") @@ -613,7 +664,7 @@ func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provide }, nil } - if err := s.wrap(ctx, md); err != nil { + if err := s.wrap(ctx, md, utils.IsAbsoluteReference(req.Ref)); err != nil { return &provider.StatResponse{ Status: status.NewInternal(ctx, err, "error wrapping path"), }, nil @@ -629,7 +680,7 @@ func (s *service) ListContainerStream(req *provider.ListContainerStreamRequest, ctx := ss.Context() log := appctx.GetLogger(ctx) - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) if err != nil { res := &provider.ListContainerStreamResponse{ Status: status.NewInternal(ctx, err, "error unwrapping path"), @@ -662,8 +713,9 @@ func (s *service) ListContainerStream(req *provider.ListContainerStreamRequest, return nil } + prefixMountpoint := utils.IsAbsoluteReference(req.Ref) for _, md := range mds { - if err := s.wrap(ctx, md); err != nil { + if err := s.wrap(ctx, md, prefixMountpoint); err != nil { res := &provider.ListContainerStreamResponse{ Status: status.NewInternal(ctx, err, "error wrapping path"), } @@ -687,7 +739,7 @@ func (s *service) ListContainerStream(req *provider.ListContainerStreamRequest, } func (s *service) ListContainer(ctx context.Context, req *provider.ListContainerRequest) (*provider.ListContainerResponse, error) { - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) if err != nil { return &provider.ListContainerResponse{ Status: status.NewInternal(ctx, err, "error unwrapping path"), @@ -711,8 +763,9 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer } var infos = make([]*provider.ResourceInfo, 0, len(mds)) + prefixMountpoint := utils.IsAbsoluteReference(req.Ref) for _, md := range mds { - if err := s.wrap(ctx, md); err != nil { + if err := s.wrap(ctx, md, prefixMountpoint); err != nil { return &provider.ListContainerResponse{ Status: status.NewInternal(ctx, err, "error wrapping path"), }, nil @@ -727,7 +780,7 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer } func (s *service) ListFileVersions(ctx context.Context, req *provider.ListFileVersionsRequest) (*provider.ListFileVersionsResponse, error) { - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) if err != nil { return &provider.ListFileVersionsResponse{ Status: status.NewInternal(ctx, err, "error unwrapping path"), @@ -758,7 +811,7 @@ func (s *service) ListFileVersions(ctx context.Context, req *provider.ListFileVe } func (s *service) RestoreFileVersion(ctx context.Context, req *provider.RestoreFileVersionRequest) (*provider.RestoreFileVersionResponse, error) { - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) if err != nil { return &provider.RestoreFileVersionResponse{ Status: status.NewInternal(ctx, err, "error unwrapping path"), @@ -875,7 +928,7 @@ func (s *service) RestoreRecycleItem(ctx context.Context, req *provider.RestoreR func (s *service) PurgeRecycle(ctx context.Context, req *provider.PurgeRecycleRequest) (*provider.PurgeRecycleResponse, error) { // if a key was sent as opaque id purge only that item - if req.GetRef().GetResourceId() != nil && req.GetRef().GetResourceId().OpaqueId != "" { + if req.GetRef() != nil && req.GetRef().GetResourceId() != nil && req.GetRef().GetResourceId().OpaqueId != "" { if err := s.storage.PurgeRecycleItem(ctx, req.GetRef().GetResourceId().OpaqueId); err != nil { var st *rpc.Status switch err.(type) { @@ -913,7 +966,7 @@ func (s *service) PurgeRecycle(ctx context.Context, req *provider.PurgeRecycleRe } func (s *service) ListGrants(ctx context.Context, req *provider.ListGrantsRequest) (*provider.ListGrantsResponse, error) { - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) if err != nil { return &provider.ListGrantsResponse{ Status: status.NewInternal(ctx, err, "error unwrapping path"), @@ -944,7 +997,7 @@ func (s *service) ListGrants(ctx context.Context, req *provider.ListGrantsReques } func (s *service) AddGrant(ctx context.Context, req *provider.AddGrantRequest) (*provider.AddGrantResponse, error) { - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) if err != nil { return &provider.AddGrantResponse{ Status: status.NewInternal(ctx, err, "error unwrapping path"), @@ -988,7 +1041,7 @@ func (s *service) UpdateGrant(ctx context.Context, req *provider.UpdateGrantRequ }, nil } - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) if err != nil { return &provider.UpdateGrantResponse{ Status: status.NewInternal(ctx, err, "error unwrapping path"), @@ -1024,7 +1077,7 @@ func (s *service) RemoveGrant(ctx context.Context, req *provider.RemoveGrantRequ }, nil } - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) if err != nil { return &provider.RemoveGrantResponse{ Status: status.NewInternal(ctx, err, "error unwrapping path"), @@ -1064,7 +1117,7 @@ func (s *service) CreateReference(ctx context.Context, req *provider.CreateRefer }, nil } - newRef, err := s.unwrap(ctx, req.Ref) + ctx, newRef, err := s.unwrap(ctx, req.Ref) if err != nil { return &provider.CreateReferenceResponse{ Status: status.NewInternal(ctx, err, "error unwrapping path"), @@ -1130,25 +1183,29 @@ func getFS(c *config) (storage.FS, error) { return nil, errtypes.NotFound("driver not found: " + c.Driver) } -func (s *service) unwrap(ctx context.Context, ref *provider.Reference) (*provider.Reference, error) { - if ref.GetResourceId() != nil { - return ref, nil +func (s *service) unwrap(ctx context.Context, ref *provider.Reference) (context.Context, *provider.Reference, error) { + // all references with an id can be passed on to the driver + // there are two cases: + // 1. absolute id references (resource_id is set, path is empty) + // 2. relative references (resource_id is set, path starts with a `.`) + if ref.ResourceId != nil { + return ctx, ref, nil } - if ref.GetPath() == "" { - // abort, no valid id nor path - return nil, errtypes.BadRequest("ref is invalid: " + ref.String()) + // if the + if !strings.HasPrefix(ref.GetPath(), "/") { + // abort, absolute path references must start with a `/`` + return ctx, nil, errtypes.BadRequest("ref is invalid: " + ref.String()) } + // TODO move mount path trimming to the gateway fn := ref.GetPath() fsfn, err := s.trimMountPrefix(fn) if err != nil { - return nil, err + return ctx, nil, err } - pathRef := &provider.Reference{Path: fsfn} - - return pathRef, nil + return ctx, &provider.Reference{Path: fsfn}, nil } func (s *service) trimMountPrefix(fn string) (string, error) { @@ -1158,11 +1215,14 @@ func (s *service) trimMountPrefix(fn string) (string, error) { return "", errtypes.BadRequest(fmt.Sprintf("path=%q does not belong to this storage provider mount path=%q", fn, s.mountPath)) } -func (s *service) wrap(ctx context.Context, ri *provider.ResourceInfo) error { +func (s *service) wrap(ctx context.Context, ri *provider.ResourceInfo, prefixMountpoint bool) error { if ri.Id.StorageId == "" { // For wrapper drivers, the storage ID might already be set. In that case, skip setting it ri.Id.StorageId = s.mountID } - ri.Path = path.Join(s.mountPath, ri.Path) + if prefixMountpoint { + // TODO move mount path prefixing to the gateway + ri.Path = path.Join(s.mountPath, ri.Path) + } return nil } diff --git a/internal/http/services/dataprovider/dataprovider.go b/internal/http/services/dataprovider/dataprovider.go index a87a32081e5..de4a7a785a3 100644 --- a/internal/http/services/dataprovider/dataprovider.go +++ b/internal/http/services/dataprovider/dataprovider.go @@ -103,6 +103,7 @@ func getDataTXs(c *config, fs storage.FS) (map[string]http.Handler, error) { } if len(c.DataTXs) == 0 { c.DataTXs["simple"] = make(map[string]interface{}) + c.DataTXs["spaces"] = make(map[string]interface{}) c.DataTXs["tus"] = make(map[string]interface{}) } diff --git a/internal/http/services/owncloud/ocdav/copy.go b/internal/http/services/owncloud/ocdav/copy.go index a3cbee49ec2..d4e244886d4 100644 --- a/internal/http/services/owncloud/ocdav/copy.go +++ b/internal/http/services/owncloud/ocdav/copy.go @@ -23,6 +23,7 @@ import ( "fmt" "net/http" "path" + "strconv" "strings" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -32,23 +33,24 @@ import ( "github.com/cs3org/reva/internal/http/services/datagateway" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/rhttp" + "github.com/cs3org/reva/pkg/rhttp/router" + "github.com/pkg/errors" + "github.com/rs/zerolog" "go.opencensus.io/trace" ) -func (s *svc) handleCopy(w http.ResponseWriter, r *http.Request, ns string) { +var ( + errInvalidValue = errors.New("invalid value") +) + +func (s *svc) handlePathCopy(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "head") defer span.End() src := path.Join(ns, r.URL.Path) - dstHeader := r.Header.Get("Destination") - overwrite := r.Header.Get("Overwrite") - depth := r.Header.Get("Depth") - if depth == "" { - depth = "infinity" - } - dst, err := extractDestination(dstHeader, r.Context().Value(ctxKeyBaseURI).(string)) + dst, err := extractDestination(r) if err != nil { w.WriteHeader(http.StatusBadRequest) return @@ -56,23 +58,22 @@ func (s *svc) handleCopy(w http.ResponseWriter, r *http.Request, ns string) { dst = path.Join(ns, dst) sublog := appctx.GetLogger(ctx).With().Str("src", src).Str("dst", dst).Logger() - sublog.Debug().Str("overwrite", overwrite).Str("depth", depth).Msg("copy") - overwrite = strings.ToUpper(overwrite) - if overwrite == "" { - overwrite = "T" - } + srcRef := &provider.Reference{Path: src} - if overwrite != "T" && overwrite != "F" { - w.WriteHeader(http.StatusBadRequest) - return + // check dst exists + dstRef := &provider.Reference{Path: dst} + + intermediateDirRefFunc := func() (*provider.Reference, *rpc.Status, error) { + intermediateDir := path.Dir(dst) + ref := &provider.Reference{Path: intermediateDir} + return ref, &rpc.Status{Code: rpc.Code_CODE_OK}, nil } - if depth != "infinity" && depth != "0" { - w.WriteHeader(http.StatusBadRequest) + srcInfo, depth, successCode, ok := s.prepareCopy(ctx, w, r, srcRef, dstRef, intermediateDirRefFunc, sublog) + if !ok { return } - client, err := s.getClient() if err != nil { sublog.Error().Err(err).Msg("error getting grpc client") @@ -80,70 +81,207 @@ func (s *svc) handleCopy(w http.ResponseWriter, r *http.Request, ns string) { return } - // check src exists - ref := &provider.Reference{Path: src} - srcStatReq := &provider.StatRequest{Ref: ref} - srcStatRes, err := client.Stat(ctx, srcStatReq) + err = s.executePathCopy(ctx, client, srcInfo, dst, depth == "infinity") if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + sublog.Error().Err(err).Str("depth", depth).Msg("error descending directory") w.WriteHeader(http.StatusInternalServerError) return } + w.WriteHeader(successCode) +} - if srcStatRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, srcStatRes.Status) +func (s *svc) executePathCopy(ctx context.Context, client gateway.GatewayAPIClient, src *provider.ResourceInfo, dst string, recurse bool) error { + log := appctx.GetLogger(ctx) + log.Debug().Str("src", src.Path).Str("dst", dst).Msg("descending") + if src.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + // create dir + createReq := &provider.CreateContainerRequest{ + Ref: &provider.Reference{Path: dst}, + } + createRes, err := client.CreateContainer(ctx, createReq) + if err != nil || createRes.Status.Code != rpc.Code_CODE_OK { + return err + } + + // TODO: also copy properties: https://tools.ietf.org/html/rfc4918#section-9.8.2 + + if !recurse { + return nil + } + + // descend for children + listReq := &provider.ListContainerRequest{ + Ref: &provider.Reference{Path: src.Path}, + } + res, err := client.ListContainer(ctx, listReq) + if err != nil { + return err + } + if res.Status.Code != rpc.Code_CODE_OK { + return fmt.Errorf("status code %d", res.Status.Code) + } + + for i := range res.Infos { + childDst := path.Join(dst, path.Base(res.Infos[i].Path)) + err := s.executePathCopy(ctx, client, res.Infos[i], childDst, recurse) + if err != nil { + return err + } + } + + } else { + // copy file + + // 1. get download url + + dReq := &provider.InitiateFileDownloadRequest{ + Ref: &provider.Reference{Path: src.Path}, + } + + dRes, err := client.InitiateFileDownload(ctx, dReq) + if err != nil { + return err + } + + if dRes.Status.Code != rpc.Code_CODE_OK { + return fmt.Errorf("status code %d", dRes.Status.Code) + } + + var downloadEP, downloadToken string + for _, p := range dRes.Protocols { + if p.Protocol == "simple" { + downloadEP, downloadToken = p.DownloadEndpoint, p.Token + } + } + + // 2. get upload url + + uReq := &provider.InitiateFileUploadRequest{ + Ref: &provider.Reference{Path: dst}, + Opaque: &typespb.Opaque{ + Map: map[string]*typespb.OpaqueEntry{ + "Upload-Length": { + Decoder: "plain", + // TODO: handle case where size is not known in advance + Value: []byte(strconv.FormatUint(src.GetSize(), 10)), + }, + }, + }, + } + + uRes, err := client.InitiateFileUpload(ctx, uReq) + if err != nil { + return err + } + + if uRes.Status.Code != rpc.Code_CODE_OK { + return fmt.Errorf("status code %d", uRes.Status.Code) + } + + var uploadEP, uploadToken string + for _, p := range uRes.Protocols { + if p.Protocol == "simple" { + uploadEP, uploadToken = p.UploadEndpoint, p.Token + } + } + + // 3. do download + + httpDownloadReq, err := rhttp.NewRequest(ctx, "GET", downloadEP, nil) + if err != nil { + return err + } + httpDownloadReq.Header.Set(datagateway.TokenTransportHeader, downloadToken) + + httpDownloadRes, err := s.client.Do(httpDownloadReq) + if err != nil { + return err + } + defer httpDownloadRes.Body.Close() + if httpDownloadRes.StatusCode != http.StatusOK { + return fmt.Errorf("status code %d", httpDownloadRes.StatusCode) + } + + // 4. do upload + + if src.GetSize() > 0 { + httpUploadReq, err := rhttp.NewRequest(ctx, "PUT", uploadEP, httpDownloadRes.Body) + if err != nil { + return err + } + httpUploadReq.Header.Set(datagateway.TokenTransportHeader, uploadToken) + + httpUploadRes, err := s.client.Do(httpUploadReq) + if err != nil { + return err + } + defer httpUploadRes.Body.Close() + if httpUploadRes.StatusCode != http.StatusOK { + return err + } + } + } + return nil +} + +func (s *svc) handleSpacesCopy(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "head") + defer span.End() + + dst, err := extractDestination(r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) return } - // check dst exists - ref = &provider.Reference{Path: dst} - dstStatReq := &provider.StatRequest{Ref: ref} - dstStatRes, err := client.Stat(ctx, dstStatReq) + sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Logger() + + // retrieve a specific storage space + srcRef, status, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + sublog.Error().Err(err).Msg("error sending a grpc request") w.WriteHeader(http.StatusInternalServerError) return } - if dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - HandleErrorStatus(&sublog, w, srcStatRes.Status) + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) return } - successCode := http.StatusCreated // 201 if new resource was created, see https://tools.ietf.org/html/rfc4918#section-9.8.5 - if dstStatRes.Status.Code == rpc.Code_CODE_OK { - successCode = http.StatusNoContent // 204 if target already existed, see https://tools.ietf.org/html/rfc4918#section-9.8.5 + dstSpaceID, dstRelPath := router.ShiftPath(dst) - if overwrite == "F" { - sublog.Warn().Str("overwrite", overwrite).Msg("dst already exists") - w.WriteHeader(http.StatusPreconditionFailed) // 412, see https://tools.ietf.org/html/rfc4918#section-9.8.5 - return - } + // retrieve a specific storage space + dstRef, status, err := s.lookUpStorageSpaceReference(ctx, dstSpaceID, dstRelPath) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } - } else { - // check if an intermediate path / the parent exists - intermediateDir := path.Dir(dst) - ref = &provider.Reference{Path: intermediateDir} - intStatReq := &provider.StatRequest{Ref: ref} - intStatRes, err := client.Stat(ctx, intStatReq) - if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") - w.WriteHeader(http.StatusInternalServerError) - return - } - if intStatRes.Status.Code != rpc.Code_CODE_OK { - if intStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND { - // 409 if intermediate dir is missing, see https://tools.ietf.org/html/rfc4918#section-9.8.5 - sublog.Debug().Str("parent", intermediateDir).Interface("status", intStatRes.Status).Msg("conflict") - w.WriteHeader(http.StatusConflict) - } else { - HandleErrorStatus(&sublog, w, srcStatRes.Status) - } - return - } - // TODO what if intermediate is a file? + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) + return + } + + intermediateDirRefFunc := func() (*provider.Reference, *rpc.Status, error) { + intermediateDir := path.Dir(dstRelPath) + return s.lookUpStorageSpaceReference(ctx, dstSpaceID, intermediateDir) + } + + srcInfo, depth, successCode, ok := s.prepareCopy(ctx, w, r, srcRef, dstRef, intermediateDirRefFunc, sublog) + if !ok { + return + } + client, err := s.getClient() + if err != nil { + sublog.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return } - err = s.descend(ctx, client, srcStatRes.Info, dst, depth == "infinity") + err = s.executeSpacesCopy(ctx, client, srcInfo, dstRef, depth == "infinity") if err != nil { sublog.Error().Err(err).Str("depth", depth).Msg("error descending directory") w.WriteHeader(http.StatusInternalServerError) @@ -152,13 +290,14 @@ func (s *svc) handleCopy(w http.ResponseWriter, r *http.Request, ns string) { w.WriteHeader(successCode) } -func (s *svc) descend(ctx context.Context, client gateway.GatewayAPIClient, src *provider.ResourceInfo, dst string, recurse bool) error { +func (s *svc) executeSpacesCopy(ctx context.Context, client gateway.GatewayAPIClient, src *provider.ResourceInfo, dst *provider.Reference, recurse bool) error { log := appctx.GetLogger(ctx) - log.Debug().Str("src", src.Path).Str("dst", dst).Msg("descending") + log.Debug().Str("src", src.Path).Interface("dst", dst).Msg("descending") + if src.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { // create dir createReq := &provider.CreateContainerRequest{ - Ref: &provider.Reference{Path: dst}, + Ref: dst, } createRes, err := client.CreateContainer(ctx, createReq) if err != nil || createRes.Status.Code != rpc.Code_CODE_OK { @@ -172,9 +311,7 @@ func (s *svc) descend(ctx context.Context, client gateway.GatewayAPIClient, src } // descend for children - listReq := &provider.ListContainerRequest{ - Ref: &provider.Reference{Path: src.Path}, - } + listReq := &provider.ListContainerRequest{Ref: &provider.Reference{ResourceId: src.Id}} res, err := client.ListContainer(ctx, listReq) if err != nil { return err @@ -184,8 +321,12 @@ func (s *svc) descend(ctx context.Context, client gateway.GatewayAPIClient, src } for i := range res.Infos { - childDst := path.Join(dst, path.Base(res.Infos[i].Path)) - err := s.descend(ctx, client, res.Infos[i], childDst, recurse) + childPath := strings.TrimPrefix(res.Infos[i].Path, src.Path) + childRef := &provider.Reference{ + ResourceId: src.Id, + Path: path.Join(dst.Path, childPath), + } + err := s.executeSpacesCopy(ctx, client, res.Infos[i], childRef, recurse) if err != nil { return err } @@ -196,9 +337,7 @@ func (s *svc) descend(ctx context.Context, client gateway.GatewayAPIClient, src // 1. get download url - dReq := &provider.InitiateFileDownloadRequest{ - Ref: &provider.Reference{Path: src.Path}, - } + dReq := &provider.InitiateFileDownloadRequest{Ref: &provider.Reference{ResourceId: src.Id}} dRes, err := client.InitiateFileDownload(ctx, dReq) if err != nil { @@ -211,7 +350,7 @@ func (s *svc) descend(ctx context.Context, client gateway.GatewayAPIClient, src var downloadEP, downloadToken string for _, p := range dRes.Protocols { - if p.Protocol == "simple" { + if p.Protocol == "spaces" { downloadEP, downloadToken = p.DownloadEndpoint, p.Token } } @@ -219,13 +358,13 @@ func (s *svc) descend(ctx context.Context, client gateway.GatewayAPIClient, src // 2. get upload url uReq := &provider.InitiateFileUploadRequest{ - Ref: &provider.Reference{Path: dst}, + Ref: dst, Opaque: &typespb.Opaque{ Map: map[string]*typespb.OpaqueEntry{ "Upload-Length": { Decoder: "plain", // TODO: handle case where size is not known in advance - Value: []byte(fmt.Sprintf("%d", src.GetSize())), + Value: []byte(strconv.FormatUint(src.GetSize(), 10)), }, }, }, @@ -285,3 +424,120 @@ func (s *svc) descend(ctx context.Context, client gateway.GatewayAPIClient, src } return nil } + +func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Request, srcRef, dstRef *provider.Reference, intermediateDirRef func() (*provider.Reference, *rpc.Status, error), log zerolog.Logger) (*provider.ResourceInfo, string, int, bool) { + overwrite, err := extractOverwrite(w, r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return nil, "", 0, false + } + depth, err := extractDepth(w, r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return nil, "", 0, false + } + + log.Debug().Str("overwrite", overwrite).Str("depth", depth).Msg("copy") + + client, err := s.getClient() + if err != nil { + log.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return nil, "", 0, false + } + + srcStatReq := &provider.StatRequest{Ref: srcRef} + srcStatRes, err := client.Stat(ctx, srcStatReq) + if err != nil { + log.Error().Err(err).Msg("error sending grpc stat request") + w.WriteHeader(http.StatusInternalServerError) + return nil, "", 0, false + } + + if srcStatRes.Status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&log, w, srcStatRes.Status) + return nil, "", 0, false + } + + dstStatReq := &provider.StatRequest{Ref: dstRef} + dstStatRes, err := client.Stat(ctx, dstStatReq) + if err != nil { + log.Error().Err(err).Msg("error sending grpc stat request") + w.WriteHeader(http.StatusInternalServerError) + return nil, "", 0, false + } + if dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND { + HandleErrorStatus(&log, w, srcStatRes.Status) + return nil, "", 0, false + } + + successCode := http.StatusCreated // 201 if new resource was created, see https://tools.ietf.org/html/rfc4918#section-9.8.5 + if dstStatRes.Status.Code == rpc.Code_CODE_OK { + successCode = http.StatusNoContent // 204 if target already existed, see https://tools.ietf.org/html/rfc4918#section-9.8.5 + + if overwrite == "F" { + log.Warn().Str("overwrite", overwrite).Msg("dst already exists") + w.WriteHeader(http.StatusPreconditionFailed) // 412, see https://tools.ietf.org/html/rfc4918#section-9.8.5 + return nil, "", 0, false + } + + } else { + // check if an intermediate path / the parent exists + intermediateRef, status, err := intermediateDirRef() + if err != nil { + log.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return nil, "", 0, false + } + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&log, w, status) + return nil, "", 0, false + } + intStatReq := &provider.StatRequest{Ref: intermediateRef} + intStatRes, err := client.Stat(ctx, intStatReq) + if err != nil { + log.Error().Err(err).Msg("error sending grpc stat request") + w.WriteHeader(http.StatusInternalServerError) + return nil, "", 0, false + } + if intStatRes.Status.Code != rpc.Code_CODE_OK { + if intStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + // 409 if intermediate dir is missing, see https://tools.ietf.org/html/rfc4918#section-9.8.5 + log.Debug().Interface("parent", intermediateRef).Interface("status", intStatRes.Status).Msg("conflict") + w.WriteHeader(http.StatusConflict) + } else { + HandleErrorStatus(&log, w, srcStatRes.Status) + } + return nil, "", 0, false + } + // TODO what if intermediate is a file? + } + + return srcStatRes.Info, depth, successCode, true +} + +func extractOverwrite(w http.ResponseWriter, r *http.Request) (string, error) { + overwrite := r.Header.Get(HeaderOverwrite) + overwrite = strings.ToUpper(overwrite) + if overwrite == "" { + overwrite = "T" + } + + if overwrite != "T" && overwrite != "F" { + return "", errInvalidValue + } + + return overwrite, nil +} + +func extractDepth(w http.ResponseWriter, r *http.Request) (string, error) { + depth := r.Header.Get(HeaderDepth) + if depth == "" { + depth = "infinity" + } + if depth != "infinity" && depth != "0" { + return "", errInvalidValue + } + return depth, nil +} diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index a1681fe29d0..321456b039e 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -47,6 +47,7 @@ type DavHandler struct { FilesHomeHandler *WebDavHandler MetaHandler *MetaHandler TrashbinHandler *TrashbinHandler + SpacesHandler *SpacesHandler PublicFolderHandler *WebDavHandler PublicFileHandler *PublicFileHandler } @@ -70,6 +71,11 @@ func (h *DavHandler) init(c *Config) error { } h.TrashbinHandler = new(TrashbinHandler) + h.SpacesHandler = new(SpacesHandler) + if err := h.SpacesHandler.init(c); err != nil { + return err + } + h.PublicFolderHandler = new(WebDavHandler) if err := h.PublicFolderHandler.init("public", true); err != nil { // jail public file requests to /public/ prefix return err @@ -163,6 +169,11 @@ func (h *DavHandler) Handler(s *svc) http.Handler { ctx := context.WithValue(ctx, ctxKeyBaseURI, base) r = r.WithContext(ctx) h.TrashbinHandler.Handler(s).ServeHTTP(w, r) + case "spaces": + base := path.Join(ctx.Value(ctxKeyBaseURI).(string), "spaces") + ctx := context.WithValue(ctx, ctxKeyBaseURI, base) + r = r.WithContext(ctx) + h.SpacesHandler.Handler(s).ServeHTTP(w, r) case "public-files": base := path.Join(ctx.Value(ctxKeyBaseURI).(string), "public-files") ctx = context.WithValue(ctx, ctxKeyBaseURI, base) diff --git a/internal/http/services/owncloud/ocdav/delete.go b/internal/http/services/owncloud/ocdav/delete.go index 8898a95d1a1..f946c440444 100644 --- a/internal/http/services/owncloud/ocdav/delete.go +++ b/internal/http/services/owncloud/ocdav/delete.go @@ -19,43 +19,70 @@ package ocdav import ( + "context" "net/http" "path" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" + "github.com/rs/zerolog" "go.opencensus.io/trace" ) -func (s *svc) handleDelete(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathDelete(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() - ctx, span := trace.StartSpan(ctx, "head") + ctx, span := trace.StartSpan(ctx, "delete") defer span.End() fn := path.Join(ns, r.URL.Path) sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() + ref := &provider.Reference{Path: fn} + s.handleDelete(ctx, w, r, ref, sublog) +} +func (s *svc) handleDelete(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) { client, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } - ref := &provider.Reference{Path: fn} req := &provider.DeleteRequest{Ref: ref} res, err := client.Delete(ctx, req) if err != nil { - sublog.Error().Err(err).Msg("error performing delete grpc request") + log.Error().Err(err).Msg("error performing delete grpc request") w.WriteHeader(http.StatusInternalServerError) return } if res.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&log, w, res.Status) return } w.WriteHeader(http.StatusNoContent) } + +func (s *svc) handleSpacesDelete(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_delete") + defer span.End() + + sublog := appctx.GetLogger(ctx).With().Logger() + // retrieve a specific storage space + ref, rpcStatus, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if rpcStatus.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, rpcStatus) + return + } + + s.handleDelete(ctx, w, r, ref, sublog) +} diff --git a/internal/http/services/owncloud/ocdav/get.go b/internal/http/services/owncloud/ocdav/get.go index d462d8fa1c4..e4786623dde 100644 --- a/internal/http/services/owncloud/ocdav/get.go +++ b/internal/http/services/owncloud/ocdav/get.go @@ -19,6 +19,7 @@ package ocdav import ( + "context" "fmt" "io" "net/http" @@ -29,6 +30,7 @@ import ( "github.com/cs3org/reva/internal/grpc/services/storageprovider" "github.com/cs3org/reva/internal/http/services/datagateway" + "github.com/rs/zerolog" "go.opencensus.io/trace" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" @@ -38,7 +40,7 @@ import ( "github.com/cs3org/reva/pkg/utils" ) -func (s *svc) handleGet(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathGet(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "get") defer span.End() @@ -47,75 +49,77 @@ func (s *svc) handleGet(w http.ResponseWriter, r *http.Request, ns string) { sublog := appctx.GetLogger(ctx).With().Str("path", fn).Str("svc", "ocdav").Str("handler", "get").Logger() + ref := &provider.Reference{Path: fn} + + s.handleGet(ctx, w, r, ref, "simple", sublog) +} + +func (s *svc) handleGet(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, dlProtocol string, log zerolog.Logger) { client, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } - sReq := &provider.StatRequest{ - Ref: &provider.Reference{Path: fn}, - } + sReq := &provider.StatRequest{Ref: ref} sRes, err := client.Stat(ctx, sReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if sRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&log, w, sRes.Status) return } info := sRes.Info if info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { - sublog.Warn().Msg("resource is a folder and cannot be downloaded") + log.Warn().Msg("resource is a folder and cannot be downloaded") w.WriteHeader(http.StatusNotImplemented) return } - dReq := &provider.InitiateFileDownloadRequest{ - Ref: &provider.Reference{Path: fn}, - } + dReq := &provider.InitiateFileDownloadRequest{Ref: ref} dRes, err := client.InitiateFileDownload(ctx, dReq) if err != nil { - sublog.Error().Err(err).Msg("error initiating file download") + log.Error().Err(err).Msg("error initiating file download") w.WriteHeader(http.StatusInternalServerError) return } if dRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, dRes.Status) + HandleErrorStatus(&log, w, dRes.Status) return } var ep, token string for _, p := range dRes.Protocols { - if p.Protocol == "simple" { + if p.Protocol == dlProtocol { ep, token = p.DownloadEndpoint, p.Token } } - httpReq, err := rhttp.NewRequest(ctx, "GET", ep, nil) + httpReq, err := rhttp.NewRequest(ctx, http.MethodGet, ep, nil) if err != nil { - sublog.Error().Err(err).Msg("error creating http request") + log.Error().Err(err).Msg("error creating http request") w.WriteHeader(http.StatusInternalServerError) return } httpReq.Header.Set(datagateway.TokenTransportHeader, token) - if r.Header.Get("Range") != "" { - httpReq.Header.Set("Range", r.Header.Get("Range")) + if r.Header.Get(HeaderRange) != "" { + httpReq.Header.Set(HeaderRange, r.Header.Get(HeaderRange)) } httpClient := s.client httpRes, err := httpClient.Do(httpReq) if err != nil { - sublog.Error().Err(err).Msg("error performing http request") + log.Error().Err(err).Msg("error performing http request") w.WriteHeader(http.StatusInternalServerError) return } @@ -126,38 +130,60 @@ func (s *svc) handleGet(w http.ResponseWriter, r *http.Request, ns string) { return } - w.Header().Set("Content-Type", info.MimeType) - w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+ + w.Header().Set(HeaderContentType, info.MimeType) + w.Header().Set(HeaderContentDisposistion, "attachment; filename*=UTF-8''"+ path.Base(info.Path)+"; filename=\""+path.Base(info.Path)+"\"") - w.Header().Set("ETag", info.Etag) - w.Header().Set("OC-FileId", wrapResourceID(info.Id)) - w.Header().Set("OC-ETag", info.Etag) + w.Header().Set(HeaderETag, info.Etag) + w.Header().Set(HeaderOCFileID, wrapResourceID(info.Id)) + w.Header().Set(HeaderOCETag, info.Etag) t := utils.TSToTime(info.Mtime).UTC() lastModifiedString := t.Format(time.RFC1123Z) - w.Header().Set("Last-Modified", lastModifiedString) + w.Header().Set(HeaderLastModified, lastModifiedString) if httpRes.StatusCode == http.StatusPartialContent { - w.Header().Set("Content-Range", httpRes.Header.Get("Content-Range")) - w.Header().Set("Content-Length", httpRes.Header.Get("Content-Length")) + w.Header().Set(HeaderContentRange, httpRes.Header.Get(HeaderContentRange)) + w.Header().Set(HeaderContentLength, httpRes.Header.Get(HeaderContentLength)) w.WriteHeader(http.StatusPartialContent) } else { - w.Header().Set("Content-Length", strconv.FormatUint(info.Size, 10)) + w.Header().Set(HeaderContentLength, strconv.FormatUint(info.Size, 10)) } if info.Checksum != nil { - w.Header().Set("OC-Checksum", fmt.Sprintf("%s:%s", strings.ToUpper(string(storageprovider.GRPC2PKGXS(info.Checksum.Type))), info.Checksum.Sum)) + w.Header().Set(HeaderOCChecksum, fmt.Sprintf("%s:%s", strings.ToUpper(string(storageprovider.GRPC2PKGXS(info.Checksum.Type))), info.Checksum.Sum)) } var c int64 if c, err = io.Copy(w, httpRes.Body); err != nil { - sublog.Error().Err(err).Msg("error finishing copying data to response") + log.Error().Err(err).Msg("error finishing copying data to response") } - if httpRes.Header.Get("Content-Length") != "" { - i, err := strconv.ParseInt(httpRes.Header.Get("Content-Length"), 10, 64) + if httpRes.Header.Get(HeaderContentLength) != "" { + i, err := strconv.ParseInt(httpRes.Header.Get(HeaderContentLength), 10, 64) if err != nil { - sublog.Error().Err(err).Str("content-length", httpRes.Header.Get("Content-Length")).Msg("invalid content length in datagateway response") + log.Error().Err(err).Str("content-length", httpRes.Header.Get(HeaderContentLength)).Msg("invalid content length in datagateway response") } if i != c { - sublog.Error().Int64("content-length", i).Int64("transferred-bytes", c).Msg("content length vs transferred bytes mismatch") + log.Error().Int64("content-length", i).Int64("transferred-bytes", c).Msg("content length vs transferred bytes mismatch") } } // TODO we need to send the If-Match etag in the GET to the datagateway to prevent race conditions between stating and reading the file } + +func (s *svc) handleSpacesGet(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_get") + defer span.End() + + sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Str("handler", "get").Logger() + + // retrieve a specific storage space + ref, rpcStatus, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if rpcStatus.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, rpcStatus) + return + } + s.handleGet(ctx, w, r, ref, "spaces", sublog) +} diff --git a/internal/http/services/owncloud/ocdav/head.go b/internal/http/services/owncloud/ocdav/head.go index f70d4b95d89..888fe497694 100644 --- a/internal/http/services/owncloud/ocdav/head.go +++ b/internal/http/services/owncloud/ocdav/head.go @@ -19,6 +19,7 @@ package ocdav import ( + "context" "fmt" "net/http" "path" @@ -31,10 +32,11 @@ import ( "github.com/cs3org/reva/internal/grpc/services/storageprovider" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/utils" + "github.com/rs/zerolog" "go.opencensus.io/trace" ) -func (s *svc) handleHead(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathHead(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "head") defer span.End() @@ -43,41 +45,67 @@ func (s *svc) handleHead(w http.ResponseWriter, r *http.Request, ns string) { sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() + ref := &provider.Reference{Path: fn} + s.handleHead(ctx, w, r, ref, sublog) +} + +func (s *svc) handleHead(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, logger zerolog.Logger) { client, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + logger.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } - ref := &provider.Reference{Path: fn} req := &provider.StatRequest{Ref: ref} res, err := client.Stat(ctx, req) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + logger.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if res.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&logger, w, res.Status) return } info := res.Info - w.Header().Set("Content-Type", info.MimeType) - w.Header().Set("ETag", info.Etag) - w.Header().Set("OC-FileId", wrapResourceID(info.Id)) - w.Header().Set("OC-ETag", info.Etag) + w.Header().Set(HeaderContentType, info.MimeType) + w.Header().Set(HeaderETag, info.Etag) + w.Header().Set(HeaderOCFileID, wrapResourceID(info.Id)) + w.Header().Set(HeaderOCETag, info.Etag) if info.Checksum != nil { - w.Header().Set("OC-Checksum", fmt.Sprintf("%s:%s", strings.ToUpper(string(storageprovider.GRPC2PKGXS(info.Checksum.Type))), info.Checksum.Sum)) + w.Header().Set(HeaderOCChecksum, fmt.Sprintf("%s:%s", strings.ToUpper(string(storageprovider.GRPC2PKGXS(info.Checksum.Type))), info.Checksum.Sum)) } t := utils.TSToTime(info.Mtime).UTC() lastModifiedString := t.Format(time.RFC1123Z) - w.Header().Set("Last-Modified", lastModifiedString) - w.Header().Set("Content-Length", strconv.FormatUint(info.Size, 10)) + w.Header().Set(HeaderLastModified, lastModifiedString) + w.Header().Set(HeaderContentLength, strconv.FormatUint(info.Size, 10)) if info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER { - w.Header().Set("Accept-Ranges", "bytes") + w.Header().Set(HeaderAcceptRanges, "bytes") } w.WriteHeader(http.StatusOK) } + +func (s *svc) handleSpacesHead(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_head") + defer span.End() + + sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Logger() + + spaceRef, status, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) + return + } + + s.handleHead(ctx, w, r, spaceRef, sublog) +} diff --git a/internal/http/services/owncloud/ocdav/mkcol.go b/internal/http/services/owncloud/ocdav/mkcol.go index 889598a43ba..c6e77e0700c 100644 --- a/internal/http/services/owncloud/ocdav/mkcol.go +++ b/internal/http/services/owncloud/ocdav/mkcol.go @@ -19,17 +19,18 @@ package ocdav import ( - "io" + "context" "net/http" "path" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" + "github.com/rs/zerolog" "go.opencensus.io/trace" ) -func (s *svc) handleMkcol(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathMkcol(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "mkcol") defer span.End() @@ -38,27 +39,52 @@ func (s *svc) handleMkcol(w http.ResponseWriter, r *http.Request, ns string) { sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() - buf := make([]byte, 1) - _, err := r.Body.Read(buf) - if err != io.EOF { - sublog.Error().Err(err).Msg("error reading request body") + ref := &provider.Reference{Path: fn} + + s.handleMkcol(ctx, w, r, ref, sublog) +} + +func (s *svc) handleSpacesMkCol(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_mkcol") + defer span.End() + + sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Str("handler", "mkcol").Logger() + + ref, rpcStatus, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if rpcStatus.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, rpcStatus) + return + } + + s.handleMkcol(ctx, w, r, ref, sublog) + +} + +func (s *svc) handleMkcol(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) { + if r.Body != http.NoBody { w.WriteHeader(http.StatusUnsupportedMediaType) return } - client, err := s.getClient() + gatewayClient, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } - // check fn exists - ref := &provider.Reference{Path: fn} + // check if ref exists statReq := &provider.StatRequest{Ref: ref} - statRes, err := client.Stat(ctx, statReq) + statRes, err := gatewayClient.Stat(ctx, statReq) if err != nil { - sublog.Error().Err(err).Msg("error sending a grpc stat request") + log.Error().Err(err).Msg("error sending a grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } @@ -67,15 +93,15 @@ func (s *svc) handleMkcol(w http.ResponseWriter, r *http.Request, ns string) { if statRes.Status.Code == rpc.Code_CODE_OK { w.WriteHeader(http.StatusMethodNotAllowed) // 405 if it already exists } else { - HandleErrorStatus(&sublog, w, statRes.Status) + HandleErrorStatus(&log, w, statRes.Status) } return } req := &provider.CreateContainerRequest{Ref: ref} - res, err := client.CreateContainer(ctx, req) + res, err := gatewayClient.CreateContainer(ctx, req) if err != nil { - sublog.Error().Err(err).Msg("error sending create container grpc request") + log.Error().Err(err).Msg("error sending create container grpc request") w.WriteHeader(http.StatusInternalServerError) return } @@ -83,9 +109,9 @@ func (s *svc) handleMkcol(w http.ResponseWriter, r *http.Request, ns string) { case rpc.Code_CODE_OK: w.WriteHeader(http.StatusCreated) case rpc.Code_CODE_NOT_FOUND: - sublog.Debug().Str("path", fn).Interface("status", statRes.Status).Msg("conflict") + log.Debug().Str("path", r.URL.Path).Interface("status", statRes.Status).Msg("conflict") w.WriteHeader(http.StatusConflict) default: - HandleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&log, w, res.Status) } } diff --git a/internal/http/services/owncloud/ocdav/move.go b/internal/http/services/owncloud/ocdav/move.go index e92bc1bedee..1fa6f399e68 100644 --- a/internal/http/services/owncloud/ocdav/move.go +++ b/internal/http/services/owncloud/ocdav/move.go @@ -19,6 +19,7 @@ package ocdav import ( + "context" "net/http" "path" "strings" @@ -26,19 +27,19 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/rhttp/router" + "github.com/rs/zerolog" "go.opencensus.io/trace" ) -func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathMove(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "move") defer span.End() src := path.Join(ns, r.URL.Path) - dstHeader := r.Header.Get("Destination") - overwrite := r.Header.Get("Overwrite") - dst, err := extractDestination(dstHeader, r.Context().Value(ctxKeyBaseURI).(string)) + dst, err := extractDestination(r) if err != nil { w.WriteHeader(http.StatusBadRequest) return @@ -46,7 +47,67 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { dst = path.Join(ns, dst) sublog := appctx.GetLogger(ctx).With().Str("src", src).Str("dst", dst).Logger() - sublog.Debug().Str("overwrite", overwrite).Msg("move") + srcRef := &provider.Reference{Path: src} + dstRef := &provider.Reference{Path: dst} + + intermediateDirRefFunc := func() (*provider.Reference, *rpc.Status, error) { + intermediateDir := path.Dir(dst) + ref := &provider.Reference{Path: intermediateDir} + return ref, &rpc.Status{Code: rpc.Code_CODE_OK}, nil + } + s.handleMove(ctx, w, r, srcRef, dstRef, intermediateDirRefFunc, sublog) +} + +func (s *svc) handleSpacesMove(w http.ResponseWriter, r *http.Request, srcSpaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_move") + defer span.End() + + dst, err := extractDestination(r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + sublog := appctx.GetLogger(ctx).With().Str("spaceid", srcSpaceID).Str("path", r.URL.Path).Logger() + // retrieve a specific storage space + srcRef, status, err := s.lookUpStorageSpaceReference(ctx, srcSpaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) + return + } + + dstSpaceID, dstRelPath := router.ShiftPath(dst) + + // retrieve a specific storage space + dstRef, status, err := s.lookUpStorageSpaceReference(ctx, dstSpaceID, dstRelPath) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) + return + } + + intermediateDirRefFunc := func() (*provider.Reference, *rpc.Status, error) { + intermediateDir := path.Dir(dstRelPath) + return s.lookUpStorageSpaceReference(ctx, dstSpaceID, intermediateDir) + } + s.handleMove(ctx, w, r, srcRef, dstRef, intermediateDirRefFunc, sublog) +} + +func (s *svc) handleMove(ctx context.Context, w http.ResponseWriter, r *http.Request, srcRef, dstRef *provider.Reference, intermediateDirRef func() (*provider.Reference, *rpc.Status, error), log zerolog.Logger) { + overwrite := r.Header.Get(HeaderOverwrite) + log.Debug().Str("overwrite", overwrite).Msg("move") overwrite = strings.ToUpper(overwrite) if overwrite == "" { @@ -60,118 +121,122 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { client, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } // check src exists - srcStatReq := &provider.StatRequest{ - Ref: &provider.Reference{Path: src}, - } + srcStatReq := &provider.StatRequest{Ref: srcRef} srcStatRes, err := client.Stat(ctx, srcStatReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if srcStatRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, srcStatRes.Status) + HandleErrorStatus(&log, w, srcStatRes.Status) return } - // check dst exists - dstStatRef := &provider.Reference{Path: dst} - dstStatReq := &provider.StatRequest{Ref: dstStatRef} + dstStatReq := &provider.StatRequest{Ref: dstRef} dstStatRes, err := client.Stat(ctx, dstStatReq) if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } if dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - HandleErrorStatus(&sublog, w, srcStatRes.Status) + HandleErrorStatus(&log, w, srcStatRes.Status) return } successCode := http.StatusCreated // 201 if new resource was created, see https://tools.ietf.org/html/rfc4918#section-9.9.4 + if dstStatRes.Status.Code == rpc.Code_CODE_OK { successCode = http.StatusNoContent // 204 if target already existed, see https://tools.ietf.org/html/rfc4918#section-9.9.4 if overwrite == "F" { - sublog.Warn().Str("overwrite", overwrite).Msg("dst already exists") + log.Warn().Str("overwrite", overwrite).Msg("dst already exists") w.WriteHeader(http.StatusPreconditionFailed) // 412, see https://tools.ietf.org/html/rfc4918#section-9.9.4 return } // delete existing tree - delReq := &provider.DeleteRequest{Ref: dstStatRef} + delReq := &provider.DeleteRequest{Ref: dstRef} delRes, err := client.Delete(ctx, delReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc delete request") + log.Error().Err(err).Msg("error sending grpc delete request") w.WriteHeader(http.StatusInternalServerError) return } if delRes.Status.Code != rpc.Code_CODE_OK && delRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - HandleErrorStatus(&sublog, w, delRes.Status) + HandleErrorStatus(&log, w, delRes.Status) return } } else { // check if an intermediate path / the parent exists - intermediateDir := path.Dir(dst) - ref2 := &provider.Reference{Path: intermediateDir} - intStatReq := &provider.StatRequest{Ref: ref2} + dstRef, status, err := intermediateDirRef() + if err != nil { + log.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&log, w, status) + return + } + intStatReq := &provider.StatRequest{Ref: dstRef} intStatRes, err := client.Stat(ctx, intStatReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if intStatRes.Status.Code != rpc.Code_CODE_OK { if intStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND { // 409 if intermediate dir is missing, see https://tools.ietf.org/html/rfc4918#section-9.8.5 - sublog.Debug().Str("parent", intermediateDir).Interface("status", intStatRes.Status).Msg("conflict") + log.Debug().Interface("parent", intermediateDirRef).Interface("status", intStatRes.Status).Msg("conflict") w.WriteHeader(http.StatusConflict) } else { - HandleErrorStatus(&sublog, w, intStatRes.Status) + HandleErrorStatus(&log, w, intStatRes.Status) } return } // TODO what if intermediate is a file? } - sourceRef := &provider.Reference{Path: src} - dstRef := &provider.Reference{Path: dst} - mReq := &provider.MoveRequest{Source: sourceRef, Destination: dstRef} + mReq := &provider.MoveRequest{Source: srcRef, Destination: dstRef} mRes, err := client.Move(ctx, mReq) if err != nil { - sublog.Error().Err(err).Msg("error sending move grpc request") + log.Error().Err(err).Msg("error sending move grpc request") w.WriteHeader(http.StatusInternalServerError) return } if mRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, mRes.Status) + HandleErrorStatus(&log, w, mRes.Status) return } dstStatRes, err = client.Stat(ctx, dstStatReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if dstStatRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, dstStatRes.Status) + HandleErrorStatus(&log, w, dstStatRes.Status) return } info := dstStatRes.Info - w.Header().Set("Content-Type", info.MimeType) - w.Header().Set("ETag", info.Etag) - w.Header().Set("OC-FileId", wrapResourceID(info.Id)) - w.Header().Set("OC-ETag", info.Etag) + w.Header().Set(HeaderContentType, info.MimeType) + w.Header().Set(HeaderETag, info.Etag) + w.Header().Set(HeaderOCFileID, wrapResourceID(info.Id)) + w.Header().Set(HeaderOCETag, info.Etag) w.WriteHeader(successCode) } diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index c2b7561cf2b..28da43e68fb 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -263,20 +263,22 @@ func addAccessHeaders(w http.ResponseWriter, r *http.Request) { } } -func extractDestination(dstHeader, baseURI string) (string, error) { +func extractDestination(r *http.Request) (string, error) { + dstHeader := r.Header.Get(HeaderDestination) if dstHeader == "" { - return "", errors.New("destination header is empty") + return "", errors.Wrap(errInvalidValue, "destination header is empty") } dstURL, err := url.ParseRequestURI(dstHeader) if err != nil { return "", err } + baseURI := r.Context().Value(ctxKeyBaseURI).(string) // TODO check if path is on same storage, return 502 on problems, see https://tools.ietf.org/html/rfc4918#section-9.9.4 // Strip the base URI from the destination. The destination might contain redirection prefixes which need to be handled urlSplit := strings.Split(dstURL.Path, baseURI) if len(urlSplit) != 2 { - return "", errors.New("destination path does not contain base URI") + return "", errors.Wrap(errInvalidValue, "destination path does not contain base URI") } return urlSplit[1], nil @@ -298,7 +300,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str return result + str[lastIndex:] } -var hrefre = regexp.MustCompile(`([^A-Za-z0-9_\-.~()/:@])`) +var hrefre = regexp.MustCompile(`([^A-Za-z0-9_\-.~()/:@!$])`) // encodePath encodes the path of a url. // diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go index 9e71d6b0369..22528eba238 100644 --- a/internal/http/services/owncloud/ocdav/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind.go @@ -43,6 +43,7 @@ import ( "github.com/cs3org/reva/pkg/appctx" ctxuser "github.com/cs3org/reva/pkg/user" "github.com/cs3org/reva/pkg/utils" + "github.com/rs/zerolog" ) const ( @@ -67,20 +68,9 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string) defer span.End() fn := path.Join(ns, r.URL.Path) - depth := r.Header.Get("Depth") - if depth == "" { - depth = "1" - } sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() - // see https://tools.ietf.org/html/rfc4918#section-9.1 - if depth != "0" && depth != "1" && depth != "infinity" { - sublog.Debug().Str("depth", depth).Msgf("invalid Depth header value") - w.WriteHeader(http.StatusBadRequest) - return - } - pf, status, err := readPropfind(r.Body) if err != nil { sublog.Debug().Err(err).Msg("error reading propfind request") @@ -88,134 +78,15 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string) return } - client, err := s.getClient() - if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") - w.WriteHeader(http.StatusInternalServerError) - return - } - - metadataKeys := []string{} - if pf.Allprop != nil { - // TODO this changes the behavior and returns all properties if allprops has been set, - // but allprops should only return some default properties - // see https://tools.ietf.org/html/rfc4918#section-9.1 - // the description of arbitrary_metadata_keys in https://cs3org.github.io/cs3apis/#cs3.storage.provider.v1beta1.ListContainerRequest an others may need clarification - // tracked in https://github.com/cs3org/cs3apis/issues/104 - metadataKeys = append(metadataKeys, "*") - } else { - for i := range pf.Prop { - if requiresExplicitFetching(&pf.Prop[i]) { - metadataKeys = append(metadataKeys, metadataKeyOf(&pf.Prop[i])) - } - } - } ref := &provider.Reference{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} - if info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER && depth == "1" { - req := &provider.ListContainerRequest{ - Ref: ref, - ArbitraryMetadataKeys: metadataKeys, - } - res, err := client.ListContainer(ctx, req) - if err != nil { - sublog.Error().Err(err).Msg("error sending list container grpc request") - w.WriteHeader(http.StatusInternalServerError) - return - } - - if res.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, res.Status) - return - } - infos = append(infos, res.Infos...) - } else if depth == "infinity" { - // FIXME: doesn't work cross-storage as the results will have the wrong paths! - // use a stack to explore sub-containers breadth-first - stack := []string{info.Path} - for len(stack) > 0 { - // retrieve path on top of stack - path := stack[len(stack)-1] - ref = &provider.Reference{Path: path} - req := &provider.ListContainerRequest{ - Ref: ref, - ArbitraryMetadataKeys: metadataKeys, - } - res, err := client.ListContainer(ctx, req) - if err != nil { - sublog.Error().Err(err).Str("path", path).Msg("error sending list container grpc request") - w.WriteHeader(http.StatusInternalServerError) - return - } - if res.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, res.Status) - return - } - - infos = append(infos, res.Infos...) - - if depth != "infinity" { - break - } - - // TODO: stream response to avoid storing too many results in memory - - stack = stack[:len(stack)-1] - - // check sub-containers in reverse order and add them to the stack - // the reversed order here will produce a more logical sorting of results - for i := len(res.Infos) - 1; i >= 0; i-- { - // for i := range res.Infos { - if res.Infos[i].Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { - stack = append(stack, res.Infos[i].Path) - } - } - } - } - propRes, err := s.formatPropfind(ctx, &pf, infos, ns) - if err != nil { - sublog.Error().Err(err).Msg("error formatting propfind") - w.WriteHeader(http.StatusInternalServerError) + parentInfo, resourceInfos, ok := s.getResourceInfos(ctx, w, r, pf, ref, sublog) + if !ok { + // getResourceInfos handles responses in case of an error so we can just return here. return } - w.Header().Set("DAV", "1, 3, extended-mkcol") - w.Header().Set("Content-Type", "application/xml; charset=utf-8") - var disableTus bool - // let clients know this collection supports tus.io POST requests to start uploads - if info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { - if info.Opaque != nil { - _, disableTus = info.Opaque.Map["disable_tus"] - } - if !disableTus { - w.Header().Add("Access-Control-Expose-Headers", "Tus-Resumable, Tus-Version, Tus-Extension") - w.Header().Set("Tus-Resumable", "1.0.0") - w.Header().Set("Tus-Version", "1.0.0") - w.Header().Set("Tus-Extension", "creation,creation-with-upload") - } - } - w.WriteHeader(http.StatusMultiStatus) - if _, err := w.Write([]byte(propRes)); err != nil { - sublog.Err(err).Msg("error writing response") - } + s.propfindResponse(ctx, w, r, ns, pf, parentInfo, resourceInfos, sublog) } func requiresExplicitFetching(n *xml.Name) bool { @@ -918,3 +789,195 @@ type propertyXML struct { // even including the DAV: namespace. InnerXML []byte `xml:",innerxml"` } + +func (s *svc) handleSpacesPropfind(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "propfind") + defer span.End() + + sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Logger() + + pf, status, err := readPropfind(r.Body) + if err != nil { + sublog.Debug().Err(err).Msg("error reading propfind request") + w.WriteHeader(status) + return + } + + // retrieve a specific storage space + ref, rpcStatus, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if rpcStatus.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, rpcStatus) + return + } + + parentInfo, resourceInfos, ok := s.getResourceInfos(ctx, w, r, pf, ref, sublog) + if !ok { + // getResourceInfos handles responses in case of an error so we can just return here. + return + } + + // parentInfo Path is the name but we need / + parentInfo.Path = "/" + + // prefix space id to paths + for i := range resourceInfos { + resourceInfos[i].Path = path.Join("/", spaceID, r.URL.Path, resourceInfos[i].Path) + } + + s.propfindResponse(ctx, w, r, "", pf, parentInfo, resourceInfos, sublog) + +} + +func (s *svc) propfindResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, namespace string, pf propfindXML, parentInfo *provider.ResourceInfo, resourceInfos []*provider.ResourceInfo, log zerolog.Logger) { + propRes, err := s.formatPropfind(ctx, &pf, resourceInfos, namespace) // no namespace because this is relative to the storage space + if err != nil { + log.Error().Err(err).Msg("error formatting propfind") + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set(HeaderDav, "1, 3, extended-mkcol") + w.Header().Set(HeaderContentType, "application/xml; charset=utf-8") + + var disableTus bool + // let clients know this collection supports tus.io POST requests to start uploads + if parentInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + if parentInfo.Opaque != nil { + _, disableTus = parentInfo.Opaque.Map["disable_tus"] + } + if !disableTus { + w.Header().Add(HeaderAccessControlExposeHeaders, strings.Join([]string{HeaderTusResumable, HeaderTusVersion, HeaderTusExtension}, ", ")) + w.Header().Set(HeaderTusResumable, "1.0.0") + w.Header().Set(HeaderTusVersion, "1.0.0") + w.Header().Set(HeaderTusExtension, "creation,creation-with-upload") + } + } + w.WriteHeader(http.StatusMultiStatus) + if _, err := w.Write([]byte(propRes)); err != nil { + log.Err(err).Msg("error writing response") + } +} + +func (s *svc) getResourceInfos(ctx context.Context, w http.ResponseWriter, r *http.Request, pf propfindXML, ref *provider.Reference, log zerolog.Logger) (*provider.ResourceInfo, []*provider.ResourceInfo, bool) { + depth := r.Header.Get(HeaderDepth) + if depth == "" { + depth = "1" + } + + // see https://tools.ietf.org/html/rfc4918#section-9.1 + if depth != "0" && depth != "1" && depth != "infinity" { + log.Debug().Str("depth", depth).Msgf("invalid Depth header value") + w.WriteHeader(http.StatusBadRequest) + return nil, nil, false + } + + // Get the getway client + gatewayClient, err := s.getClient() + if err != nil { + log.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return nil, nil, false + } + + metadataKeys := []string{} + if pf.Allprop != nil { + // TODO this changes the behavior and returns all properties if allprops has been set, + // but allprops should only return some default properties + // see https://tools.ietf.org/html/rfc4918#section-9.1 + // the description of arbitrary_metadata_keys in https://cs3org.github.io/cs3apis/#cs3.storage.provider.v1beta1.ListContainerRequest an others may need clarification + // tracked in https://github.com/cs3org/cs3apis/issues/104 + metadataKeys = append(metadataKeys, "*") + } else { + for i := range pf.Prop { + if requiresExplicitFetching(&pf.Prop[i]) { + metadataKeys = append(metadataKeys, metadataKeyOf(&pf.Prop[i])) + } + } + } + req := &provider.StatRequest{ + Ref: ref, + ArbitraryMetadataKeys: metadataKeys, + } + res, err := gatewayClient.Stat(ctx, req) + if err != nil { + log.Error().Err(err).Interface("req", req).Msg("error sending a grpc stat request") + w.WriteHeader(http.StatusInternalServerError) + return nil, nil, false + } + + if res.Status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&log, w, res.Status) + return nil, nil, false + } + + parentInfo := res.Info + resourceInfos := []*provider.ResourceInfo{parentInfo} + if parentInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER && depth == "1" { + req := &provider.ListContainerRequest{ + Ref: ref, + ArbitraryMetadataKeys: metadataKeys, + } + res, err := gatewayClient.ListContainer(ctx, req) + if err != nil { + log.Error().Err(err).Msg("error sending list container grpc request") + w.WriteHeader(http.StatusInternalServerError) + return nil, nil, false + } + + if res.Status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&log, w, res.Status) + return nil, nil, false + } + resourceInfos = append(resourceInfos, res.Infos...) + } else if depth == "infinity" { + // FIXME: doesn't work cross-storage as the results will have the wrong paths! + // use a stack to explore sub-containers breadth-first + stack := []string{parentInfo.Path} + for len(stack) > 0 { + // retrieve path on top of stack + currentPath := stack[len(stack)-1] + ref = &provider.Reference{Path: currentPath} + req := &provider.ListContainerRequest{ + Ref: ref, + ArbitraryMetadataKeys: metadataKeys, + } + res, err := gatewayClient.ListContainer(ctx, req) + if err != nil { + log.Error().Err(err).Str("path", currentPath).Msg("error sending list container grpc request") + w.WriteHeader(http.StatusInternalServerError) + return nil, nil, false + } + if res.Status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&log, w, res.Status) + return nil, nil, false + } + + resourceInfos = append(resourceInfos, res.Infos...) + + if depth != "infinity" { + break + } + + // TODO: stream response to avoid storing too many results in memory + + stack = stack[:len(stack)-1] + + // check sub-containers in reverse order and add them to the stack + // the reversed order here will produce a more logical sorting of results + for i := len(res.Infos) - 1; i >= 0; i-- { + // for i := range res.Infos { + if res.Infos[i].Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + stack = append(stack, res.Infos[i].Path) + } + } + } + } + + return parentInfo, resourceInfos, true +} diff --git a/internal/http/services/owncloud/ocdav/proppatch.go b/internal/http/services/owncloud/ocdav/proppatch.go index f7dd9277438..e319dd1d715 100644 --- a/internal/http/services/owncloud/ocdav/proppatch.go +++ b/internal/http/services/owncloud/ocdav/proppatch.go @@ -33,16 +33,14 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" "github.com/pkg/errors" + "github.com/rs/zerolog" ) -func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathProppatch(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "proppatch") defer span.End() - acceptedProps := []xml.Name{} - removedProps := []xml.Name{} - fn := path.Join(ns, r.URL.Path) sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() @@ -61,10 +59,10 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) return } + ref := &provider.Reference{Path: fn} + // check if resource exists - statReq := &provider.StatRequest{ - Ref: &provider.Reference{Path: fn}, - } + statReq := &provider.StatRequest{Ref: ref} statRes, err := c.Stat(ctx, statReq) if err != nil { sublog.Error().Err(err).Msg("error sending a grpc stat request") @@ -77,93 +75,19 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) return } - rreq := &provider.UnsetArbitraryMetadataRequest{ - Ref: &provider.Reference{Path: fn}, - ArbitraryMetadataKeys: []string{""}, - } - sreq := &provider.SetArbitraryMetadataRequest{ - Ref: &provider.Reference{Path: fn}, - ArbitraryMetadata: &provider.ArbitraryMetadata{ - Metadata: map[string]string{}, - }, - } - for i := range pp { - if len(pp[i].Props) < 1 { - continue - } - for j := range pp[i].Props { - propNameXML := pp[i].Props[j].XMLName - // don't use path.Join. It removes the double slash! concatenate with a / - key := fmt.Sprintf("%s/%s", pp[i].Props[j].XMLName.Space, pp[i].Props[j].XMLName.Local) - value := string(pp[i].Props[j].InnerXML) - remove := pp[i].Remove - // boolean flags may be "set" to false as well - if s.isBooleanProperty(key) { - // Make boolean properties either "0" or "1" - value = s.as0or1(value) - if value == "0" { - remove = true - } - } - // Webdav spec requires the operations to be executed in the order - // specified in the PROPPATCH request - // http://www.webdav.org/specs/rfc2518.html#rfc.section.8.2 - // FIXME: batch this somehow - if remove { - rreq.ArbitraryMetadataKeys[0] = key - res, err := c.UnsetArbitraryMetadata(ctx, rreq) - if err != nil { - sublog.Error().Err(err).Msg("error sending a grpc UnsetArbitraryMetadata request") - w.WriteHeader(http.StatusInternalServerError) - return - } - - if res.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, res.Status) - return - } - removedProps = append(removedProps, propNameXML) - } else { - sreq.ArbitraryMetadata.Metadata[key] = value - res, err := c.SetArbitraryMetadata(ctx, sreq) - if err != nil { - sublog.Error().Err(err).Str("key", key).Str("value", value).Msg("error sending a grpc SetArbitraryMetadata request") - w.WriteHeader(http.StatusInternalServerError) - return - } - - if res.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, res.Status) - return - } - - acceptedProps = append(acceptedProps, propNameXML) - delete(sreq.ArbitraryMetadata.Metadata, key) - } - } - // FIXME: in case of error, need to set all properties back to the original state, - // and return the error in the matching propstat block, if applicable - // http://www.webdav.org/specs/rfc2518.html#rfc.section.8.2 + acceptedProps, removedProps, ok := s.handleProppatch(ctx, w, r, ref, pp, sublog) + if !ok { + // handleProppatch handles responses in error cases so we can just return + return } - ref := strings.TrimPrefix(fn, ns) - ref = path.Join(ctx.Value(ctxKeyBaseURI).(string), ref) + nRef := strings.TrimPrefix(fn, ns) + nRef = path.Join(ctx.Value(ctxKeyBaseURI).(string), nRef) if statRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { - ref += "/" + nRef += "/" } - propRes, err := s.formatProppatchResponse(ctx, acceptedProps, removedProps, ref) - if err != nil { - sublog.Error().Err(err).Msg("error formatting proppatch response") - w.WriteHeader(http.StatusInternalServerError) - return - } - w.Header().Set("DAV", "1, 3, extended-mkcol") - w.Header().Set("Content-Type", "application/xml; charset=utf-8") - w.WriteHeader(http.StatusMultiStatus) - if _, err := w.Write([]byte(propRes)); err != nil { - sublog.Err(err).Msg("error writing response") - } + s.handleProppatchResponse(ctx, w, r, acceptedProps, removedProps, nRef, sublog) } func (s *svc) formatProppatchResponse(ctx context.Context, acceptedProps []xml.Name, removedProps []xml.Name, ref string) (string, error) { @@ -347,3 +271,165 @@ func next(d *xml.Decoder) (xml.Token, error) { } var errInvalidProppatch = errors.New("webdav: invalid proppatch") + +func (s *svc) handleSpacesProppatch(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces_proppatch") + defer span.End() + + sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Logger() + + pp, status, err := readProppatch(r.Body) + if err != nil { + sublog.Debug().Err(err).Msg("error reading proppatch") + w.WriteHeader(status) + return + } + + // retrieve a specific storage space + ref, rpcStatus, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if rpcStatus.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, rpcStatus) + return + } + + c, err := s.getClient() + if err != nil { + sublog.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return + } + // check if resource exists + statReq := &provider.StatRequest{ + Ref: ref, + } + statRes, err := c.Stat(ctx, statReq) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc stat request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if statRes.Status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, statRes.Status) + return + } + + acceptedProps, removedProps, ok := s.handleProppatch(ctx, w, r, ref, pp, sublog) + if !ok { + // handleProppatch handles responses in error cases so we can just return + return + } + + nRef := path.Join(spaceID, statRes.Info.Path) + nRef = path.Join(ctx.Value(ctxKeyBaseURI).(string), nRef) + if statRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + nRef += "/" + } + + s.handleProppatchResponse(ctx, w, r, acceptedProps, removedProps, nRef, sublog) +} + +func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, patches []Proppatch, log zerolog.Logger) (accepted []xml.Name, removed []xml.Name, ok bool) { + c, err := s.getClient() + if err != nil { + log.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return nil, nil, false + } + + rreq := &provider.UnsetArbitraryMetadataRequest{ + Ref: ref, + ArbitraryMetadataKeys: []string{""}, + } + sreq := &provider.SetArbitraryMetadataRequest{ + Ref: ref, + ArbitraryMetadata: &provider.ArbitraryMetadata{ + Metadata: map[string]string{}, + }, + } + + acceptedProps := []xml.Name{} + removedProps := []xml.Name{} + for i := range patches { + if len(patches[i].Props) < 1 { + continue + } + for j := range patches[i].Props { + propNameXML := patches[i].Props[j].XMLName + // don't use path.Join. It removes the double slash! concatenate with a / + key := fmt.Sprintf("%s/%s", patches[i].Props[j].XMLName.Space, patches[i].Props[j].XMLName.Local) + value := string(patches[i].Props[j].InnerXML) + remove := patches[i].Remove + // boolean flags may be "set" to false as well + if s.isBooleanProperty(key) { + // Make boolean properties either "0" or "1" + value = s.as0or1(value) + if value == "0" { + remove = true + } + } + // Webdav spec requires the operations to be executed in the order + // specified in the PROPPATCH request + // http://www.webdav.org/specs/rfc2518.html#rfc.section.8.2 + // FIXME: batch this somehow + if remove { + rreq.ArbitraryMetadataKeys[0] = key + res, err := c.UnsetArbitraryMetadata(ctx, rreq) + if err != nil { + log.Error().Err(err).Msg("error sending a grpc UnsetArbitraryMetadata request") + w.WriteHeader(http.StatusInternalServerError) + return nil, nil, false + } + + if res.Status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&log, w, res.Status) + return nil, nil, false + } + removedProps = append(removedProps, propNameXML) + } else { + sreq.ArbitraryMetadata.Metadata[key] = value + res, err := c.SetArbitraryMetadata(ctx, sreq) + if err != nil { + log.Error().Err(err).Str("key", key).Str("value", value).Msg("error sending a grpc SetArbitraryMetadata request") + w.WriteHeader(http.StatusInternalServerError) + return nil, nil, false + } + + if res.Status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&log, w, res.Status) + return nil, nil, false + } + + acceptedProps = append(acceptedProps, propNameXML) + delete(sreq.ArbitraryMetadata.Metadata, key) + } + } + // FIXME: in case of error, need to set all properties back to the original state, + // and return the error in the matching propstat block, if applicable + // http://www.webdav.org/specs/rfc2518.html#rfc.section.8.2 + } + + return acceptedProps, removedProps, true +} + +func (s *svc) handleProppatchResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, acceptedProps, removedProps []xml.Name, path string, log zerolog.Logger) { + propRes, err := s.formatProppatchResponse(ctx, acceptedProps, removedProps, path) + if err != nil { + log.Error().Err(err).Msg("error formatting proppatch response") + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set(HeaderDav, "1, 3, extended-mkcol") + w.Header().Set(HeaderContentType, "application/xml; charset=utf-8") + w.WriteHeader(http.StatusMultiStatus) + if _, err := w.Write([]byte(propRes)); err != nil { + log.Err(err).Msg("error writing response") + } +} diff --git a/internal/http/services/owncloud/ocdav/publicfile.go b/internal/http/services/owncloud/ocdav/publicfile.go index c5670014f07..69085d8b40e 100644 --- a/internal/http/services/owncloud/ocdav/publicfile.go +++ b/internal/http/services/owncloud/ocdav/publicfile.go @@ -51,34 +51,34 @@ func (h *PublicFileHandler) Handler(s *svc) http.Handler { if relativePath != "" && relativePath != "/" { // accessing the file // PROPFIND has an implicit call - if r.Method != "PROPFIND" && !s.adjustResourcePathInURL(w, r) { + if r.Method != MethodPropfind && !s.adjustResourcePathInURL(w, r) { return } r.URL.Path = path.Base(r.URL.Path) switch r.Method { - case "PROPFIND": + case MethodPropfind: s.handlePropfindOnToken(w, r, h.namespace, false) case http.MethodGet: - s.handleGet(w, r, h.namespace) + s.handlePathGet(w, r, h.namespace) case http.MethodOptions: s.handleOptions(w, r, h.namespace) case http.MethodHead: - s.handleHead(w, r, h.namespace) + s.handlePathHead(w, r, h.namespace) case http.MethodPut: - s.handlePut(w, r, h.namespace) + s.handlePathPut(w, r, h.namespace) default: w.WriteHeader(http.StatusMethodNotAllowed) } } else { // accessing the virtual parent folder switch r.Method { - case "PROPFIND": + case MethodPropfind: s.handlePropfindOnToken(w, r, h.namespace, true) case http.MethodOptions: s.handleOptions(w, r, h.namespace) case http.MethodHead: - s.handleHead(w, r, h.namespace) + s.handlePathHead(w, r, h.namespace) default: w.WriteHeader(http.StatusMethodNotAllowed) } diff --git a/internal/http/services/owncloud/ocdav/put.go b/internal/http/services/owncloud/ocdav/put.go index 7a9ad49963b..b5199e0b66e 100644 --- a/internal/http/services/owncloud/ocdav/put.go +++ b/internal/http/services/owncloud/ocdav/put.go @@ -19,7 +19,7 @@ package ocdav import ( - "io" + "context" "net/http" "path" "strconv" @@ -35,11 +35,12 @@ import ( "github.com/cs3org/reva/pkg/rhttp" "github.com/cs3org/reva/pkg/storage/utils/chunking" "github.com/cs3org/reva/pkg/utils" + "github.com/rs/zerolog" "go.opencensus.io/trace" ) func sufferMacOSFinder(r *http.Request) bool { - return r.Header.Get("X-Expected-Entity-Length") != "" + return r.Header.Get(HeaderExpectedEntityLength) != "" } func handleMacOSFinder(w http.ResponseWriter, r *http.Request) error { @@ -61,8 +62,8 @@ func handleMacOSFinder(w http.ResponseWriter, r *http.Request) error { */ log := appctx.GetLogger(r.Context()) - content := r.Header.Get("Content-Length") - expected := r.Header.Get("X-Expected-Entity-Length") + content := r.Header.Get(HeaderContentLength) + expected := r.Header.Get(HeaderExpectedEntityLength) log.Warn().Str("content-length", content).Str("x-expected-entity-length", expected).Msg("Mac OS Finder corner-case detected") // The best mitigation to this problem is to tell users to not use crappy Finder. @@ -100,87 +101,66 @@ func isContentRange(r *http.Request) bool { in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject all PUT requests with a Content-Range for now. */ - return r.Header.Get("Content-Range") != "" + return r.Header.Get(HeaderContentRange) != "" } -func (s *svc) handlePut(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathPut(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "put") + defer span.End() + fn := path.Join(ns, r.URL.Path) sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() - if r.Body == nil { - sublog.Debug().Msg("body is nil") - w.WriteHeader(http.StatusBadRequest) - return - } + ref := &provider.Reference{Path: fn} - if isContentRange(r) { - sublog.Debug().Msg("Content-Range not supported for PUT") - w.WriteHeader(http.StatusNotImplemented) - return - } + s.handlePut(ctx, w, r, ref, sublog) +} - if sufferMacOSFinder(r) { - err := handleMacOSFinder(w, r) - if err != nil { - sublog.Debug().Err(err).Msg("error handling Mac OS corner-case") - w.WriteHeader(http.StatusInternalServerError) - return - } +func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) { + if !checkPreconditions(w, r, log) { + // checkPreconditions handles error returns + return } - length, err := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64) + length, err := getContentLength(w, r) if err != nil { - // Fallback to Upload-Length - length, err = strconv.ParseInt(r.Header.Get("Upload-Length"), 10, 64) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - return - } + w.WriteHeader(http.StatusBadRequest) + return } - s.handlePutHelper(w, r, r.Body, fn, length) -} - -func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io.Reader, fn string, length int64) { - ctx := r.Context() - ctx, span := trace.StartSpan(ctx, "put") - defer span.End() - - sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() client, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } - ref := &provider.Reference{Path: fn} sReq := &provider.StatRequest{Ref: ref} sRes, err := client.Stat(ctx, sReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - HandleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&log, w, sRes.Status) return } info := sRes.Info if info != nil { if info.Type != provider.ResourceType_RESOURCE_TYPE_FILE { - sublog.Debug().Msg("resource is not a file") + log.Debug().Msg("resource is not a file") w.WriteHeader(http.StatusConflict) return } - clientETag := r.Header.Get("If-Match") + clientETag := r.Header.Get(HeaderIfMatch) serverETag := info.Etag if clientETag != "" { if clientETag != serverETag { - sublog.Debug().Str("client-etag", clientETag).Str("server-etag", serverETag).Msg("etags mismatch") + log.Debug().Str("client-etag", clientETag).Str("server-etag", serverETag).Msg("etags mismatch") w.WriteHeader(http.StatusPreconditionFailed) return } @@ -188,38 +168,38 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io } opaqueMap := map[string]*typespb.OpaqueEntry{ - "Upload-Length": { + HeaderUploadLength: { Decoder: "plain", Value: []byte(strconv.FormatInt(length, 10)), }, } - if mtime := r.Header.Get("X-OC-Mtime"); mtime != "" { - opaqueMap["X-OC-Mtime"] = &typespb.OpaqueEntry{ + if mtime := r.Header.Get(HeaderOCMtime); mtime != "" { + opaqueMap[HeaderOCMtime] = &typespb.OpaqueEntry{ Decoder: "plain", Value: []byte(mtime), } // TODO: find a way to check if the storage really accepted the value - w.Header().Set("X-OC-Mtime", "accepted") + w.Header().Set(HeaderOCMtime, "accepted") } // curl -X PUT https://demo.owncloud.com/remote.php/webdav/testcs.bin -u demo:demo -d '123' -v -H 'OC-Checksum: SHA1:40bd001563085fc35165329ea1ff5c5ecbdbbeef' var cparts []string // TUS Upload-Checksum header takes precedence - if checksum := r.Header.Get("Upload-Checksum"); checksum != "" { + if checksum := r.Header.Get(HeaderUploadChecksum); checksum != "" { cparts = strings.SplitN(checksum, " ", 2) if len(cparts) != 2 { - sublog.Debug().Str("upload-checksum", checksum).Msg("invalid Upload-Checksum format, expected '[algorithm] [checksum]'") + log.Debug().Str("upload-checksum", checksum).Msg("invalid Upload-Checksum format, expected '[algorithm] [checksum]'") w.WriteHeader(http.StatusBadRequest) return } // Then try owncloud header - } else if checksum := r.Header.Get("OC-Checksum"); checksum != "" { + } else if checksum := r.Header.Get(HeaderOCChecksum); checksum != "" { cparts = strings.SplitN(checksum, ":", 2) if len(cparts) != 2 { - sublog.Debug().Str("oc-checksum", checksum).Msg("invalid OC-Checksum format, expected '[algorithm]:[checksum]'") + log.Debug().Str("oc-checksum", checksum).Msg("invalid OC-Checksum format, expected '[algorithm]:[checksum]'") w.WriteHeader(http.StatusBadRequest) return } @@ -227,7 +207,7 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io // we do not check the algorithm here, because it might depend on the storage if len(cparts) == 2 { // Translate into TUS style Upload-Checksum header - opaqueMap["Upload-Checksum"] = &typespb.OpaqueEntry{ + opaqueMap[HeaderUploadChecksum] = &typespb.OpaqueEntry{ Decoder: "plain", // algorithm is always lowercase, checksum is separated by space Value: []byte(strings.ToLower(cparts[0]) + " " + cparts[1]), @@ -242,13 +222,13 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io // where to upload the file? uRes, err := client.InitiateFileUpload(ctx, uReq) if err != nil { - sublog.Error().Err(err).Msg("error initiating file upload") + log.Error().Err(err).Msg("error initiating file upload") w.WriteHeader(http.StatusInternalServerError) return } if uRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, uRes.Status) + HandleErrorStatus(&log, w, uRes.Status) return } @@ -260,7 +240,7 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io } if length > 0 { - httpReq, err := rhttp.NewRequest(ctx, "PUT", ep, content) + httpReq, err := rhttp.NewRequest(ctx, http.MethodPut, ep, r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) return @@ -269,7 +249,7 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io httpRes, err := s.client.Do(httpReq) if err != nil { - sublog.Error().Err(err).Msg("error doing PUT request to data service") + log.Error().Err(err).Msg("error doing PUT request to data service") w.WriteHeader(http.StatusInternalServerError) return } @@ -286,29 +266,29 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io message: "The computed checksum does not match the one received from the client.", }) if err != nil { - sublog.Error().Msgf("error marshaling xml response: %s", b) + log.Error().Msgf("error marshaling xml response: %s", b) w.WriteHeader(http.StatusInternalServerError) return } _, err = w.Write(b) if err != nil { - sublog.Err(err).Msg("error writing response") + log.Err(err).Msg("error writing response") } return } - sublog.Error().Err(err).Msg("PUT request to data server failed") + log.Error().Err(err).Msg("PUT request to data server failed") w.WriteHeader(httpRes.StatusCode) return } } - ok, err := chunking.IsChunked(fn) + ok, err := chunking.IsChunked(ref.Path) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if ok { - chunk, err := chunking.GetChunkBLOBInfo(fn) + chunk, err := chunking.GetChunkBLOBInfo(ref.Path) if err != nil { w.WriteHeader(http.StatusInternalServerError) return @@ -319,25 +299,25 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io // stat again to check the new file's metadata sRes, err = client.Stat(ctx, sReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if sRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&log, w, sRes.Status) return } newInfo := sRes.Info - w.Header().Add("Content-Type", newInfo.MimeType) - w.Header().Set("ETag", newInfo.Etag) - w.Header().Set("OC-FileId", wrapResourceID(newInfo.Id)) - w.Header().Set("OC-ETag", newInfo.Etag) + w.Header().Add(HeaderContentType, newInfo.MimeType) + w.Header().Set(HeaderETag, newInfo.Etag) + w.Header().Set(HeaderOCFileID, wrapResourceID(newInfo.Id)) + w.Header().Set(HeaderOCETag, newInfo.Etag) t := utils.TSToTime(newInfo.Mtime).UTC() lastModifiedString := t.Format(time.RFC1123Z) - w.Header().Set("Last-Modified", lastModifiedString) + w.Header().Set(HeaderLastModified, lastModifiedString) // file was new if info == nil { @@ -348,3 +328,53 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io // overwrite w.WriteHeader(http.StatusNoContent) } + +func (s *svc) handleSpacesPut(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + + sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Logger() + + spaceRef, status, err := s.lookUpStorageSpaceReference(ctx, spaceID, r.URL.Path) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) + return + } + + s.handlePut(ctx, w, r, spaceRef, sublog) +} + +func checkPreconditions(w http.ResponseWriter, r *http.Request, log zerolog.Logger) bool { + if isContentRange(r) { + log.Debug().Msg("Content-Range not supported for PUT") + w.WriteHeader(http.StatusNotImplemented) + return false + } + + if sufferMacOSFinder(r) { + err := handleMacOSFinder(w, r) + if err != nil { + log.Debug().Err(err).Msg("error handling Mac OS corner-case") + w.WriteHeader(http.StatusInternalServerError) + return false + } + } + return true +} + +func getContentLength(w http.ResponseWriter, r *http.Request) (int64, error) { + length, err := strconv.ParseInt(r.Header.Get(HeaderContentLength), 10, 64) + if err != nil { + // Fallback to Upload-Length + length, err = strconv.ParseInt(r.Header.Get(HeaderUploadLength), 10, 64) + if err != nil { + return 0, err + } + } + return length, nil +} diff --git a/internal/http/services/owncloud/ocdav/spaces.go b/internal/http/services/owncloud/ocdav/spaces.go new file mode 100644 index 00000000000..309d20ae3ca --- /dev/null +++ b/internal/http/services/owncloud/ocdav/spaces.go @@ -0,0 +1,132 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocdav + +import ( + "context" + "fmt" + "net/http" + + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/pkg/rhttp/router" + "github.com/cs3org/reva/pkg/utils" +) + +// SpacesHandler handles trashbin requests +type SpacesHandler struct { + gatewaySvc string +} + +func (h *SpacesHandler) init(c *Config) error { + h.gatewaySvc = c.GatewaySvc + return nil +} + +// Handler handles requests +func (h *SpacesHandler) Handler(s *svc) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // ctx := r.Context() + // log := appctx.GetLogger(ctx) + + if r.Method == http.MethodOptions { + s.handleOptions(w, r, "spaces") + return + } + + var spaceID string + spaceID, r.URL.Path = router.ShiftPath(r.URL.Path) + + if spaceID == "" { + // listing is disabled, no auth will change that + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + switch r.Method { + case MethodPropfind: + s.handleSpacesPropfind(w, r, spaceID) + case MethodProppatch: + s.handleSpacesProppatch(w, r, spaceID) + case MethodLock: + s.handleLock(w, r, spaceID) + case MethodUnlock: + s.handleUnlock(w, r, spaceID) + case MethodMkcol: + s.handleSpacesMkCol(w, r, spaceID) + case MethodMove: + s.handleSpacesMove(w, r, spaceID) + case MethodCopy: + s.handleSpacesCopy(w, r, spaceID) + case MethodReport: + s.handleReport(w, r, spaceID) + case http.MethodGet: + s.handleSpacesGet(w, r, spaceID) + case http.MethodPut: + s.handleSpacesPut(w, r, spaceID) + case http.MethodPost: + s.handleSpacesTusPost(w, r, spaceID) + case http.MethodOptions: + s.handleOptions(w, r, spaceID) + case http.MethodHead: + s.handleSpacesHead(w, r, spaceID) + case http.MethodDelete: + s.handleSpacesDelete(w, r, spaceID) + default: + http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) + } + }) +} + +func (s *svc) lookUpStorageSpaceReference(ctx context.Context, spaceID string, relativePath string) (*storageProvider.Reference, *rpc.Status, error) { + // Get the getway client + gatewayClient, err := s.getClient() + if err != nil { + return nil, nil, err + } + + // retrieve a specific storage space + lSSReq := &storageProvider.ListStorageSpacesRequest{ + Filters: []*storageProvider.ListStorageSpacesRequest_Filter{ + { + Type: storageProvider.ListStorageSpacesRequest_Filter_TYPE_ID, + Term: &storageProvider.ListStorageSpacesRequest_Filter_Id{ + Id: &storageProvider.StorageSpaceId{ + OpaqueId: spaceID, + }, + }, + }, + }, + } + + lSSRes, err := gatewayClient.ListStorageSpaces(ctx, lSSReq) + if err != nil || lSSRes.Status.Code != rpc.Code_CODE_OK { + return nil, lSSRes.Status, err + } + + if len(lSSRes.StorageSpaces) != 1 { + return nil, nil, fmt.Errorf("unexpected number of spaces") + } + space := lSSRes.StorageSpaces[0] + + return &storageProvider.Reference{ + ResourceId: space.Root, + Path: utils.MakeRelativePath(relativePath), + }, lSSRes.Status, nil +} diff --git a/internal/http/services/owncloud/ocdav/trashbin.go b/internal/http/services/owncloud/ocdav/trashbin.go index cbc992f922b..ae81db1291c 100644 --- a/internal/http/services/owncloud/ocdav/trashbin.go +++ b/internal/http/services/owncloud/ocdav/trashbin.go @@ -107,11 +107,11 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler { // return //} - if key == "" && r.Method == "PROPFIND" { + if key == "" && r.Method == MethodPropfind { h.listTrashbin(w, r, s, u) return } - if key != "" && r.Method == "MOVE" { + if key != "" && r.Method == MethodMove { // find path in url relative to trash base trashBase := ctx.Value(ctxKeyBaseURI).(string) baseURI := path.Join(path.Dir(trashBase), "files", username) @@ -119,8 +119,7 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler { r = r.WithContext(ctx) // TODO make request.php optional in destination header - dstHeader := r.Header.Get("Destination") - dst, err := extractDestination(dstHeader, baseURI) + dst, err := extractDestination(r) if err != nil { w.WriteHeader(http.StatusBadRequest) return @@ -133,7 +132,7 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler { return } - if r.Method == "DELETE" { + if r.Method == http.MethodDelete { h.delete(w, r, s, u, key) return } @@ -393,9 +392,7 @@ func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc // this means we can only undelete on the same storage, not to a different folder // use the key which is prefixed with the StoragePath to lookup the correct storage ... // TODO currently limited to the home storage - Ref: &provider.Reference{ - Path: getHomeRes.Path, - }, + Ref: &provider.Reference{Path: getHomeRes.Path}, Key: key, RestoreRef: &provider.Reference{Path: dst}, } @@ -454,12 +451,10 @@ func (h *TrashbinHandler) delete(w http.ResponseWriter, r *http.Request, s *svc, // storage drives PurgeRecycleItem key call req := &gateway.PurgeRecycleRequest{ - Ref: &provider.Reference{ - ResourceId: &provider.ResourceId{ - StorageId: sRes.Info.Id.StorageId, - OpaqueId: key, - }, - }, + Ref: &provider.Reference{ResourceId: &provider.ResourceId{ + StorageId: sRes.Info.Id.StorageId, + OpaqueId: key, + }}, } res, err := client.PurgeRecycle(ctx, req) diff --git a/internal/http/services/owncloud/ocdav/tus.go b/internal/http/services/owncloud/ocdav/tus.go index 8fb4c7a1a15..d694d3f0ab1 100644 --- a/internal/http/services/owncloud/ocdav/tus.go +++ b/internal/http/services/owncloud/ocdav/tus.go @@ -19,6 +19,7 @@ package ocdav import ( + "context" "net/http" "path" "strconv" @@ -31,28 +32,75 @@ import ( "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/rhttp" "github.com/cs3org/reva/pkg/utils" + "github.com/rs/zerolog" tusd "github.com/tus/tusd/pkg/handler" "go.opencensus.io/trace" ) -func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathTusPost(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "tus-post") defer span.End() - w.Header().Add("Access-Control-Allow-Headers", "Tus-Resumable, Upload-Length, Upload-Metadata, If-Match") - w.Header().Add("Access-Control-Expose-Headers", "Tus-Resumable, Location") + // read filename from metadata + meta := tusd.ParseMetadataHeader(r.Header.Get(HeaderUploadMetadata)) + if meta["filename"] == "" { + w.WriteHeader(http.StatusPreconditionFailed) + return + } + + // append filename to current dir + fn := path.Join(ns, r.URL.Path, meta["filename"]) + + sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() + // check tus headers? + + ref := &provider.Reference{Path: fn} + s.handleTusPost(ctx, w, r, meta, ref, sublog) +} + +func (s *svc) handleSpacesTusPost(w http.ResponseWriter, r *http.Request, spaceID string) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "spaces-tus-post") + defer span.End() + + // read filename from metadata + meta := tusd.ParseMetadataHeader(r.Header.Get(HeaderUploadMetadata)) + if meta["filename"] == "" { + w.WriteHeader(http.StatusPreconditionFailed) + return + } - w.Header().Set("Tus-Resumable", "1.0.0") + sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Logger() + + spaceRef, status, err := s.lookUpStorageSpaceReference(ctx, spaceID, path.Join(r.URL.Path, meta["filename"])) + if err != nil { + sublog.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&sublog, w, status) + return + } + + s.handleTusPost(ctx, w, r, meta, spaceRef, sublog) +} + +func (s *svc) handleTusPost(ctx context.Context, w http.ResponseWriter, r *http.Request, meta map[string]string, ref *provider.Reference, log zerolog.Logger) { + w.Header().Add(HeaderAccessControlAllowHeaders, strings.Join([]string{HeaderTusResumable, HeaderUploadLength, HeaderUploadMetadata, HeaderIfMatch}, ", ")) + w.Header().Add(HeaderAccessControlExposeHeaders, strings.Join([]string{HeaderTusResumable, HeaderLocation}, ", ")) + + w.Header().Set(HeaderTusResumable, "1.0.0") // Test if the version sent by the client is supported // GET methods are not checked since a browser may visit this URL and does // not include this header. This request is not part of the specification. - if r.Header.Get("Tus-Resumable") != "1.0.0" { + if r.Header.Get(HeaderTusResumable) != "1.0.0" { w.WriteHeader(http.StatusPreconditionFailed) return } - if r.Header.Get("Upload-Length") == "" { + if r.Header.Get(HeaderUploadLength) == "" { w.WriteHeader(http.StatusPreconditionFailed) return } @@ -61,56 +109,41 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { // curl -X PUT https://demo.owncloud.com/remote.php/webdav/testcs.bin -u demo:demo -d '123' -v -H 'OC-Checksum: SHA1:40bd001563085fc35165329ea1ff5c5ecbdbbeef' // TODO check Expect: 100-continue - - // read filename from metadata - meta := tusd.ParseMetadataHeader(r.Header.Get("Upload-Metadata")) - if meta["filename"] == "" { - w.WriteHeader(http.StatusPreconditionFailed) - return - } - - // append filename to current dir - fn := path.Join(ns, r.URL.Path, meta["filename"]) - - sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() - // check tus headers? - // check if destination exists or is a file client, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } - sReq := &provider.StatRequest{ - Ref: &provider.Reference{Path: fn}, + Ref: ref, } sRes, err := client.Stat(ctx, sReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - HandleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&log, w, sRes.Status) return } info := sRes.Info if info != nil && info.Type != provider.ResourceType_RESOURCE_TYPE_FILE { - sublog.Warn().Msg("resource is not a file") + log.Warn().Msg("resource is not a file") w.WriteHeader(http.StatusConflict) return } if info != nil { - clientETag := r.Header.Get("If-Match") + clientETag := r.Header.Get(HeaderIfMatch) serverETag := info.Etag if clientETag != "" { if clientETag != serverETag { - sublog.Warn().Str("client-etag", clientETag).Str("server-etag", serverETag).Msg("etags mismatch") + log.Warn().Str("client-etag", clientETag).Str("server-etag", serverETag).Msg("etags mismatch") w.WriteHeader(http.StatusPreconditionFailed) return } @@ -118,15 +151,15 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { } opaqueMap := map[string]*typespb.OpaqueEntry{ - "Upload-Length": { + HeaderUploadLength: { Decoder: "plain", - Value: []byte(r.Header.Get("Upload-Length")), + Value: []byte(r.Header.Get(HeaderUploadLength)), }, } mtime := meta["mtime"] if mtime != "" { - opaqueMap["X-OC-Mtime"] = &typespb.OpaqueEntry{ + opaqueMap[HeaderOCMtime] = &typespb.OpaqueEntry{ Decoder: "plain", Value: []byte(mtime), } @@ -134,7 +167,7 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { // initiateUpload uReq := &provider.InitiateFileUploadRequest{ - Ref: &provider.Reference{Path: fn}, + Ref: ref, Opaque: &typespb.Opaque{ Map: opaqueMap, }, @@ -142,13 +175,13 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { uRes, err := client.InitiateFileUpload(ctx, uReq) if err != nil { - sublog.Error().Err(err).Msg("error initiating file upload") + log.Error().Err(err).Msg("error initiating file upload") w.WriteHeader(http.StatusInternalServerError) return } if uRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, uRes.Status) + HandleErrorStatus(&log, w, uRes.Status) return } @@ -168,15 +201,15 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { ep += token } - w.Header().Set("Location", ep) + w.Header().Set(HeaderLocation, ep) // for creation-with-upload extension forward bytes to dataprovider // TODO check this really streams - if r.Header.Get("Content-Type") == "application/offset+octet-stream" { + if r.Header.Get(HeaderContentType) == "application/offset+octet-stream" { - length, err := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64) + length, err := strconv.ParseInt(r.Header.Get(HeaderContentLength), 10, 64) if err != nil { - sublog.Debug().Err(err).Msg("wrong request") + log.Debug().Err(err).Msg("wrong request") w.WriteHeader(http.StatusBadRequest) return } @@ -184,73 +217,73 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { var httpRes *http.Response if length != 0 { - httpReq, err := rhttp.NewRequest(ctx, "PATCH", ep, r.Body) + httpReq, err := rhttp.NewRequest(ctx, http.MethodPatch, ep, r.Body) if err != nil { - sublog.Debug().Err(err).Msg("wrong request") + log.Debug().Err(err).Msg("wrong request") w.WriteHeader(http.StatusInternalServerError) return } - httpReq.Header.Set("Content-Type", r.Header.Get("Content-Type")) - httpReq.Header.Set("Content-Length", r.Header.Get("Content-Length")) - if r.Header.Get("Upload-Offset") != "" { - httpReq.Header.Set("Upload-Offset", r.Header.Get("Upload-Offset")) + httpReq.Header.Set(HeaderContentType, r.Header.Get(HeaderContentType)) + httpReq.Header.Set(HeaderContentLength, r.Header.Get(HeaderContentLength)) + if r.Header.Get(HeaderUploadOffset) != "" { + httpReq.Header.Set(HeaderUploadOffset, r.Header.Get(HeaderUploadOffset)) } else { - httpReq.Header.Set("Upload-Offset", "0") + httpReq.Header.Set(HeaderUploadOffset, "0") } - httpReq.Header.Set("Tus-Resumable", r.Header.Get("Tus-Resumable")) + httpReq.Header.Set(HeaderTusResumable, r.Header.Get(HeaderTusResumable)) httpRes, err = s.client.Do(httpReq) if err != nil { - sublog.Error().Err(err).Msg("error doing GET request to data service") + log.Error().Err(err).Msg("error doing GET request to data service") w.WriteHeader(http.StatusInternalServerError) return } defer httpRes.Body.Close() - w.Header().Set("Upload-Offset", httpRes.Header.Get("Upload-Offset")) - w.Header().Set("Tus-Resumable", httpRes.Header.Get("Tus-Resumable")) + w.Header().Set(HeaderUploadOffset, httpRes.Header.Get(HeaderUploadOffset)) + w.Header().Set(HeaderTusResumable, httpRes.Header.Get(HeaderTusResumable)) if httpRes.StatusCode != http.StatusNoContent { w.WriteHeader(httpRes.StatusCode) return } } else { - sublog.Debug().Msg("Skipping sending a Patch request as body is empty") + log.Debug().Msg("Skipping sending a Patch request as body is empty") } // check if upload was fully completed - if length == 0 || httpRes.Header.Get("Upload-Offset") == r.Header.Get("Upload-Length") { + if length == 0 || httpRes.Header.Get(HeaderUploadOffset) == r.Header.Get(HeaderUploadOffset) { // get uploaded file metadata sRes, err := client.Stat(ctx, sReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - HandleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&log, w, sRes.Status) return } info := sRes.Info if info == nil { - sublog.Error().Msg("No info found for uploaded file") + log.Error().Msg("No info found for uploaded file") w.WriteHeader(http.StatusInternalServerError) return } - if httpRes != nil && httpRes.Header != nil && httpRes.Header.Get("X-OC-Mtime") != "" { + if httpRes != nil && httpRes.Header != nil && httpRes.Header.Get(HeaderOCMtime) != "" { // set the "accepted" value if returned in the upload response headers - w.Header().Set("X-OC-Mtime", httpRes.Header.Get("X-OC-Mtime")) + w.Header().Set(HeaderOCMtime, httpRes.Header.Get(HeaderOCMtime)) } - w.Header().Set("Content-Type", info.MimeType) - w.Header().Set("OC-FileId", wrapResourceID(info.Id)) - w.Header().Set("OC-ETag", info.Etag) - w.Header().Set("ETag", info.Etag) + w.Header().Set(HeaderContentType, info.MimeType) + w.Header().Set(HeaderOCFileID, wrapResourceID(info.Id)) + w.Header().Set(HeaderOCETag, info.Etag) + w.Header().Set(HeaderETag, info.Etag) t := utils.TSToTime(info.Mtime).UTC() lastModifiedString := t.Format(time.RFC1123Z) - w.Header().Set("Last-Modified", lastModifiedString) + w.Header().Set(HeaderLastModified, lastModifiedString) } } diff --git a/internal/http/services/owncloud/ocdav/versions.go b/internal/http/services/owncloud/ocdav/versions.go index af3644cbdfb..7706b2f5451 100644 --- a/internal/http/services/owncloud/ocdav/versions.go +++ b/internal/http/services/owncloud/ocdav/versions.go @@ -62,11 +62,11 @@ func (h *VersionsHandler) Handler(s *svc, rid *provider.ResourceId) http.Handler s.handleOptions(w, r, "versions") return } - if key == "" && r.Method == "PROPFIND" { + if key == "" && r.Method == MethodPropfind { h.doListVersions(w, r, s, rid) return } - if key != "" && r.Method == "COPY" { + if key != "" && r.Method == MethodCopy { // TODO(jfd) it seems we cannot directly GET version content with cs3 ... // TODO(jfd) cs3api has no delete file version call // TODO(jfd) restore version to given Destination, but cs3api has no destination @@ -98,8 +98,8 @@ func (h *VersionsHandler) doListVersions(w http.ResponseWriter, r *http.Request, w.WriteHeader(http.StatusInternalServerError) return } - ref := &provider.Reference{ResourceId: rid} + res, err := client.Stat(ctx, &provider.StatRequest{Ref: ref}) if err != nil { sublog.Error().Err(err).Msg("error sending a grpc stat request") diff --git a/internal/http/services/owncloud/ocdav/webdav.go b/internal/http/services/owncloud/ocdav/webdav.go index b8693ef97fc..a98714a5bf8 100644 --- a/internal/http/services/owncloud/ocdav/webdav.go +++ b/internal/http/services/owncloud/ocdav/webdav.go @@ -23,6 +23,56 @@ import ( "path" ) +// Common Webdav methods. +// +// Unless otherwise noted, these are defined in RFC 4918 section 9. +const ( + MethodPropfind = "PROPFIND" + MethodLock = "LOCK" + MethodUnlock = "UNLOCK" + MethodProppatch = "PROPPATCH" + MethodMkcol = "MKCOL" + MethodMove = "MOVE" + MethodCopy = "COPY" + MethodReport = "REPORT" +) + +// Common HTTP headers. +const ( + HeaderAcceptRanges = "Accept-Ranges" + HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" + HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" + HeaderContentDisposistion = "Content-Disposition" + HeaderContentLength = "Content-Length" + HeaderContentRange = "Content-Range" + HeaderContentType = "Content-Type" + HeaderETag = "ETag" + HeaderLastModified = "Last-Modified" + HeaderLocation = "Location" + HeaderRange = "Range" + HeaderIfMatch = "If-Match" +) + +// Non standard HTTP headers. +const ( + HeaderOCFileID = "OC-FileId" + HeaderOCETag = "OC-ETag" + HeaderOCChecksum = "OC-Checksum" + HeaderDepth = "Depth" + HeaderDav = "DAV" + HeaderTusResumable = "Tus-Resumable" + HeaderTusVersion = "Tus-Version" + HeaderTusExtension = "Tus-Extension" + HeaderDestination = "Destination" + HeaderOverwrite = "Overwrite" + HeaderUploadChecksum = "Upload-Checksum" + HeaderUploadLength = "Upload-Length" + HeaderUploadMetadata = "Upload-Metadata" + HeaderUploadOffset = "Upload-Offset" + HeaderOCMtime = "X-OC-Mtime" + HeaderExpectedEntityLength = "X-Expected-Entity-Length" +) + // WebDavHandler implements a dav endpoint type WebDavHandler struct { namespace string @@ -40,34 +90,34 @@ func (h *WebDavHandler) Handler(s *svc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ns := applyLayout(r.Context(), h.namespace, h.useLoggedInUserNS, r.URL.Path) switch r.Method { - case "PROPFIND": + case MethodPropfind: s.handlePropfind(w, r, ns) - case "LOCK": + case MethodLock: s.handleLock(w, r, ns) - case "UNLOCK": + case MethodUnlock: s.handleUnlock(w, r, ns) - case "PROPPATCH": - s.handleProppatch(w, r, ns) - case "MKCOL": - s.handleMkcol(w, r, ns) - case "MOVE": - s.handleMove(w, r, ns) - case "COPY": - s.handleCopy(w, r, ns) - case "REPORT": + case MethodProppatch: + s.handlePathProppatch(w, r, ns) + case MethodMkcol: + s.handlePathMkcol(w, r, ns) + case MethodMove: + s.handlePathMove(w, r, ns) + case MethodCopy: + s.handlePathCopy(w, r, ns) + case MethodReport: s.handleReport(w, r, ns) case http.MethodGet: - s.handleGet(w, r, ns) + s.handlePathGet(w, r, ns) case http.MethodPut: - s.handlePut(w, r, ns) + s.handlePathPut(w, r, ns) case http.MethodPost: - s.handleTusPost(w, r, ns) + s.handlePathTusPost(w, r, ns) case http.MethodOptions: s.handleOptions(w, r, ns) case http.MethodHead: - s.handleHead(w, r, ns) + s.handlePathHead(w, r, ns) case http.MethodDelete: - s.handleDelete(w, r, ns) + s.handlePathDelete(w, r, ns) default: w.WriteHeader(http.StatusNotFound) } 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 624df6bcf96..4416ae14cb5 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 @@ -506,9 +506,7 @@ func (h *Handler) updateShare(w http.ResponseWriter, r *http.Request, shareID st return } - statReq := provider.StatRequest{Ref: &provider.Reference{ - ResourceId: uRes.Share.ResourceId, - }} + statReq := provider.StatRequest{Ref: &provider.Reference{ResourceId: uRes.Share.ResourceId}} statRes, err := client.Stat(r.Context(), &statReq) if err != nil { diff --git a/pkg/app/provider/demo/demo.go b/pkg/app/provider/demo/demo.go index 28134083e57..361a8616038 100644 --- a/pkg/app/provider/demo/demo.go +++ b/pkg/app/provider/demo/demo.go @@ -33,6 +33,7 @@ type provider struct { } func (p *provider) GetIFrame(ctx context.Context, resID *providerpb.Reference, token string) (string, error) { + // TODO @labkode @ishank011 should we use !: here? Or verify resource id is set and path is empty? or change signature to use a ResourceID directly? msg := fmt.Sprintf("