package svc import ( "context" "encoding/json" "fmt" "math" "net/http" "net/url" "path" "slices" "sort" "strconv" "strings" "github.com/CiscoM31/godata" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" 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/opencloud-eu/libre-graph-api-go" revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" "github.com/opencloud-eu/reva/v2/pkg/storagespace" "github.com/opencloud-eu/reva/v2/pkg/utils" "github.com/pkg/errors" merrors "go-micro.dev/v4/errors" "golang.org/x/sync/errgroup" "golang.org/x/text/collate" "golang.org/x/text/language" "google.golang.org/protobuf/proto" "github.com/qsfera/server/pkg/l10n" v0 "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0" settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0" "github.com/qsfera/server/services/graph/pkg/errorcode" "github.com/qsfera/server/services/graph/pkg/odata" settingsServiceExt "github.com/qsfera/server/services/settings/pkg/store/defaults" ) const ( _spaceTypePersonal = "personal" _spaceTypeProject = "project" _spaceTypeVirtual = "virtual" _spaceTypeMountpoint = "mountpoint" _spaceStateTrashed = "trashed" _sortDescending = "desc" ) var ( _invalidSpaceNameCharacters = []string{`/`, `\`, `.`, `:`, `?`, `*`, `"`, `>`, `<`, `|`} _maxSpaceNameLength = 255 // ErrNameTooLong is thrown when the spacename is too long ErrNameTooLong = fmt.Errorf("spacename must be smaller than %d", _maxSpaceNameLength) // ErrNameEmpty is thrown when the spacename is empty ErrNameEmpty = errors.New("spacename must not be empty") // ErrForbiddenCharacter is thrown when the spacename contains an invalid character ErrForbiddenCharacter = fmt.Errorf("spacenames must not contain %v", _invalidSpaceNameCharacters) ) // GetDrives serves as a factory method that returns the appropriate // http.Handler function based on the specified API version. func (g Graph) GetDrives(version APIVersion) http.HandlerFunc { switch version { case APIVersion_1: return g.GetDrivesV1 case APIVersion_1_Beta_1: return g.GetDrivesV1Beta1 default: return func(w http.ResponseWriter, r *http.Request) { errorcode.New(errorcode.NotSupported, "api version not supported").Render(w, r) } } } // GetDrivesV1 attempts to retrieve the current users drives; // it lists all drives the current user has access to. func (g Graph) GetDrivesV1(w http.ResponseWriter, r *http.Request) { spaces, errCode := g.getDrives(r, false, APIVersion_1) if errCode != nil { errorcode.RenderError(w, r, errCode) return } render.Status(r, http.StatusOK) switch { case spaces == nil && errCode == nil: render.JSON(w, r, nil) default: render.JSON(w, r, &ListResponse{Value: spaces}) } } // GetDrivesV1Beta1 is the same as the GetDrivesV1 endpoint, expect: // it includes the grantedtoV2 property // it uses unified roles instead of the cs3 representations func (g Graph) GetDrivesV1Beta1(w http.ResponseWriter, r *http.Request) { spaces, errCode := g.getDrives(r, false, APIVersion_1_Beta_1) if errCode != nil { errorcode.RenderError(w, r, errCode) return } render.Status(r, http.StatusOK) switch { case spaces == nil && errCode == nil: render.JSON(w, r, nil) default: render.JSON(w, r, &ListResponse{Value: spaces}) } } // GetAllDrives serves as a factory method that returns the appropriate // http.Handler function based on the specified API version. func (g Graph) GetAllDrives(version APIVersion) http.HandlerFunc { switch version { case APIVersion_1: return g.GetAllDrivesV1 case APIVersion_1_Beta_1: return g.GetAllDrivesV1Beta1 default: return func(w http.ResponseWriter, r *http.Request) { errorcode.New(errorcode.NotSupported, "api version not supported").Render(w, r) } } } // GetAllDrivesV1 attempts to retrieve the current users drives; // it includes another user's drives, if the current user has the permission. func (g Graph) GetAllDrivesV1(w http.ResponseWriter, r *http.Request) { spaces, errCode := g.getDrives(r, true, APIVersion_1) if errCode != nil { errorcode.RenderError(w, r, errCode) return } render.Status(r, http.StatusOK) switch { case spaces == nil && errCode == nil: render.JSON(w, r, nil) default: render.JSON(w, r, &ListResponse{Value: spaces}) } } // GetAllDrivesV1Beta1 is the same as the GetAllDrivesV1 endpoint, expect: // it includes the grantedtoV2 property // it uses unified roles instead of the cs3 representations func (g Graph) GetAllDrivesV1Beta1(w http.ResponseWriter, r *http.Request) { drives, errCode := g.getDrives(r, true, APIVersion_1_Beta_1) if errCode != nil { errorcode.RenderError(w, r, errCode) return } render.Status(r, http.StatusOK) switch { case drives == nil && errCode == nil: render.JSON(w, r, nil) default: render.JSON(w, r, &ListResponse{Value: drives}) } } func sanitizePath(path string, apiVersion APIVersion) string { switch apiVersion { case APIVersion_1: return strings.TrimPrefix(path, "/graph/v1.0/") case APIVersion_1_Beta_1: return strings.TrimPrefix(path, "/graph/v1beta1/") default: return path } } // parseDriveRequest parses the odata request and returns the parsed request and a boolean indicating if the request should expand root driveItems. func parseDriveRequest(r *http.Request) (*godata.GoDataRequest, bool, error) { odataReq, err := godata.ParseRequest(r.Context(), sanitizePath(r.URL.Path, APIVersion_1), r.URL.Query()) if err != nil { return nil, false, errorcode.New(errorcode.InvalidRequest, err.Error()) } exp, err := odata.GetExpandValues(odataReq.Query) if err != nil { return nil, false, errorcode.New(errorcode.InvalidRequest, err.Error()) } expandPermissions := slices.Contains(exp, "root.permissions") return odataReq, expandPermissions, nil } // getDrives implements the Service interface. func (g Graph) getDrives(r *http.Request, unrestricted bool, apiVersion APIVersion) ([]*libregraph.Drive, error) { ctx := r.Context() log := g.logger.SubloggerWithRequestID(ctx).With().Interface("query", r.URL.Query()).Bool("unrestricted", unrestricted).Logger() log.Debug().Msg("calling get drives") webDavBaseURL, err := g.getWebDavBaseURL() if err != nil { log.Error().Err(err).Msg("could not get drives: error parsing url") return nil, errorcode.New(errorcode.GeneralException, err.Error()) } log = log.With().Str("url", webDavBaseURL.String()).Logger() odataReq, expandPermissions, err := parseDriveRequest(r) if err != nil { log.Debug().Err(err).Msg("could not get drives: error parsing odata request") return nil, err } filters, err := generateCs3Filters(odataReq) if err != nil { log.Debug().Err(err).Msg("could not get drives: error parsing filters") return nil, errorcode.New(errorcode.NotSupported, err.Error()) } if !unrestricted { user, ok := revactx.ContextGetUser(r.Context()) if !ok { log.Debug().Msg("could not create drive: invalid user") return nil, errorcode.New(errorcode.AccessDenied, "invalid user") } filters = append(filters, &storageprovider.ListStorageSpacesRequest_Filter{ Type: storageprovider.ListStorageSpacesRequest_Filter_TYPE_USER, Term: &storageprovider.ListStorageSpacesRequest_Filter_User{ User: user.GetId(), }, }) } log.Debug(). Interface("filters", filters). Msg("calling list storage spaces on backend") res, err := g.ListStorageSpacesWithFilters(ctx, filters, unrestricted) switch { case err != nil: log.Error().Err(err).Msg("could not get drives: transport error") return nil, errorcode.New(errorcode.GeneralException, err.Error()) case res.Status.Code != cs3rpc.Code_CODE_OK: if res.Status.Code == cs3rpc.Code_CODE_NOT_FOUND { // ok, empty return return nil, nil } log.Debug().Str("message", res.GetStatus().GetMessage()).Msg("could not get drives: grpc error") return nil, errorcode.New(errorcode.GeneralException, res.Status.Message) } spaces, err := g.formatDrives(ctx, webDavBaseURL, res.StorageSpaces, apiVersion, expandPermissions, getFieldMask(odataReq)) if err != nil { log.Debug().Err(err).Msg("could not get drives: error parsing grpc response") return nil, errorcode.New(errorcode.GeneralException, err.Error()) } spaces, err = sortSpaces(odataReq, spaces) if err != nil { log.Debug().Err(err).Msg("could not get drives: error sorting the spaces list according to query") return nil, errorcode.New(errorcode.InvalidRequest, err.Error()) } return spaces, nil } // GetSingleDrive does a lookup of a single space by spaceId func (g Graph) GetSingleDrive(w http.ResponseWriter, r *http.Request) { ctx := r.Context() log := g.logger.SubloggerWithRequestID(ctx).With().Interface("query", r.URL.Query()).Logger() log.Debug().Msg("calling get drive") rid, err := parseIDParam(r, "driveID") if err != nil { errorcode.RenderError(w, r, err) return } log = log.With().Str("storage", rid.StorageId).Str("space", rid.SpaceId).Str("node", rid.OpaqueId).Logger() webDavBaseURL, err := g.getWebDavBaseURL() if err != nil { log.Error().Err(err).Msg("could not get drive: error parsing webdav base url") errorcode.RenderError(w, r, err) return } log = log.With().Str("url", webDavBaseURL.String()).Logger() odataReq, expandPermissions, err := parseDriveRequest(r) if err != nil { log.Debug().Err(err).Msg("could not get drives: error parsing odata request") errorcode.RenderError(w, r, err) return } log.Debug().Msg("calling list storage spaces with id filter") filters := []*storageprovider.ListStorageSpacesRequest_Filter{ listStorageSpacesIDFilter(storagespace.FormatResourceID(&rid)), } res, err := g.ListStorageSpacesWithFilters(ctx, filters, true) switch { case err != nil: log.Error().Err(err).Msg("could not get drive: transport error") errorcode.GeneralException.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 { // the client is doing a lookup for a specific space, therefore we need to return // not found to the caller log.Debug().Msg("could not get drive: not found") errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "drive not found") return } log.Debug(). Str("grpcmessage", res.GetStatus().GetMessage()). Msg("could not get drive: grpc error") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, res.Status.Message) return } spaces, err := g.formatDrives(ctx, webDavBaseURL, res.StorageSpaces, APIVersion_1, expandPermissions, getFieldMask(odataReq)) if err != nil { log.Debug().Err(err).Msg("could not get drive: error parsing grpc response") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } switch num := len(spaces); { case num == 0: log.Debug().Msg("could not get drive: no drive returned from storage") errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "no drive returned from storage") return case num == 1: render.Status(r, http.StatusOK) render.JSON(w, r, spaces[0]) default: log.Debug().Int("number", num).Msg("could not get drive: expected to find a single drive but fetched more") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not get drive: expected to find a single drive but fetched more") return } } func (g Graph) canCreateSpace(ctx context.Context, ownPersonalHome bool) bool { pr, err := g.permissionsService.GetPermissionByID(ctx, &settingssvc.GetPermissionByIDRequest{ PermissionId: settingsServiceExt.CreateSpacesPermission(0).Id, }) if err != nil || pr.Permission == nil { return false } // TODO @C0rby shouldn't the permissions service check this? aka shouldn't we call CheckPermission? if pr.Permission.Constraint == v0.Permission_CONSTRAINT_OWN && !ownPersonalHome { return false } return true } // CreateDrive creates a storage drive (space). func (g Graph) CreateDrive(w http.ResponseWriter, r *http.Request) { ctx := r.Context() log := g.logger.SubloggerWithRequestID(ctx).With().Interface("query", r.URL.Query()).Logger() log.Debug().Msg("calling create drive") webDavBaseURL, err := g.getWebDavBaseURL() if err != nil { log.Error().Err(err).Msg("could not create drive: error parsing webdav base url") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } log = log.With().Str("url", webDavBaseURL.String()).Logger() us, ok := revactx.ContextGetUser(ctx) if !ok { log.Debug().Msg("could not create drive: invalid user") errorcode.NotAllowed.Render(w, r, http.StatusUnauthorized, "invalid user") return } // TODO determine if the user tries to create his own personal space and pass that as a boolean canCreateSpace := g.canCreateSpace(ctx, false) if !canCreateSpace { log.Debug().Bool("cancreatespace", canCreateSpace).Msg("could not create drive: insufficient permissions") // if the permission is not existing for the user in context we can assume we don't have it. Return 401. errorcode.NotAllowed.Render(w, r, http.StatusForbidden, "insufficient permissions to create a space.") return } gatewayClient, err := g.gatewaySelector.Next() if err != nil { log.Error().Err(err).Msg("could not select next gateway client") errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "could not select next gateway client, aborting") return } drive := libregraph.Drive{} if err := StrictJSONUnmarshal(r.Body, &drive); err != nil { log.Debug().Err(err).Interface("body", r.Body).Msg("could not create drive: invalid body schema definition") errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid body schema definition") return } spaceName := strings.TrimSpace(drive.Name) if err := validateSpaceName(spaceName); err != nil { log.Debug().Str("name", spaceName).Err(err).Msg("could not create drive: name validation failed") errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid spacename: %s", err.Error())) return } var driveType string if drive.DriveType != nil { driveType = *drive.DriveType } switch driveType { case "", _spaceTypeProject: driveType = _spaceTypeProject default: log.Debug().Str("type", driveType).Msg("could not create drive: drives of this type cannot be created via this api") errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "drives of this type cannot be created via this api") return } csr := storageprovider.CreateStorageSpaceRequest{ Type: driveType, Name: spaceName, Quota: getQuota(drive.Quota, g.config.Spaces.DefaultQuota), } if drive.Description != nil { csr.Opaque = utils.AppendPlainToOpaque(csr.Opaque, "description", *drive.Description) } if drive.DriveAlias != nil { csr.Opaque = utils.AppendPlainToOpaque(csr.Opaque, "spaceAlias", *drive.DriveAlias) } if driveType == _spaceTypePersonal { csr.Owner = us } resp, err := gatewayClient.CreateStorageSpace(ctx, &csr) if err != nil { log.Error().Err(err).Msg("could not create drive: transport error") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } if resp.GetStatus().GetCode() != cs3rpc.Code_CODE_OK { if resp.GetStatus().GetCode() == cs3rpc.Code_CODE_PERMISSION_DENIED { log.Debug().Str("grpcmessage", resp.GetStatus().GetMessage()).Msg("could not create drive: permission denied") errorcode.NotAllowed.Render(w, r, http.StatusForbidden, "permission denied") return } if resp.GetStatus().GetCode() == cs3rpc.Code_CODE_INVALID_ARGUMENT { log.Debug().Str("grpcmessage", resp.GetStatus().GetMessage()).Msg("could not create drive: bad request") errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, resp.GetStatus().GetMessage()) return } log.Debug().Interface("grpcmessage", csr).Str("grpc", resp.GetStatus().GetMessage()).Msg("could not create drive: grpc error") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, resp.GetStatus().GetMessage()) return } space := resp.GetStorageSpace() if t := r.URL.Query().Get(TemplateParameter); t != "" && driveType == _spaceTypeProject { loc := l10n.MustGetUserLocale(ctx, us.GetId().GetOpaqueId(), r.Header.Get(HeaderAcceptLanguage), g.valueService) if err := g.applySpaceTemplate(ctx, gatewayClient, space.GetRoot(), t, loc); err != nil { log.Error().Err(err).Msg("could not apply template to space") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } // refetch the drive to get quota information - should we calculate this ourselves to avoid the extra call? space, err = utils.GetSpace(ctx, space.GetId().GetOpaqueId(), gatewayClient) if err != nil { log.Error().Err(err).Msg("could not refetch space") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } } spaces, err := g.formatDrives(ctx, webDavBaseURL, []*storageprovider.StorageSpace{space}, APIVersion_1, false, nil) if err != nil { log.Debug().Err(err).Msg("could not get drive: error parsing grpc response") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } if len(spaces) == 0 { log.Error().Msg("could not convert space") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not convert space") return } render.Status(r, http.StatusCreated) render.JSON(w, r, spaces[0]) } // UpdateDrive updates the properties of a storage drive (space). func (g Graph) UpdateDrive(w http.ResponseWriter, r *http.Request) { log := g.logger.SubloggerWithRequestID(r.Context()).With().Interface("query", r.URL.Query()).Logger() log.Debug().Msg("calling update drive") rid, err := parseIDParam(r, "driveID") if err != nil { errorcode.RenderError(w, r, err) return } log = log.With().Str("storage", rid.StorageId).Str("space", rid.SpaceId).Str("node", rid.OpaqueId).Logger() webDavBaseURL, err := g.getWebDavBaseURL() if err != nil { log.Error().Err(err).Interface("url", webDavBaseURL.String()).Msg("could not update drive: error parsing url") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } log = log.With().Str("url", webDavBaseURL.String()).Logger() drive := libregraph.DriveUpdate{} if err = StrictJSONUnmarshal(r.Body, &drive); err != nil { log.Debug().Err(err).Interface("body", r.Body).Msg("could not update drive, invalid request body") errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: error: %v", err.Error())) return } gatewayClient, err := g.gatewaySelector.Next() if err != nil { g.logger.Error().Err(err).Msg("could not select next gateway client") errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "could not select next gateway client, aborting") return } root := &rid updateSpaceRequest := &storageprovider.UpdateStorageSpaceRequest{ // Prepare the object to apply the diff from. The properties on StorageSpace will overwrite // the original storage space. StorageSpace: &storageprovider.StorageSpace{ Id: &storageprovider.StorageSpaceId{ OpaqueId: storagespace.FormatResourceID(&rid), }, Root: root, }, } // Note: this is the Opaque prop of the request if restore, _ := strconv.ParseBool(r.Header.Get("restore")); restore { updateSpaceRequest.Opaque = &types.Opaque{ Map: map[string]*types.OpaqueEntry{ "restore": { Decoder: "plain", Value: []byte("true"), }, }, } } if drive.Description != nil { updateSpaceRequest.StorageSpace.Opaque = utils.AppendPlainToOpaque(updateSpaceRequest.StorageSpace.Opaque, "description", *drive.Description) } if drive.DriveAlias != nil { updateSpaceRequest.StorageSpace.Opaque = utils.AppendPlainToOpaque(updateSpaceRequest.StorageSpace.Opaque, "spaceAlias", *drive.DriveAlias) } for _, special := range drive.Special { if special.Id != nil { updateSpaceRequest.StorageSpace.Opaque = utils.AppendPlainToOpaque(updateSpaceRequest.StorageSpace.Opaque, *special.SpecialFolder.Name, *special.Id) } } if drive.GetName() != "" { spacename := strings.TrimSpace(drive.GetName()) if err := validateSpaceName(spacename); err != nil { log.Info().Err(err).Msg("could not update drive: spacename invalid") errorcode.GeneralException.Render(w, r, http.StatusBadRequest, err.Error()) return } updateSpaceRequest.StorageSpace.Name = spacename } if drive.Quota.HasTotal() { user := revactx.ContextMustGetUser(r.Context()) // NOTE: a space admin cannot get a space by ID. We need to fetch all spaces and search for it dt := _spaceTypePersonal filters := []*storageprovider.ListStorageSpacesRequest_Filter{listStorageSpacesTypeFilter(_spaceTypeProject)} res, err := g.ListStorageSpacesWithFilters(r.Context(), filters, true) if err == nil && res.GetStatus().GetCode() == cs3rpc.Code_CODE_OK { for _, sp := range res.StorageSpaces { id, _ := storagespace.ParseID(sp.GetId().GetOpaqueId()) if id.GetSpaceId() == rid.GetSpaceId() { dt = _spaceTypeProject } } } canSetSpaceQuota, err := g.canSetSpaceQuota(r.Context(), user, dt) if err != nil { log.Error().Err(err).Msg("could not update drive: failed to check if the user can set space quota") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } if !canSetSpaceQuota { log.Debug(). Bool("cansetspacequota", canSetSpaceQuota). Msg("could not update drive: user is not allowed to set the space quota") errorcode.NotAllowed.Render(w, r, http.StatusForbidden, "user is not allowed to set the space quota") return } updateSpaceRequest.StorageSpace.Quota = &storageprovider.Quota{ QuotaMaxBytes: uint64(*drive.Quota.Total), } } log.Debug().Interface("payload", updateSpaceRequest).Msg("calling update space on backend") resp, err := gatewayClient.UpdateStorageSpace(r.Context(), updateSpaceRequest) if err != nil { log.Error().Err(err).Msg("could not update drive: transport error") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "transport error") return } if resp.GetStatus().GetCode() != cs3rpc.Code_CODE_OK { switch resp.Status.GetCode() { case cs3rpc.Code_CODE_NOT_FOUND: log.Debug().Msg("could not update drive: drive not found") errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "drive not found") return case cs3rpc.Code_CODE_PERMISSION_DENIED: log.Debug().Msg("could not update drive, permission denied") errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "drive not found") return case cs3rpc.Code_CODE_INVALID_ARGUMENT: log.Debug().Msg("could not update drive, invalid argument") errorcode.NotAllowed.Render(w, r, http.StatusBadRequest, resp.GetStatus().GetMessage()) return case cs3rpc.Code_CODE_UNIMPLEMENTED: log.Debug().Msg("could not delete drive: delete not implemented for this type of drive") errorcode.NotAllowed.Render(w, r, http.StatusMethodNotAllowed, "drive cannot be updated") return default: log.Debug().Str("grpc", resp.GetStatus().GetMessage()).Msg("could not update drive: grpc error") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "grpc error") return } } spaces, err := g.formatDrives(r.Context(), webDavBaseURL, []*storageprovider.StorageSpace{resp.StorageSpace}, APIVersion_1, false, nil) if err != nil { log.Debug().Err(err).Msg("could not update drive: error parsing grpc response") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) return } render.Status(r, http.StatusOK) render.JSON(w, r, spaces[0]) } func (g Graph) formatDrives(ctx context.Context, baseURL *url.URL, storageSpaces []*storageprovider.StorageSpace, apiVersion APIVersion, expandPermissions bool, fieldMask map[string]struct{}) ([]*libregraph.Drive, error) { errg, ctx := errgroup.WithContext(ctx) work := make(chan *storageprovider.StorageSpace, len(storageSpaces)) results := make(chan *libregraph.Drive, len(storageSpaces)) // Distribute work errg.Go(func() error { defer close(work) for _, space := range storageSpaces { select { case work <- space: case <-ctx.Done(): return ctx.Err() } } return nil }) // Spawn workers that'll concurrently work the queue numWorkers := 20 if len(storageSpaces) < numWorkers { numWorkers = len(storageSpaces) } for i := 0; i < numWorkers; i++ { errg.Go(func() error { for storageSpace := range work { if storageSpace.GetRoot().GetStorageId() == utils.OCMStorageProviderID { // skip OCM shares they are no supposed to show up in the drives list continue } res, err := g.cs3StorageSpaceToDrive(ctx, baseURL, storageSpace, apiVersion, expandPermissions, fieldMask) if err != nil { return err } // can't access disabled space if utils.ReadPlainFromOpaque(storageSpace.Opaque, _spaceStateTrashed) != _spaceStateTrashed { res.Special = g.getSpecialDriveItems(ctx, baseURL, storageSpace) if storageSpace.SpaceType != _spaceTypeMountpoint && storageSpace.SpaceType != _spaceTypeVirtual { quota, err := g.getDriveQuota(ctx, storageSpace) res.Quota = "a if err != nil { return err } } else { res.Quota = nil } } select { case results <- res: case <-ctx.Done(): return ctx.Err() } } return nil }) } // Wait for things to settle down, then close results chan go func() { _ = errg.Wait() // error is checked later close(results) }() responses := make([]*libregraph.Drive, 0, len(storageSpaces)) for r := range results { responses = append(responses, r) } if err := errg.Wait(); err != nil { return nil, err } return responses, nil } // ListStorageSpacesWithFilters List Storage Spaces using filters func (g Graph) ListStorageSpacesWithFilters(ctx context.Context, filters []*storageprovider.ListStorageSpacesRequest_Filter, unrestricted bool) (*storageprovider.ListStorageSpacesResponse, error) { _, err := g.permissionsService.GetPermissionByID(ctx, &settingssvc.GetPermissionByIDRequest{ PermissionId: settingsServiceExt.ListSpacesPermission(0).Id, }) permissions := make(map[string]struct{}, 1) // No error means the user has the permission if err == nil { permissions[settingsServiceExt.ListSpacesPermission(0).Name] = struct{}{} } value, err := json.Marshal(permissions) if err != nil { return nil, err } lReq := &storageprovider.ListStorageSpacesRequest{ Opaque: &types.Opaque{Map: map[string]*types.OpaqueEntry{ "permissions": { Decoder: "json", Value: value, }, "unrestricted": { Decoder: "plain", Value: []byte(strconv.FormatBool(unrestricted)), }, }}, Filters: filters, } gatewayClient, err := g.gatewaySelector.Next() if err != nil { return nil, err } res, err := gatewayClient.ListStorageSpaces(ctx, lReq) return res, err } func (g Graph) cs3StorageSpaceToDrive(ctx context.Context, baseURL *url.URL, space *storageprovider.StorageSpace, apiVersion APIVersion, expandPermissions bool, fieldMask map[string]struct{}) (*libregraph.Drive, error) { logger := g.logger.SubloggerWithRequestID(ctx) if space.Root == nil { logger.Error().Msg("unable to parse space: space has no root") return nil, errors.New("space has no root") } spaceRid := proto.Clone(space.Root).(*storageprovider.ResourceId) if space.Root.GetSpaceId() == space.Root.GetOpaqueId() { spaceRid.OpaqueId = "" } spaceID := storagespace.FormatResourceID(spaceRid) drive := &libregraph.Drive{ Id: libregraph.PtrString(spaceID), Name: space.Name, //"createdDateTime": "string (timestamp)", // TODO read from StorageSpace ... needs Opaque for now DriveType: &space.SpaceType, // we currently always expand the root because it carries the deleted property that indicates if a space is trashed Root: &libregraph.DriveItem{ Id: libregraph.PtrString(storagespace.FormatResourceID(spaceRid)), }, } if expandPermissions { drive.Root.Permissions, _ = g.cs3SpacePermissionsToLibreGraph(ctx, space, false, apiVersion) } if _, ok := fieldMask["@libre.graph.hasTrashedItems"]; ok { drive.LibreGraphHasTrashedItems = &space.HasTrashedItems } if space.SpaceType == _spaceTypeMountpoint { var remoteItem *libregraph.RemoteItem grantID := storageprovider.ResourceId{ StorageId: utils.ReadPlainFromOpaque(space.Opaque, "grantStorageID"), SpaceId: utils.ReadPlainFromOpaque(space.Opaque, "grantSpaceID"), OpaqueId: utils.ReadPlainFromOpaque(space.Opaque, "grantOpaqueID"), } if grantID.SpaceId != "" && grantID.OpaqueId != "" { var err error remoteItem, err = g.getRemoteItem(ctx, &grantID, baseURL) if err != nil { logger.Debug().Err(err).Interface("id", grantID).Msg("could not fetch remote item for space, continue") } } if remoteItem != nil { drive.Root.RemoteItem = remoteItem } } if space.Opaque != nil { if description, ok := space.Opaque.Map["description"]; ok { drive.Description = libregraph.PtrString(string(description.Value)) } if alias, ok := space.Opaque.Map["spaceAlias"]; ok { drive.DriveAlias = libregraph.PtrString(string(alias.Value)) } if v, ok := space.Opaque.Map[_spaceStateTrashed]; ok { deleted := &libregraph.Deleted{} deleted.SetState(string(v.Value)) drive.Root.Deleted = deleted } if entry, ok := space.Opaque.Map["etag"]; ok { drive.Root.ETag = libregraph.PtrString(string(entry.Value)) } } if baseURL != nil { webDavURL := *baseURL webDavURL.Path = path.Join(webDavURL.Path, spaceID) drive.Root.WebDavUrl = libregraph.PtrString(webDavURL.String()) } webURL, err := url.Parse(g.config.Spaces.WebDavBase) if err != nil { logger.Error(). Err(err). Str("url", g.config.Spaces.WebDavBase). Msg("failed to parse webURL base url") return nil, err } webURL.Path = path.Join(webURL.Path, "f", storagespace.FormatResourceID(spaceRid)) drive.WebUrl = libregraph.PtrString(webURL.String()) if space.Owner != nil && space.Owner.Id != nil { drive.Owner = &libregraph.IdentitySet{ User: &libregraph.Identity{ Id: &space.Owner.Id.OpaqueId, // DisplayName: , TODO read and cache from users provider }, } } if space.Mtime != nil { lastModified := cs3TimestampToTime(space.Mtime) drive.LastModifiedDateTime = &lastModified } drive.Quota = &libregraph.Quota{} if space.Quota != nil { var t int64 if space.Quota.QuotaMaxBytes > math.MaxInt64 { t = math.MaxInt64 } else { t = int64(space.Quota.QuotaMaxBytes) } drive.Quota.Total = &t } return drive, nil } func (g Graph) getDriveQuota(ctx context.Context, space *storageprovider.StorageSpace) (libregraph.Quota, error) { logger := g.logger.SubloggerWithRequestID(ctx) noQuotaInOpaque := true remainingUnknown := true var remaining, used, total int64 if space.Opaque != nil { m := space.Opaque.Map if e, ok := m["quota.remaining"]; ok { noQuotaInOpaque = false remainingUnknown = false remaining, _ = strconv.ParseInt(string(e.Value), 10, 64) } if e, ok := m["quota.used"]; ok { noQuotaInOpaque = false used, _ = strconv.ParseInt(string(e.Value), 10, 64) } if e, ok := m["quota.total"]; ok { noQuotaInOpaque = false total, _ = strconv.ParseInt(string(e.Value), 10, 64) } } if noQuotaInOpaque { // we have to make a trip to the storage // TODO only if quota property was requested gatewayClient, err := g.gatewaySelector.Next() if err != nil { return libregraph.Quota{}, err } req := &gateway.GetQuotaRequest{ Ref: &storageprovider.Reference{ ResourceId: space.Root, Path: ".", }, } res, err := gatewayClient.GetQuota(ctx, req) switch { case err != nil: logger.Error().Err(err).Interface("ref", req.Ref).Msg("could not call GetQuota: transport error") return libregraph.Quota{}, nil case res.GetStatus().GetCode() == cs3rpc.Code_CODE_UNIMPLEMENTED: logger.Debug().Msg("get quota is not implemented on the storage driver") return libregraph.Quota{}, nil case res.GetStatus().GetCode() != cs3rpc.Code_CODE_OK: logger.Debug().Str("grpc", res.GetStatus().GetMessage()).Msg("error sending get quota grpc request") return libregraph.Quota{}, errors.New(res.GetStatus().GetMessage()) } if res.Opaque != nil { m := res.Opaque.Map if e, ok := m["remaining"]; ok { remainingUnknown = false remaining, _ = strconv.ParseInt(string(e.Value), 10, 64) } } used = int64(res.UsedBytes) total = int64(res.TotalBytes) } qta := libregraph.Quota{ Used: &used, Total: &total, } if !remainingUnknown { qta.Remaining = &remaining } var t int64 switch { case total != 0: t = total case remainingUnknown: t = math.MaxInt64 default: // Quota was not set // Use remaining bytes to calculate state t = remaining } state := calculateQuotaState(t, used) qta.State = &state return qta, nil } func calculateQuotaState(total int64, used int64) (state string) { percent := (float64(used) / float64(total)) * 100 switch { case percent <= float64(75): return "normal" case percent <= float64(90): return "nearing" case percent <= float64(99): return "critical" default: return "exceeded" } } func getQuota(quota *libregraph.Quota, defaultQuota string) *storageprovider.Quota { switch { case quota != nil && quota.Total != nil: if q := *quota.Total; q >= 0 { return &storageprovider.Quota{QuotaMaxBytes: uint64(q)} } fallthrough case defaultQuota != "": if q, err := strconv.ParseInt(defaultQuota, 10, 64); err == nil && q >= 0 { return &storageprovider.Quota{QuotaMaxBytes: uint64(q)} } fallthrough default: return nil } } func (g Graph) canSetSpaceQuota(ctx context.Context, _ *userv1beta1.User, typ string) (bool, error) { permID := settingsServiceExt.SetPersonalSpaceQuotaPermission(0).Id if typ == _spaceTypeProject { permID = settingsServiceExt.SetProjectSpaceQuotaPermission(0).Id } _, err := g.permissionsService.GetPermissionByID(ctx, &settingssvc.GetPermissionByIDRequest{PermissionId: permID}) if err != nil { merror := merrors.FromError(err) if merror.Status == http.StatusText(http.StatusNotFound) { return false, nil } return false, err } return true, nil } func generateCs3Filters(request *godata.GoDataRequest) ([]*storageprovider.ListStorageSpacesRequest_Filter, error) { var filters []*storageprovider.ListStorageSpacesRequest_Filter if request.Query.Filter != nil { if request.Query.Filter.Tree.Token.Value == "eq" { switch request.Query.Filter.Tree.Children[0].Token.Value { case "driveType": filters = append(filters, listStorageSpacesTypeFilter(strings.Trim(request.Query.Filter.Tree.Children[1].Token.Value, "'"))) case "id": filters = append(filters, listStorageSpacesIDFilter(strings.Trim(request.Query.Filter.Tree.Children[1].Token.Value, "'"))) } } else { err := errors.Errorf("unsupported filter operand: %s", request.Query.Filter.Tree.Token.Value) return nil, err } } return filters, nil } func listStorageSpacesIDFilter(id string) *storageprovider.ListStorageSpacesRequest_Filter { return &storageprovider.ListStorageSpacesRequest_Filter{ Type: storageprovider.ListStorageSpacesRequest_Filter_TYPE_ID, Term: &storageprovider.ListStorageSpacesRequest_Filter_Id{ Id: &storageprovider.StorageSpaceId{ OpaqueId: id, }, }, } } func listStorageSpacesUserFilter(id string) *storageprovider.ListStorageSpacesRequest_Filter { return &storageprovider.ListStorageSpacesRequest_Filter{ Type: storageprovider.ListStorageSpacesRequest_Filter_TYPE_USER, Term: &storageprovider.ListStorageSpacesRequest_Filter_User{ User: &userv1beta1.UserId{ OpaqueId: id, }, }, } } func listStorageSpacesTypeFilter(spaceType string) *storageprovider.ListStorageSpacesRequest_Filter { return &storageprovider.ListStorageSpacesRequest_Filter{ Type: storageprovider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE, Term: &storageprovider.ListStorageSpacesRequest_Filter_SpaceType{ SpaceType: spaceType, }, } } // DeleteDrive deletes a storage drive (space). func (g Graph) DeleteDrive(w http.ResponseWriter, r *http.Request) { logger := g.logger.SubloggerWithRequestID(r.Context()) logger.Info().Msg("calling delete drive") rid, err := parseIDParam(r, "driveID") if err != nil { errorcode.RenderError(w, r, err) return } purge := parsePurgeHeader(r.Header) var opaque *types.Opaque if purge { opaque = &types.Opaque{ Map: map[string]*types.OpaqueEntry{ "purge": {}, }, } } gatewayClient, _ := g.gatewaySelector.Next() dRes, err := gatewayClient.DeleteStorageSpace(r.Context(), &storageprovider.DeleteStorageSpaceRequest{ Opaque: opaque, Id: &storageprovider.StorageSpaceId{ OpaqueId: storagespace.FormatResourceID(&rid), }, }) if err != nil { logger.Error().Err(err).Str("id", rid.String()).Msg("could not delete drive: transport error") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "transport error") return } switch dRes.GetStatus().GetCode() { case cs3rpc.Code_CODE_OK: w.WriteHeader(http.StatusNoContent) return case cs3rpc.Code_CODE_INVALID_ARGUMENT: logger.Debug().Interface("id", rid).Str("grpc", dRes.GetStatus().GetMessage()).Msg("could not delete drive: invalid argument") errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, dRes.Status.Message) return case cs3rpc.Code_CODE_PERMISSION_DENIED: logger.Debug().Interface("id", rid).Msg("could not delete drive: permission denied") errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "drive not found") return case cs3rpc.Code_CODE_NOT_FOUND: logger.Debug().Interface("id", rid).Msg("could not delete drive: drive not found") errorcode.ItemNotFound.Render(w, r, http.StatusNotFound, "drive not found") return case cs3rpc.Code_CODE_UNIMPLEMENTED: logger.Debug().Interface("id", rid).Msg("could not delete drive: delete not implemented for this type of drive") errorcode.NotAllowed.Render(w, r, http.StatusMethodNotAllowed, "drive cannot be deleted") return // don't expose internal error codes to the outside world default: logger.Debug().Str("grpc", dRes.GetStatus().GetMessage()).Interface("id", rid).Msg("could not delete drive: grpc error") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "grpc error") return } } func sortSpaces(req *godata.GoDataRequest, spaces []*libregraph.Drive) ([]*libregraph.Drive, error) { if req.Query.OrderBy == nil || len(req.Query.OrderBy.OrderByItems) != 1 { return spaces, nil } var less func(i, j int) bool switch req.Query.OrderBy.OrderByItems[0].Field.Value { case "name": // Natural / numeric-aware ordering so e.g. "Project 2" < "Project 10". c := collate.New(language.Und, collate.Numeric, collate.IgnoreCase) less = func(i, j int) bool { return c.CompareString(spaces[i].GetName(), spaces[j].GetName()) < 0 } case "lastModifiedDateTime": less = func(i, j int) bool { return lessSpacesByLastModifiedDateTime(spaces[i], spaces[j]) } default: return nil, errors.Errorf("we do not support <%s> as a order parameter", req.Query.OrderBy.OrderByItems[0].Field.Value) } if req.Query.OrderBy.OrderByItems[0].Order == _sortDescending { sort.Slice(spaces, reverse(less)) } else { sort.Slice(spaces, less) } return spaces, nil } func validateSpaceName(name string) error { if name == "" { return ErrNameEmpty } if len(name) > _maxSpaceNameLength { return ErrNameTooLong } for _, c := range _invalidSpaceNameCharacters { if strings.Contains(name, c) { return ErrForbiddenCharacter } } return nil } func getFieldMask(odataReq *godata.GoDataRequest) map[string]struct{} { fieldMask := map[string]struct{}{} if odataReq != nil && odataReq.Query.Select != nil && len(odataReq.Query.Select.SelectItems) > 0 { for _, item := range odataReq.Query.Select.SelectItems { for _, token := range item.Segments { fieldMask[token.Value] = struct{}{} } } } return fieldMask }