Space Trash-bin expiration cli (#5500)

* add storage-users trash-bin cli
add task to clean up outdated trash-bin resources
add trash-bin cli purge-expired command to purge expired trash-bin resources
add purge-expired task tests
This commit is contained in:
Florian Schade
2023-02-10 12:04:47 +01:00
committed by GitHub
parent 9e73221a68
commit 5da3df8ffe
13 changed files with 650 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
Enhancement: Cli to purge expired trash-bin items
Introduction of a new cli command to purge old trash-bin items.
The command is part of the `storage-users` service and can be used as follows:
`ocis storage-users trash-bin purge-expired`.
The `purge-expired` command configuration is done in the `ocis`configuration or as usual by using environment variables.
ENV `STORAGE_USERS_PURGE_TRASH_BIN_USER_ID` is used to obtain space trash-bin information and takes the system admin user as the default `OCIS_ADMIN_USER_ID`.
It should be noted, that this is only set by default in the single binary. The command only considers spaces to which the user has access and delete permission.
ENV `STORAGE_USERS_PURGE_TRASH_BIN_PERSONAL_DELETE_BEFORE` has a default value of `30 days`, which means the command will delete all files older than `30 days`.
The value is human-readable, valid values are `24h`, `60m`, `60s` etc. `0` is equivalent to disable and prevents the deletion of `personal space` trash-bin files.
ENV `STORAGE_USERS_PURGE_TRASH_BIN_PROJECT_DELETE_BEFORE` has a default value of `30 days`, which means the command will delete all files older than `30 days`.
The value is human-readable, valid values are `24h`, `60m`, `60s` etc. `0` is equivalent to disable and prevents the deletion of `project space` trash-bin files.
Likewise, only spaces of the type `project` and `personal` are taken into account.
Spaces of type `virtual`, for example, are ignored.
https://github.com/owncloud/ocis/pull/5500
https://github.com/owncloud/ocis/issues/5499

View File

@@ -19,6 +19,7 @@ func GetCommands(cfg *config.Config) cli.Commands {
// interaction with this service
Uploads(cfg),
TrashBin(cfg),
// infos about this service
Health(cfg),

View File

@@ -7,6 +7,7 @@ import (
"path"
"github.com/cs3org/reva/v2/cmd/revad/runtime"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/gofrs/uuid"
"github.com/oklog/run"
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
@@ -15,6 +16,7 @@ import (
"github.com/owncloud/ocis/v2/ocis-pkg/version"
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config/parser"
"github.com/owncloud/ocis/v2/services/storage-users/pkg/event"
"github.com/owncloud/ocis/v2/services/storage-users/pkg/logging"
"github.com/owncloud/ocis/v2/services/storage-users/pkg/revaconfig"
"github.com/owncloud/ocis/v2/services/storage-users/pkg/server/debug"
@@ -88,6 +90,27 @@ func Server(cfg *config.Config) *cli.Command {
logger.Fatal().Err(err).Msg("failed to register the grpc endpoint")
}
{
stream, err := event.NewStream(cfg.Events)
if err != nil {
logger.Fatal().Err(err).Msg("can't connect to nats")
}
gw, err := pool.GetGatewayServiceClient(cfg.Reva.Address)
if err != nil {
return err
}
eventSVC, err := event.NewService(gw, stream, logger, *cfg)
if err != nil {
logger.Fatal().Err(err).Msg("can't create event service")
}
gr.Add(eventSVC.Run, func(_ error) {
cancel()
})
}
return gr.Run()
},
}

View File

@@ -0,0 +1,55 @@
package command
import (
"time"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/owncloud/ocis/v2/ocis-pkg/config/configlog"
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config/parser"
"github.com/owncloud/ocis/v2/services/storage-users/pkg/event"
"github.com/urfave/cli/v2"
)
// TrashBin wraps trash-bin related sub-commands.
func TrashBin(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "trash-bin",
Usage: "manage trash-bin's",
Subcommands: []*cli.Command{
PurgeExpiredResources(cfg),
},
}
}
// PurgeExpiredResources cli command removes old trash-bin items.
func PurgeExpiredResources(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "purge-expired",
Usage: "Purge expired trash-bin items",
Flags: []cli.Flag{},
Before: func(c *cli.Context) error {
return configlog.ReturnFatal(parser.ParseConfig(cfg))
},
Action: func(c *cli.Context) error {
stream, err := event.NewStream(cfg.Events)
if err != nil {
return err
}
if err := events.Publish(stream, event.PurgeTrashBin{ExecutionTime: time.Now()}); err != nil {
return err
}
// go-micro nats implementation uses async publishing,
// therefore we need to manually wait.
//
// FIXME: upstream pr
//
// https://github.com/go-micro/plugins/blob/3e77393890683be4bacfb613bc5751867d584692/v4/events/natsjs/nats.go#L115
time.Sleep(5 * time.Second)
return nil
},
}
}

