diff --git a/graph/pkg/service/v0/driveitems.go b/graph/pkg/service/v0/driveitems.go new file mode 100644 index 0000000000..870aee2d81 --- /dev/null +++ b/graph/pkg/service/v0/driveitems.go @@ -0,0 +1,144 @@ +package svc + +import ( + "context" + "net/http" + "path" + "path/filepath" + "time" + + cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/go-chi/render" + libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/graph/pkg/service/v0/errorcode" +) + +// GetRootDriveChildren implements the Service interface. +func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { + g.logger.Info().Msg("Calling GetRootDriveChildren") + ctx := r.Context() + + client, err := g.GetClient() + if err != nil { + g.logger.Error().Err(err).Msg("could not get client") + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + res, err := client.GetHome(ctx, &storageprovider.GetHomeRequest{}) + switch { + case err != nil: + g.logger.Error().Err(err).Msg("error sending get home grpc request") + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) + return + case res.Status.Code != cs3rpc.Code_CODE_OK: + if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND { + errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message) + return + } + g.logger.Error().Err(err).Msg("error sending get home grpc request") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message) + return + } + + lRes, err := client.ListContainer(ctx, &storageprovider.ListContainerRequest{ + Ref: &storageprovider.Reference{ + Path: res.Path, + }, + }) + switch { + case err != nil: + g.logger.Error().Err(err).Msg("error sending list container grpc request") + errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) + return + case res.Status.Code != cs3rpc.Code_CODE_OK: + if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND { + errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message) + return + } + if res.Status.Code == cs3rpc.Code_CODE_PERMISSION_DENIED { + // TODO check if we should return 404 to not disclose existing items + errorcode.AccessDenied.Render(w, r, http.StatusForbidden, res.Status.Message) + return + } + g.logger.Error().Err(err).Msg("error sending list container grpc request") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message) + return + } + + files, err := formatDriveItems(lRes.Infos) + if err != nil { + g.logger.Error().Err(err).Msg("error encoding response as json") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + return + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, &listResponse{Value: files}) +} + +func (g Graph) getDriveItem(ctx context.Context, root *storageprovider.ResourceId, relativePath string) (*libregraph.DriveItem, error) { + + client, err := g.GetClient() + if err != nil { + g.logger.Error().Err(err).Msg("error creating grpc client") + return nil, err + } + + ref := &storageprovider.Reference{ + ResourceId: root, + // the path is always relative to the root of the drive, not the location of the .config/ocis/space.yaml file + Path: filepath.Join("./", relativePath), + } + res, err := client.Stat(ctx, &storageprovider.StatRequest{Ref: ref}) + if res.Status.Code != cs3rpc.Code_CODE_OK { + return nil, err + } + + return cs3ResourceToDriveItem(res.Info) +} + +func formatDriveItems(mds []*storageprovider.ResourceInfo) ([]*libregraph.DriveItem, error) { + responses := make([]*libregraph.DriveItem, 0, len(mds)) + for i := range mds { + res, err := cs3ResourceToDriveItem(mds[i]) + if err != nil { + return nil, err + } + responses = append(responses, res) + } + + return responses, nil +} + +func cs3TimestampToTime(t *types.Timestamp) time.Time { + return time.Unix(int64(t.Seconds), int64(t.Nanos)) +} + +func cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) { + size := new(int64) + *size = int64(res.Size) // uint64 -> int :boom: + name := path.Base(res.Path) + + driveItem := &libregraph.DriveItem{ + Id: &res.Id.OpaqueId, + Name: &name, + ETag: &res.Etag, + Size: size, + } + if res.Mtime != nil { + lastModified := cs3TimestampToTime(res.Mtime) + driveItem.LastModifiedDateTime = &lastModified + } + if res.Type == storageprovider.ResourceType_RESOURCE_TYPE_FILE { + driveItem.File = &libregraph.OpenGraphFile{ // FIXME We cannot use libregraph.File here because the openapi codegenerator autodetects 'File' as a go type ... + MimeType: &res.MimeType, + } + } + if res.Type == storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER { + driveItem.Folder = &libregraph.Folder{} + } + return driveItem, nil +} diff --git a/graph/pkg/service/v0/drives.go b/graph/pkg/service/v0/drives.go index 12059dd0b7..2758476979 100644 --- a/graph/pkg/service/v0/drives.go +++ b/graph/pkg/service/v0/drives.go @@ -2,15 +2,15 @@ package svc import ( "context" + "crypto/tls" "encoding/json" "fmt" + "io/ioutil" "math" "net/http" "net/url" - "path" "strconv" "strings" - "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" @@ -18,6 +18,7 @@ import ( storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/rhttp" "github.com/go-chi/chi/v5" "github.com/go-chi/render" libregraph "github.com/owncloud/libre-graph-api-go" @@ -25,10 +26,17 @@ import ( "github.com/owncloud/ocis/ocis-pkg/service/grpc" sproto "github.com/owncloud/ocis/settings/pkg/proto/v0" settingsSvc "github.com/owncloud/ocis/settings/pkg/service/v0" + "gopkg.in/yaml.v2" merrors "go-micro.dev/v4/errors" ) +const ( + // "github.com/cs3org/reva/internal/http/services/datagateway" is internal so we redeclare it here + // TokenTransportHeader holds the header key for the reva transfer token + TokenTransportHeader = "X-Reva-Transfer" +) + // GetDrives implements the Service interface. func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) { g.logger.Info().Msg("Calling GetDrives") @@ -100,70 +108,6 @@ func (g Graph) GetDrives(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, &listResponse{Value: files}) } -// GetRootDriveChildren implements the Service interface. -func (g Graph) GetRootDriveChildren(w http.ResponseWriter, r *http.Request) { - g.logger.Info().Msg("Calling GetRootDriveChildren") - ctx := r.Context() - - client, err := g.GetClient() - if err != nil { - g.logger.Error().Err(err).Msg("could not get client") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) - return - } - - res, err := client.GetHome(ctx, &storageprovider.GetHomeRequest{}) - switch { - case err != nil: - g.logger.Error().Err(err).Msg("error sending get home grpc request") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) - return - case res.Status.Code != cs3rpc.Code_CODE_OK: - if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND { - errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message) - return - } - g.logger.Error().Err(err).Msg("error sending get home grpc request") - errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message) - return - } - - lRes, err := client.ListContainer(ctx, &storageprovider.ListContainerRequest{ - Ref: &storageprovider.Reference{ - Path: res.Path, - }, - }) - switch { - case err != nil: - g.logger.Error().Err(err).Msg("error sending list container grpc request") - errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error()) - return - case res.Status.Code != cs3rpc.Code_CODE_OK: - if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND { - errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, res.Status.Message) - return - } - if res.Status.Code == cs3rpc.Code_CODE_PERMISSION_DENIED { - // TODO check if we should return 404 to not disclose existing items - errorcode.AccessDenied.Render(w, r, http.StatusForbidden, res.Status.Message) - return - } - g.logger.Error().Err(err).Msg("error sending list container grpc request") - errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message) - return - } - - files, err := formatDriveItems(lRes.Infos) - if err != nil { - g.logger.Error().Err(err).Msg("error encoding response as json") - errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) - return - } - - render.Status(r, http.StatusOK) - render.JSON(w, r, &listResponse{Value: files}) -} - // CreateDrive creates a storage drive (space). func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) { us, ok := ctxpkg.ContextGetUser(r.Context()) @@ -350,40 +294,38 @@ func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, updatedDrive) } -func cs3TimestampToTime(t *types.Timestamp) time.Time { - return time.Unix(int64(t.Seconds), int64(t.Nanos)) -} - -func cs3ResourceToDriveItem(res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) { - size := new(int64) - *size = int64(res.Size) // uint64 -> int :boom: - name := path.Base(res.Path) - - driveItem := &libregraph.DriveItem{ - Id: &res.Id.OpaqueId, - Name: &name, - ETag: &res.Etag, - Size: size, - } - if res.Mtime != nil { - lastModified := cs3TimestampToTime(res.Mtime) - driveItem.LastModifiedDateTime = &lastModified - } - if res.Type == storageprovider.ResourceType_RESOURCE_TYPE_FILE { - driveItem.File = &libregraph.OpenGraphFile{ // FIXME We cannot use libregraph.File here because the openapi codegenerator autodetects 'File' as a go type ... - MimeType: &res.MimeType, +func (g Graph) formatDrives(ctx context.Context, baseURL *url.URL, mds []*storageprovider.StorageSpace) ([]*libregraph.Drive, error) { + responses := make([]*libregraph.Drive, 0, len(mds)) + for _, space := range mds { + res, err := cs3StorageSpaceToDrive(baseURL, space) + if err != nil { + return nil, err } - } - if res.Type == storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER { - driveItem.Folder = &libregraph.Folder{} - } - return driveItem, nil -} - -func formatDriveItems(mds []*storageprovider.ResourceInfo) ([]*libregraph.DriveItem, error) { - responses := make([]*libregraph.DriveItem, 0, len(mds)) - for i := range mds { - res, err := cs3ResourceToDriveItem(mds[i]) + spaceYaml, err := g.getSpaceYaml(ctx, space) + if err == nil { + if spaceYaml.Description != "" { + res.Description = &spaceYaml.Description + } + if len(spaceYaml.Special) > 0 { + s := make([]libregraph.DriveItem, 0, len(spaceYaml.Special)) + for name, relativePath := range spaceYaml.Special { + sdi, err := g.getDriveItem(ctx, space.Root, relativePath) + if err != nil { + // TODO log + continue + } + n := name // copy the name to a dedicated variable + sdi.SpecialFolder = &libregraph.SpecialFolder{ + Name: &n, + } + // TODO cache until ./.config/ocis/space.yaml file changes + s = append(s, *sdi) + } + res.Special = &s + } + } + // TODO this overwrites the quota that might already have been mapped in cs3StorageSpaceToDrive above ... move this into the cs3StorageSpaceToDrive method? + res.Quota, err = g.getDriveQuota(ctx, space) if err != nil { return nil, err } @@ -444,27 +386,12 @@ func cs3StorageSpaceToDrive(baseURL *url.URL, space *storageprovider.StorageSpac } } // FIXME use coowner from https://github.com/owncloud/open-graph-api + // TODO read .space.yaml and parse it for description and thumbnail? + // return drive, nil } -func (g Graph) formatDrives(ctx context.Context, baseURL *url.URL, mds []*storageprovider.StorageSpace) ([]*libregraph.Drive, error) { - responses := make([]*libregraph.Drive, 0, len(mds)) - for i := range mds { - res, err := cs3StorageSpaceToDrive(baseURL, mds[i]) - if err != nil { - return nil, err - } - res.Quota, err = g.getDriveQuota(ctx, mds[i]) - if err != nil { - return nil, err - } - responses = append(responses, res) - } - - return responses, nil -} - func (g Graph) getDriveQuota(ctx context.Context, space *storageprovider.StorageSpace) (*libregraph.Quota, error) { client, err := g.GetClient() if err != nil { @@ -484,13 +411,13 @@ func (g Graph) getDriveQuota(ctx context.Context, space *storageprovider.Storage res, err := client.GetQuota(ctx, req) switch { case err != nil: - g.logger.Error().Err(err).Msg("error sending get quota grpc request") + g.logger.Error().Err(err).Msg("could not call GetQuota") return nil, nil case res.Status.Code == cs3rpc.Code_CODE_UNIMPLEMENTED: // TODO well duh return nil, nil case res.Status.Code != cs3rpc.Code_CODE_OK: - g.logger.Error().Err(err).Msg("error sending sending get quota grpc request") + g.logger.Error().Err(err).Msg("error sending get quota grpc request") return nil, err } @@ -509,6 +436,110 @@ func (g Graph) getDriveQuota(ctx context.Context, space *storageprovider.Storage return &qta, nil } +type SpaceYaml struct { + Version string `yaml:"version"` + Description string `yaml:"description"` + // map of {name} -> {relative path to resource}, eg: + // readme -> readme.md + // image -> .config/ocis/space.png + Special map[string]string `yaml:"special"` +} + +// generates a space root stat cache key used to detect changes in a space +func spaceRootStatKey(id *storageprovider.ResourceId) string { + if id == nil || id.StorageId == "" || id.OpaqueId == "" { + return "" + } + return "sid:" + id.StorageId + "!oid:" + id.OpaqueId +} + +type spaceYamlEntry struct { + spaceYaml SpaceYaml + spaceYamlMtime *types.Timestamp + rootMtime *types.Timestamp +} + +func (g Graph) getSpaceYaml(ctx context.Context, space *storageprovider.StorageSpace) (*SpaceYaml, error) { + + /*} + if syc, err := g.spaceYamlCache.Get(spaceRootStatKey(space.Root)); err == nil { + if spaceYamlEntry, ok := spaceYamlEntry(syc) { + if spaceYamlEntry.rootMtime != nil && space.Mtime != nil { + + + } + } + } + */ + // TODO try reading from cache, invalidate when space mtime changes + client, err := g.GetClient() + if err != nil { + g.logger.Error().Err(err).Msg("error creating grpc client") + return nil, err + } + dlReq := &storageprovider.InitiateFileDownloadRequest{ + Ref: &storageprovider.Reference{ + ResourceId: &storageprovider.ResourceId{ + StorageId: space.Root.StorageId, + OpaqueId: space.Root.OpaqueId, + }, + Path: "./.config/ocis/space.yaml", + }, + } + // ctx := metadata.AppendToOutgoingContext(ctx, "If-Modified-Since", TODO send cached mtime of space yaml ) + // TODO initiate file download only if the etag does not match + rsp, err := client.InitiateFileDownload(ctx, dlReq) + if err != nil { + return nil, err + } + if rsp.Status.Code != cs3rpc.Code_CODE_OK { + return nil, fmt.Errorf("could not initiate download of %s: %s", dlReq.Ref.Path, rsp.Status.Message) + } + var ep, tk string + for _, p := range rsp.Protocols { + if p.Protocol == "spaces" { + ep, tk = p.DownloadEndpoint, p.Token + } + } + if ep == "" { + return nil, fmt.Errorf("space does not support the spaces download protocol") + } + + httpReq, err := rhttp.NewRequest(ctx, "GET", ep, nil) + if err != nil { + return nil, err + } + // httpReq.Header.Set(ctxpkg.TokenHeader, auth) + httpReq.Header.Set(TokenTransportHeader, tk) + + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec + } + httpClient := &http.Client{} + + resp, err := httpClient.Do(httpReq) // nolint:bodyclose + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("could not get the .space.yaml. Request returned with statuscode %d ", resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + spaceYaml := &SpaceYaml{} + //if err := yaml.NewDecoder(resp.Body).Decode(spaceYaml); err != nil { + if err := yaml.Unmarshal(body, spaceYaml); err != nil { + return nil, err + } + + return spaceYaml, nil +} + func calculateQuotaState(total int64, used int64) (state string) { percent := (float64(used) / float64(total)) * 100 diff --git a/graph/pkg/service/v0/graph.go b/graph/pkg/service/v0/graph.go index 251bd0edaa..2b7cb0141e 100644 --- a/graph/pkg/service/v0/graph.go +++ b/graph/pkg/service/v0/graph.go @@ -3,6 +3,7 @@ package svc import ( "net/http" + "github.com/ReneKroon/ttlcache/v2" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/go-chi/chi/v5" @@ -17,6 +18,7 @@ type Graph struct { mux *chi.Mux logger *log.Logger identityBackend identity.Backend + spaceYamlCache *ttlcache.Cache } // ServeHTTP implements the Service interface. diff --git a/graph/pkg/service/v0/service.go b/graph/pkg/service/v0/service.go index 36d0cf451b..c9b98d41cb 100644 --- a/graph/pkg/service/v0/service.go +++ b/graph/pkg/service/v0/service.go @@ -3,6 +3,7 @@ package svc import ( "net/http" + "github.com/ReneKroon/ttlcache/v2" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -55,6 +56,7 @@ func NewService(opts ...Option) Service { mux: m, logger: &options.Logger, identityBackend: backend, + spaceYamlCache: ttlcache.NewCache(), } m.Route(options.Config.HTTP.Root, func(r chi.Router) {