View File

@@ -2,6 +2,7 @@ package config
import (
"context"
"time"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
)
@@ -32,6 +33,7 @@ type Config struct {
ExposeDataServer bool `yaml:"expose_data_server" env:"STORAGE_USERS_EXPOSE_DATA_SERVER" desc:"Exposes the data server directly to users and bypasses the data gateway. Ensure that the data server address is reachable by users."`
ReadOnly bool `yaml:"readonly" env:"STORAGE_USERS_READ_ONLY" desc:"Set this storage to be read-only."`
UploadExpiration int64 `yaml:"upload_expiration" env:"STORAGE_USERS_UPLOAD_EXPIRATION" desc:"Duration in seconds after which uploads will expire."`
Tasks Tasks `yaml:"tasks"`
Supervised bool `yaml:"-"`
Context context.Context `yaml:"-"`
@@ -218,3 +220,15 @@ type LocalDriver struct {
ShareFolder string `yaml:"share_folder"`
UserLayout string `yaml:"user_layout"`
}
// Tasks wraps task configurations
type Tasks struct {
PurgeTrashBin PurgeTrashBin `yaml:"purge_trash_bin"`
}
// PurgeTrashBin contains all necessary configurations to clean up the respective trash cans
type PurgeTrashBin struct {
UserID string `yaml:"user_id" env:"OCIS_ADMIN_USER_ID;STORAGE_USERS_PURGE_TRASH_BIN_USER_ID" desc:"ID of the user who collects all necessary information for deletion."`
PersonalDeleteBefore time.Duration `yaml:"personal_delete_before" env:"STORAGE_USERS_PURGE_TRASH_BIN_PERSONAL_DELETE_BEFORE" desc:"Specifies the period of time in which items that have been in the personal trash-bin for longer than this value should be deleted. A value of 0 means no automatic deletion"`
ProjectDeleteBefore time.Duration `yaml:"project_delete_before" env:"STORAGE_USERS_PURGE_TRASH_BIN_PROJECT_DELETE_BEFORE" desc:"Specifies the period of time in which items that have been in the project trash-bin for longer than this value should be deleted. A value of 0 means no automatic deletion"`
}

View File

@@ -2,6 +2,7 @@ package defaults
import (
"path/filepath"
"time"
"github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
@@ -87,6 +88,12 @@ func DefaultConfig() *config.Config {
Store: "memory",
Database: "users",
},
Tasks: config.Tasks{
PurgeTrashBin: config.PurgeTrashBin{
ProjectDeleteBefore: 30 * 24 * time.Hour,
PersonalDeleteBefore: 30 * 24 * time.Hour,
},
},
}
}
@@ -139,6 +146,10 @@ func EnsureDefaults(cfg *config.Config) {
cfg.GRPC.TLS.Key = cfg.Commons.GRPCServiceTLS.Key
}
}
if cfg.Tasks.PurgeTrashBin.UserID == "" && cfg.Commons != nil {
cfg.Tasks.PurgeTrashBin.UserID = cfg.Commons.AdminUserID
}
}
func Sanitize(cfg *config.Config) {

View File

@@ -0,0 +1,51 @@
package event
import (
"crypto/tls"
"crypto/x509"
"os"
"github.com/cs3org/reva/v2/pkg/events/stream"
"github.com/go-micro/plugins/v4/events/natsjs"
ociscrypto "github.com/owncloud/ocis/v2/ocis-pkg/crypto"
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
"go-micro.dev/v4/events"
)
// NewStream prepares the requested nats stream and returns it.
func NewStream(cfg config.Events) (events.Stream, error) {
var tlsConf *tls.Config
if cfg.EnableTLS {
var rootCAPool *x509.CertPool
if cfg.TLSRootCaCertPath != "" {
rootCrtFile, err := os.Open(cfg.TLSRootCaCertPath)
if err != nil {
return nil, err
}
rootCAPool, err = ociscrypto.NewCertPoolFromPEM(rootCrtFile)
if err != nil {
return nil, err
}
cfg.TLSInsecure = false
}
tlsConf = &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: rootCAPool,
}
}
s, err := stream.Nats(
natsjs.TLSConfig(tlsConf),
natsjs.Address(cfg.Addr),
natsjs.ClusterID(cfg.ClusterID),
)
if err != nil {
return nil, err
}
return s, nil
}

View File

@@ -0,0 +1,85 @@
package event
import (
"time"
apiGateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
"github.com/owncloud/ocis/v2/services/storage-users/pkg/config"
"github.com/owncloud/ocis/v2/services/storage-users/pkg/task"
)
const (
consumerGroup = "storage-users"
)
// Service wraps all common logic that is needed to react to incoming events.
type Service struct {
gatewayClient apiGateway.GatewayAPIClient
eventStream events.Stream
logger log.Logger
config config.Config
}
// NewService prepares and returns a Service implementation.
func NewService(gatewayClient apiGateway.GatewayAPIClient, eventStream events.Stream, logger log.Logger, conf config.Config) (Service, error) {
svc := Service{
gatewayClient: gatewayClient,
eventStream: eventStream,
logger: logger,
config: conf,
}
return svc, nil
}
// Run to fulfil Runner interface
func (s Service) Run() error {
ch, err := events.Consume(s.eventStream, consumerGroup, PurgeTrashBin{})
if err != nil {
return err
}
for e := range ch {
var errs []error
switch ev := e.(type) {
case PurgeTrashBin:
executionTime := ev.ExecutionTime
if executionTime.IsZero() {
executionTime = time.Now()
}
executantID := ev.ExecutantID
if executantID == nil {
executantID = &apiUser.UserId{OpaqueId: s.config.Tasks.PurgeTrashBin.UserID}
}
tasks := map[task.SpaceType]time.Time{
task.Project: executionTime.Add(-s.config.Tasks.PurgeTrashBin.ProjectDeleteBefore),
task.Personal: executionTime.Add(-s.config.Tasks.PurgeTrashBin.PersonalDeleteBefore),
}
for spaceType, deleteBefore := range tasks {
// skip task execution if the deleteBefore time is the same as the now time,
// which indicates that the duration configuration for this space type is set to 0 which is the equivalent to disabled.
if deleteBefore.Equal(executionTime) {
continue
}
if err = task.PurgeTrashBin(executantID, deleteBefore, spaceType, s.gatewayClient, s.config.Commons.MachineAuthAPIKey); err != nil {
errs = append(errs, err)
}
}
}
for _, err := range errs {
s.logger.Error().Err(err).Interface("event", e)
}
}
return nil
}

View File

@@ -0,0 +1,21 @@
package event
import (
"encoding/json"
"time"
apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
)
// PurgeTrashBin wraps all needed information to purge a trash-bin
type PurgeTrashBin struct {
ExecutantID *apiUser.UserId
ExecutionTime time.Time
}
// Unmarshal to fulfill umarshaller interface
func (PurgeTrashBin) Unmarshal(v []byte) (interface{}, error) {
e := PurgeTrashBin{}
err := json.Unmarshal(v, &e)
return e, err
}

View File

@@ -0,0 +1,11 @@
package task
// SpaceType represents known space types
type SpaceType string
const (
// Personal represents a space of type personal
Personal SpaceType = "personal"
// Project represents a space of type project
Project SpaceType = "project"
)

View File

@@ -0,0 +1,13 @@
package task_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestTask(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Task Suite")
}

View File

@@ -0,0 +1,112 @@
package task
import (
"fmt"
"time"
apiGateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
apiRpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
apiProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/utils"
)
// PurgeTrashBin can be used to purge space trash-bin's,
// the provided executantID must have space access.
// removeBefore specifies how long an item must be in the trash-bin to be deleted,
// items that stay there for a shorter time are ignored and kept in place.
func PurgeTrashBin(executantID *apiUser.UserId, deleteBefore time.Time, spaceType SpaceType, gwc apiGateway.GatewayAPIClient, machineAuthAPIKey string) error {
executantCtx, _, err := utils.Impersonate(executantID, gwc, machineAuthAPIKey)
if err != nil {
return err
}
listStorageSpacesResponse, err := gwc.ListStorageSpaces(executantCtx, &apiProvider.ListStorageSpacesRequest{
Filters: []*apiProvider.ListStorageSpacesRequest_Filter{
{
Type: apiProvider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE,
Term: &apiProvider.ListStorageSpacesRequest_Filter_SpaceType{
SpaceType: string(spaceType),
},
},
},
})
if err != nil {
return err
}
for _, storageSpace := range listStorageSpacesResponse.StorageSpaces {
var (
err error
impersonationID *apiUser.UserId
storageSpaceReference = &apiProvider.Reference{
ResourceId: storageSpace.GetRoot(),
}
)
switch SpaceType(storageSpace.GetSpaceType()) {
case Personal:
impersonationID = storageSpace.GetOwner().GetId()
case Project:
var permissionsMap map[string]*apiProvider.ResourcePermissions
err := utils.ReadJSONFromOpaque(storageSpace.GetOpaque(), "grants", &permissionsMap)
if err != nil {
break
}
for id, permissions := range permissionsMap {
if !permissions.Delete {
continue
}
impersonationID = &apiUser.UserId{
OpaqueId: id,
}
break
}
default:
continue
}
if err != nil {
return err
}
if impersonationID == nil {
return fmt.Errorf("can't impersonate space user for space: %s", storageSpace.GetId().GetOpaqueId())
}
impersonatedCtx, _, err := utils.Impersonate(impersonationID, gwc, machineAuthAPIKey)
if err != nil {
return err
}
listRecycleResponse, err := gwc.ListRecycle(impersonatedCtx, &apiProvider.ListRecycleRequest{Ref: storageSpaceReference})
if err != nil {
return err
}
for _, recycleItem := range listRecycleResponse.GetRecycleItems() {
doDelete := utils.TSToUnixNano(recycleItem.DeletionTime) < utils.TSToUnixNano(utils.TimeToTS(deleteBefore))
if !doDelete {
continue
}
purgeRecycleResponse, err := gwc.PurgeRecycle(impersonatedCtx, &apiProvider.PurgeRecycleRequest{
Ref: storageSpaceReference,
Key: recycleItem.Key,
})
if purgeRecycleResponse.GetStatus().GetCode() != apiRpc.Code_CODE_OK {
return errtypes.NewErrtypeFromStatus(purgeRecycleResponse.Status)
}
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -0,0 +1,230 @@
package task_test
import (
"context"
"encoding/json"
"errors"
"google.golang.org/grpc"
"time"
apiGateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
apiUser "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
apiRpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
apiProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
apiTypes "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/cs3org/reva/v2/pkg/utils"
cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/owncloud/ocis/v2/services/storage-users/pkg/task"
"github.com/stretchr/testify/mock"
)
func MustMarshal(v any) []byte {
b, err := json.Marshal(v)
if err != nil {
panic(err)
}
return b
}
var _ = Describe("trash", func() {
var (
gwc *cs3mocks.GatewayAPIClient
ctx context.Context
now time.Time
genericError error
user *apiUser.User
getUserResponse *apiUser.GetUserResponse
authenticateResponse *apiGateway.AuthenticateResponse
listStorageSpacesResponse *apiProvider.ListStorageSpacesResponse
personalSpace *apiProvider.StorageSpace
projectSpace *apiProvider.StorageSpace
virtualSpace *apiProvider.StorageSpace
)
BeforeEach(func() {
gwc = &cs3mocks.GatewayAPIClient{}
ctx = context.Background()
now = time.Now()
genericError = errors.New("any")
getUserResponse = &apiUser.GetUserResponse{
Status: status.NewOK(ctx),
}
authenticateResponse = &apiGateway.AuthenticateResponse{
Status: status.NewOK(ctx),
Token: "",
}
listStorageSpacesResponse = &apiProvider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*apiProvider.StorageSpace{},
}
personalSpace = &apiProvider.StorageSpace{
SpaceType: "personal",
Id: &apiProvider.StorageSpaceId{
OpaqueId: "personal",
},
Root: &apiProvider.ResourceId{
OpaqueId: "personal",
},
}
projectSpace = &apiProvider.StorageSpace{
SpaceType: "project",
Id: &apiProvider.StorageSpaceId{
OpaqueId: "project",
},
Root: &apiProvider.ResourceId{
OpaqueId: "project",
},
Opaque: &apiTypes.Opaque{},
}
// virtual is here as an example,
// the task ignores all space types expect `project` and `personal`.
virtualSpace = &apiProvider.StorageSpace{
SpaceType: "virtual",
Id: &apiProvider.StorageSpaceId{
OpaqueId: "virtual",
},
Root: &apiProvider.ResourceId{
OpaqueId: "virtual",
},
}
user = &apiUser.User{
Id: &apiUser.UserId{
OpaqueId: "user",
},
}
})
Describe("PurgeTrashBin", func() {
It("throws an error if the user cannot authenticate", func() {
gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(nil, genericError)
err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
Expect(err).To(HaveOccurred())
})
It("throws an error if space listing fails", func() {
gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(nil, genericError)
err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
Expect(err).To(HaveOccurred())
})
It("throws an error if a personal space user can't be impersonated", func() {
listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{personalSpace}
gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
Expect(err).To(MatchError(errors.New("can't impersonate space user for space: personal")))
})
It("throws an error if a project space user can't be impersonated", func() {
listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{projectSpace}
gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
Expect(err).To(MatchError(errors.New("can't impersonate space user for space: project")))
})
It("throws an error if a project space has no user with delete permissions", func() {
listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{projectSpace}
projectSpace.Opaque.Map = map[string]*apiTypes.OpaqueEntry{
"grants": {
Value: MustMarshal(map[string]*apiProvider.ResourcePermissions{
"admin": {
Delete: false,
},
}),
},
}
gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
Expect(err).To(MatchError(errors.New("can't impersonate space user for space: project")))
})
It("only deletes items older than the specified period", func() {
var (
recycleItems = map[string][]*apiProvider.RecycleItem{
"personal": {
{Key: "now", DeletionTime: utils.TimeToTS(now)},
{Key: "after", DeletionTime: utils.TimeToTS(now.Add(1 * time.Second))},
{Key: "before", DeletionTime: utils.TimeToTS(now.Add(-1 * time.Second))},
},
"project": {
{Key: "now", DeletionTime: utils.TimeToTS(now)},
{Key: "after", DeletionTime: utils.TimeToTS(now.Add(1 * time.Minute))},
{Key: "before", DeletionTime: utils.TimeToTS(now.Add(-1 * time.Minute))},
},
"virtual": {
{Key: "now", DeletionTime: utils.TimeToTS(now)},
{Key: "after", DeletionTime: utils.TimeToTS(now.Add(1 * time.Hour))},
{Key: "before", DeletionTime: utils.TimeToTS(now.Add(-1 * time.Hour))},
},
}
)
personalSpace.Owner = user
listStorageSpacesResponse.StorageSpaces = []*apiProvider.StorageSpace{
personalSpace,
projectSpace,
virtualSpace,
}
projectSpace.Opaque.Map = map[string]*apiTypes.OpaqueEntry{
"grants": {
Decoder: "json",
Value: MustMarshal(map[string]*apiProvider.ResourcePermissions{
"admin": {
Delete: true,
},
}),
},
}
gwc.On("GetUser", mock.Anything, mock.Anything).Return(getUserResponse, nil)
gwc.On("Authenticate", mock.Anything, mock.Anything).Return(authenticateResponse, nil)
gwc.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(listStorageSpacesResponse, nil)
gwc.On("ListRecycle", mock.Anything, mock.Anything).Return(
func(_ context.Context, req *apiProvider.ListRecycleRequest, _ ...grpc.CallOption) *apiProvider.ListRecycleResponse {
return &apiProvider.ListRecycleResponse{
RecycleItems: recycleItems[req.Ref.ResourceId.OpaqueId],
}
}, nil,
)
gwc.On("PurgeRecycle", mock.Anything, mock.Anything).Return(
func(_ context.Context, req *apiProvider.PurgeRecycleRequest, _ ...grpc.CallOption) *apiProvider.PurgeRecycleResponse {
var items []*apiProvider.RecycleItem
for _, item := range recycleItems[req.Ref.ResourceId.OpaqueId] {
if req.Key == item.Key {
continue
}
items = append(items, item)
}
recycleItems[req.Ref.ResourceId.OpaqueId] = items
return &apiProvider.PurgeRecycleResponse{
Status: &apiRpc.Status{
Code: apiRpc.Code_CODE_OK,
},
}
}, nil,
)
err := task.PurgeTrashBin(user.Id, now, task.Project, gwc, "")
Expect(err).To(BeNil())
Expect(recycleItems["personal"]).To(HaveLen(2))
Expect(recycleItems["project"]).To(HaveLen(2))
// virtual spaces are ignored
Expect(recycleItems["virtual"]).To(HaveLen(3))
})
})
})