From 367f1ff9e8927f3ced7df002d0ec07150f88caec Mon Sep 17 00:00:00 2001 From: Roman Perekhod <2403905@gmail.com> Date: Tue, 19 Dec 2023 15:44:28 +0100 Subject: [PATCH] trash-bin cli has been exteneded by the list and restore commands (#7917) * trash-bin cli has been exteneded by the list and restore commands * v4 to v5 changes --------- Co-authored-by: Roman Perekhod --- changelog/unreleased/add-trach-bin-cli.md | 7 + services/gateway/pkg/config/config.go | 2 +- services/storage-users/README.md | 63 ++- .../storage-users/pkg/command/trash_bin.go | 433 ++++++++++++++++++ .../pkg/command/trash_bin_test.go | 65 +++ services/storage-users/pkg/config/config.go | 14 +- .../pkg/config/defaults/defaultconfig.go | 1 + 7 files changed, 579 insertions(+), 6 deletions(-) create mode 100644 changelog/unreleased/add-trach-bin-cli.md create mode 100644 services/storage-users/pkg/command/trash_bin_test.go diff --git a/changelog/unreleased/add-trach-bin-cli.md b/changelog/unreleased/add-trach-bin-cli.md new file mode 100644 index 00000000000..5b2771035ba --- /dev/null +++ b/changelog/unreleased/add-trach-bin-cli.md @@ -0,0 +1,7 @@ +Enhancement: Add cli commands for trash-bin + +We added the `list` and `restore` commands to the trash-bin items to the CLI + +https://github.com/owncloud/ocis/pull/7917 +https://github.com/cs3org/reva/pull/4392 +https://github.com/owncloud/ocis/issues/7845 diff --git a/services/gateway/pkg/config/config.go b/services/gateway/pkg/config/config.go index 4240be1b80d..e676f59ec4b 100644 --- a/services/gateway/pkg/config/config.go +++ b/services/gateway/pkg/config/config.go @@ -70,7 +70,7 @@ type Debug struct { } type GRPCConfig struct { - Addr string `yaml:"addr" env:"GATEWAY_GRPC_ADDR" desc:"The bind address of the GRPC service."` + Addr string `yaml:"addr" env:"OCIS_GATEWAY_GRPC_ADDR;GATEWAY_GRPC_ADDR" desc:"The bind address of the GRPC service."` TLS *shared.GRPCServiceTLS `yaml:"tls"` Namespace string `yaml:"-"` Protocol string `yaml:"protocol" env:"GATEWAY_GRPC_PROTOCOL" desc:"The transport protocol of the GRPC service."` diff --git a/services/storage-users/README.md b/services/storage-users/README.md index 1cd9ac1c5d4..e89cbd30a91 100644 --- a/services/storage-users/README.md +++ b/services/storage-users/README.md @@ -78,12 +78,13 @@ Cleaned uploads: -This command is about purging old trash-bin items of `project` spaces (spaces that have been created manually) and `personal` spaces. +This command is about the trash-bin to get an overview of items, restore items and purging old items of `project` spaces (spaces that have been created manually) and `personal` spaces. ```bash ocis storage-users trash-bin ``` +#### Purge-expired ```plaintext COMMANDS: purge-expired Purge all expired items from the trashbin @@ -97,6 +98,66 @@ The configuration for the `purge-expired` command is done by using the following * `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. +#### List and Restore Trash-Bins Items + +To authenticate the cli command use `OCIS_SERVICE_ACCOUNT_SECRET=` and `OCIS_SERVICE_ACCOUNT_ID=`. The `storage-users` cli tool uses the default address to establish the connection to the `gateway` service. If the connection is failed check your custom `gateway` +service `GATEWAY_GRPC_ADDR` configuration and set the same address to `storage-users` variable `OCIS_GATEWAY_GRPC_ADDR` or `STORAGE_USERS_GATEWAY_GRPC_ADDR`. The variable `STORAGE_USERS_CLI_MAX_ATTEMPTS_RENAME_FILE` +defines a maximum number of attempts to rename a file when the user restores the file with `--option keep-both` to existing destination with the same name. + +The ID sources: +- personal 'spaceID' in a `https://{host}/graph/v1.0/me/drives?$filter=driveType+eq+personal` +- project 'spaceID' in a `https://{host}/graph/v1.0/me/drives?$filter=driveType+eq+project` + +```bash +NAME: + ocis storage-users trash-bin list - Print a list of all trash-bin items of a space. + +USAGE: + ocis storage-users trash-bin list command [command options] ['spaceID' required] + +COMMANDS: + help, h Shows a list of commands or help for one command + +OPTIONS: + --verbose, -v Get more verbose output (default: false) + --help, -h show help + +``` + +```bash +NAME: + ocis storage-users trash-bin restore-all - Restore all trash-bin items for a space. + +USAGE: + ocis storage-users trash-bin restore-all command [command options] ['spaceID' required] + +COMMANDS: + help, h Shows a list of commands or help for one command + +OPTIONS: + --option value, -o value The restore option defines the behavior for a file to be restored, where the file name already already exists in the target space. Supported values are: 'skip', 'replace' and 'keep-both'. (default: The default value is 'skip' overwriting an existing file) + --verbose, -v Get more verbose output (default: false) + --yes, -y Automatic yes to prompts. Assume 'yes' as answer to all prompts and run non-interactively. (default: false) + --help, -h show help + +``` + +```bash +NAME: + ocis storage-users trash-bin restore - Restore a trash-bin item by ID. + +USAGE: + ocis storage-users trash-bin restore command [command options] ['spaceID' required] ['itemID' required] + +COMMANDS: + help, h Shows a list of commands or help for one command + +OPTIONS: + --option value, -o value The restore option defines the behavior for a file to be restored, where the file name already already exists in the target space. Supported values are: 'skip', 'replace' and 'keep-both'. (default: The default value is 'skip' overwriting an existing file) + --verbose, -v Get more verbose output (default: false) + --help, -h show help +``` + ## Caching The `storage-users` service caches stat, metadata and uuids of files and folders via the configured store in `STORAGE_USERS_STAT_CACHE_STORE`, `STORAGE_USERS_FILEMETADATA_CACHE_STORE` and `STORAGE_USERS_ID_CACHE_STORE`. Possible stores are: diff --git a/services/storage-users/pkg/command/trash_bin.go b/services/storage-users/pkg/command/trash_bin.go index 8489ff90bc2..dcdb079594f 100644 --- a/services/storage-users/pkg/command/trash_bin.go +++ b/services/storage-users/pkg/command/trash_bin.go @@ -1,16 +1,59 @@ package command import ( + "context" + "fmt" + "os" + "path" + "path/filepath" + "strconv" + "strings" "time" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/v2/pkg/events" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/v2/pkg/storagespace" + "github.com/cs3org/reva/v2/pkg/utils" + "github.com/mohae/deepcopy" + tw "github.com/olekukonko/tablewriter" "github.com/owncloud/ocis/v2/ocis-pkg/config/configlog" + zlog "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/config/parser" "github.com/owncloud/ocis/v2/services/storage-users/pkg/event" + "github.com/rs/zerolog" "github.com/urfave/cli/v2" ) +const ( + SKIP = iota + REPLACE + KEEP_BOTH +) + +var _optionFlagTmpl = cli.StringFlag{ + Name: "option", + Value: "skip", + Aliases: []string{"o"}, + Usage: "The restore option defines the behavior for a file to be restored, where the file name already already exists in the target space. Supported values are: 'skip', 'replace' and 'keep-both'.", + DefaultText: "The default value is 'skip' overwriting an existing file", +} + +var _verboseFlagTmpl = cli.BoolFlag{ + Name: "verbose", + Aliases: []string{"v"}, + Usage: "Get more verbose output", +} + +var _applyYesFlagTmpl = cli.BoolFlag{ + Name: "yes", + Aliases: []string{"y"}, + Usage: "Automatic yes to prompts. Assume 'yes' as answer to all prompts and run non-interactively.", +} + // TrashBin wraps trash-bin related sub-commands. func TrashBin(cfg *config.Config) *cli.Command { return &cli.Command{ @@ -18,6 +61,9 @@ func TrashBin(cfg *config.Config) *cli.Command { Usage: "manage trash-bin's", Subcommands: []*cli.Command{ PurgeExpiredResources(cfg), + listTrashBinItems(cfg), + restoreAllTrashBinItems(cfg), + restoreTrashBindItem(cfg), }, } } @@ -53,3 +99,390 @@ func PurgeExpiredResources(cfg *config.Config) *cli.Command { }, } } + +func listTrashBinItems(cfg *config.Config) *cli.Command { + var verboseVal bool + verboseFlag := _verboseFlagTmpl + verboseFlag.Destination = &verboseVal + return &cli.Command{ + Name: "list", + Usage: "Print a list of all trash-bin items of a space.", + ArgsUsage: "['spaceID' required]", + Flags: []cli.Flag{ + &verboseFlag, + }, + Before: func(c *cli.Context) error { + return configlog.ReturnFatal(parser.ParseConfig(cfg)) + }, + Action: func(c *cli.Context) error { + log := cliLogger(verboseVal) + var spaceID string + if c.NArg() > 0 { + spaceID = c.Args().Get(0) + } + if spaceID == "" { + _ = cli.ShowSubcommandHelp(c) + return fmt.Errorf("spaceID is requered") + } + log.Info().Msgf("Getting trash-bin items for spaceID: '%s' ...", spaceID) + + ref, err := storagespace.ParseReference(spaceID) + if err != nil { + return err + } + client, err := pool.GetGatewayServiceClient(cfg.RevaGatewayGRPCAddr) + if err != nil { + return fmt.Errorf("error selecting gateway client %w", err) + } + ctx, err := utils.GetServiceUserContext(cfg.ServiceAccount.ServiceAccountID, client, cfg.ServiceAccount.ServiceAccountSecret) + if err != nil { + return fmt.Errorf("could not get service user context %w", err) + } + res, err := listRecycle(ctx, client, ref) + if err != nil { + return err + } + + table := itemsTable(len(res.GetRecycleItems())) + for _, item := range res.GetRecycleItems() { + table.Append([]string{item.GetKey(), item.GetRef().GetPath(), itemType(item.GetType()), utils.TSToTime(item.GetDeletionTime()).UTC().Format(time.RFC3339)}) + } + table.Render() + fmt.Println("Use an itemID to restore an item.") + return nil + }, + } +} + +func restoreAllTrashBinItems(cfg *config.Config) *cli.Command { + var optionFlagVal string + var overwriteOption int + optionFlag := _optionFlagTmpl + optionFlag.Destination = &optionFlagVal + var verboseVal bool + verboseFlag := _verboseFlagTmpl + verboseFlag.Destination = &verboseVal + var applyYesVal bool + applyYesFlag := _applyYesFlagTmpl + applyYesFlag.Destination = &applyYesVal + return &cli.Command{ + Name: "restore-all", + Usage: "Restore all trash-bin items for a space.", + ArgsUsage: "['spaceID' required]", + Flags: []cli.Flag{ + &optionFlag, + &verboseFlag, + &applyYesFlag, + }, + Before: func(c *cli.Context) error { + return configlog.ReturnFatal(parser.ParseConfig(cfg)) + }, + Action: func(c *cli.Context) error { + log := cliLogger(verboseVal) + var spaceID string + if c.NArg() > 0 { + spaceID = c.Args().Get(0) + } + if spaceID == "" { + _ = cli.ShowSubcommandHelp(c) + return cli.Exit("The spaceID is required", 1) + } + switch optionFlagVal { + case "skip": + overwriteOption = SKIP + case "replace": + overwriteOption = REPLACE + case "keep-both": + overwriteOption = KEEP_BOTH + default: + _ = cli.ShowSubcommandHelp(c) + return cli.Exit("The option flag is invalid", 1) + } + log.Info().Msgf("Restoring trash-bin items for spaceID: '%s' ...", spaceID) + + ref, err := storagespace.ParseReference(spaceID) + if err != nil { + return err + } + client, err := pool.GetGatewayServiceClient(cfg.RevaGatewayGRPCAddr) + if err != nil { + return fmt.Errorf("error selecting gateway client %w", err) + } + ctx, err := utils.GetServiceUserContext(cfg.ServiceAccount.ServiceAccountID, client, cfg.ServiceAccount.ServiceAccountSecret) + if err != nil { + return fmt.Errorf("could not get service user context %w", err) + } + res, err := listRecycle(ctx, client, ref) + if err != nil { + return err + } + + if !applyYesVal { + for { + fmt.Printf("Found %d items that could be restored, continue (Y/n), show the items list (s): ", len(res.GetRecycleItems())) + var i string + _, err := fmt.Scanf("%s", &i) + if err != nil { + log.Err(err).Send() + continue + } + if strings.ToLower(i) == "y" { + break + } else if strings.ToLower(i) == "n" { + return nil + } else if strings.ToLower(i) == "s" { + table := itemsTable(len(res.GetRecycleItems())) + for _, item := range res.GetRecycleItems() { + table.Append([]string{item.GetKey(), item.GetRef().GetPath(), itemType(item.GetType()), utils.TSToTime(item.GetDeletionTime()).UTC().Format(time.RFC3339)}) + } + table.Render() + } + } + } + + log.Info().Msgf("Run restoring-all with option=%s", optionFlagVal) + for _, item := range res.GetRecycleItems() { + log.Info().Msgf("restoring itemID: '%s', path: '%s', type: '%s'", item.GetKey(), item.GetRef().GetPath(), itemType(item.GetType())) + dstRes, err := restore(ctx, client, ref, item, overwriteOption, cfg.CliMaxAttemptsRenameFile, log) + if err != nil { + log.Err(err).Msg("trash-bin item restoring error") + continue + } + fmt.Printf("itemID: '%s', path: '%s', restored as '%s'\n", item.GetKey(), item.GetRef().GetPath(), dstRes.GetPath()) + } + return nil + }, + } +} + +func restoreTrashBindItem(cfg *config.Config) *cli.Command { + var optionFlagVal string + var overwriteOption int + optionFlag := _optionFlagTmpl + optionFlag.Destination = &optionFlagVal + var verboseVal bool + verboseFlag := _verboseFlagTmpl + verboseFlag.Destination = &verboseVal + return &cli.Command{ + Name: "restore", + Usage: "Restore a trash-bin item by ID.", + ArgsUsage: "['spaceID' required] ['itemID' required]", + Flags: []cli.Flag{ + &optionFlag, + &verboseFlag, + }, + Before: func(c *cli.Context) error { + return configlog.ReturnFatal(parser.ParseConfig(cfg)) + }, + Action: func(c *cli.Context) error { + log := cliLogger(verboseVal) + var spaceID, itemID string + if c.NArg() > 1 { + spaceID = c.Args().Get(0) + itemID = c.Args().Get(1) + } + if spaceID == "" { + _ = cli.ShowSubcommandHelp(c) + return fmt.Errorf("spaceID is requered") + } + if itemID == "" { + _ = cli.ShowSubcommandHelp(c) + return fmt.Errorf("itemID is requered") + } + switch optionFlagVal { + case "skip": + overwriteOption = SKIP + case "replace": + overwriteOption = REPLACE + case "keep-both": + overwriteOption = KEEP_BOTH + default: + _ = cli.ShowSubcommandHelp(c) + return cli.Exit("The option flag is invalid", 1) + } + log.Info().Msgf("Restoring trash-bin item for spaceID: '%s' itemID: '%s' ...", spaceID, itemID) + + ref, err := storagespace.ParseReference(spaceID) + if err != nil { + return err + } + client, err := pool.GetGatewayServiceClient(cfg.RevaGatewayGRPCAddr) + if err != nil { + return fmt.Errorf("error selecting gateway client %w", err) + } + ctx, err := utils.GetServiceUserContext(cfg.ServiceAccount.ServiceAccountID, client, cfg.ServiceAccount.ServiceAccountSecret) + if err != nil { + return fmt.Errorf("could not get service user context %w", err) + } + res, err := listRecycle(ctx, client, ref) + if err != nil { + return err + } + + var found bool + var itemRef *provider.RecycleItem + for _, item := range res.GetRecycleItems() { + if item.GetKey() == itemID { + itemRef = item + found = true + break + } + } + if !found { + return fmt.Errorf("itemID '%s' not found", itemID) + } + log.Info().Msgf("Run restoring with option=%s", optionFlagVal) + log.Info().Msgf("restoring itemID: '%s', path: '%s', type: '%s", itemRef.GetKey(), itemRef.GetRef().GetPath(), itemType(itemRef.GetType())) + dstRes, err := restore(ctx, client, ref, itemRef, overwriteOption, cfg.CliMaxAttemptsRenameFile, log) + if err != nil { + return err + } + fmt.Printf("itemID: '%s', path: '%s', restored as '%s'\n", itemRef.GetKey(), itemRef.GetRef().GetPath(), dstRes.GetPath()) + return nil + }, + } +} + +func listRecycle(ctx context.Context, client gateway.GatewayAPIClient, ref provider.Reference) (*provider.ListRecycleResponse, error) { + _retrievingErrorMsg := "trash-bin items retrieving error" + res, err := client.ListRecycle(ctx, &provider.ListRecycleRequest{Ref: &ref, Key: "/"}) + if err != nil { + return nil, fmt.Errorf("%s %w", _retrievingErrorMsg, err) + } + if res.Status.Code != rpc.Code_CODE_OK { + return nil, fmt.Errorf("%s %s", _retrievingErrorMsg, res.Status.Code) + } + if len(res.GetRecycleItems()) == 0 { + return res, cli.Exit("The trash-bin is empty. Nothing to restore", 0) + } + return res, nil +} + +func restore(ctx context.Context, client gateway.GatewayAPIClient, ref provider.Reference, item *provider.RecycleItem, overwriteOption int, maxRenameAttempt int, log zlog.Logger) (*provider.Reference, error) { + dst, _ := deepcopy.Copy(ref).(provider.Reference) + dst.Path = utils.MakeRelativePath(item.GetRef().GetPath()) + // Restore request + req := &provider.RestoreRecycleItemRequest{ + Ref: &ref, + Key: path.Join(item.GetKey(), "/"), + RestoreRef: &dst, + } + + exists, dstStatRes, err := isDestinationExists(ctx, client, dst) + if err != nil { + return &dst, err + } + + if exists { + log.Info().Msgf("destination '%s' exists.", dstStatRes.GetInfo().GetPath()) + switch overwriteOption { + case SKIP: + return &dst, nil + case REPLACE: + // delete existing tree + delReq := &provider.DeleteRequest{Ref: &dst} + delRes, err := client.Delete(ctx, delReq) + if err != nil { + return &dst, fmt.Errorf("error sending grpc delete request %w", err) + } + if delRes.Status.Code != rpc.Code_CODE_OK && delRes.Status.Code != rpc.Code_CODE_NOT_FOUND { + return &dst, fmt.Errorf("deleting error %w", err) + } + case KEEP_BOTH: + // modify the file name + req.RestoreRef, err = resolveDestination(ctx, client, dst, maxRenameAttempt) + if err != nil { + return &dst, err + } + } + } + + res, err := client.RestoreRecycleItem(ctx, req) + if err != nil { + return req.RestoreRef, fmt.Errorf("restoring error %w", err) + } + if res.Status.Code != rpc.Code_CODE_OK { + return req.RestoreRef, fmt.Errorf("can not restore %s", res.Status.Code) + } + return req.RestoreRef, nil +} + +func resolveDestination(ctx context.Context, client gateway.GatewayAPIClient, dstRef provider.Reference, maxRenameAttempt int) (*provider.Reference, error) { + dst := dstRef + if maxRenameAttempt < 100 { + maxRenameAttempt = 100 + } + for i := 1; i < maxRenameAttempt; i++ { + dst.Path = modifyFilename(dstRef.Path, i) + exists, _, err := isDestinationExists(ctx, client, dst) + if err != nil { + return nil, err + } + if exists { + continue + } + return &dst, nil + } + return nil, fmt.Errorf("too many attempts to resolve the destination") +} + +func isDestinationExists(ctx context.Context, client gateway.GatewayAPIClient, dst provider.Reference) (bool, *provider.StatResponse, error) { + dstStatReq := &provider.StatRequest{Ref: &dst} + dstStatRes, err := client.Stat(ctx, dstStatReq) + if err != nil { + return false, nil, fmt.Errorf("error sending grpc stat request %w", err) + } + if dstStatRes.GetStatus().GetCode() == rpc.Code_CODE_OK { + return true, dstStatRes, nil + } + if dstStatRes.GetStatus().GetCode() == rpc.Code_CODE_NOT_FOUND { + return false, dstStatRes, nil + } + return false, dstStatRes, fmt.Errorf("stat request failed %s", dstStatRes.GetStatus()) +} + +// modify the file name like UI do +func modifyFilename(filename string, mod int) string { + var extension string + var found bool + expected := []string{".tar.gz", ".tar.bz", ".tar.bz2"} + for _, s := range expected { + var prefix string + prefix, found = strings.CutSuffix(strings.ToLower(filename), s) + if found { + extension = strings.TrimPrefix(filename, prefix) + break + } + } + if !found { + extension = filepath.Ext(filename) + } + name := filename[0 : len(filename)-len(extension)] + return fmt.Sprintf("%s (%d)%s", name, mod, extension) +} + +func itemType(it provider.ResourceType) string { + var itemType = "file" + if it == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + itemType = "folder" + } + return itemType +} + +func itemsTable(total int) *tw.Table { + table := tw.NewWriter(os.Stdout) + table.SetHeader([]string{"itemID", "path", "type", "delete at"}) + table.SetAutoFormatHeaders(false) + table.SetFooter([]string{"", "", "", "total count: " + strconv.Itoa(total)}) + return table +} + +func cliLogger(verbose bool) zlog.Logger { + logLvl := zerolog.ErrorLevel + if verbose { + logLvl = zerolog.InfoLevel + } + zerolog.SetGlobalLevel(zerolog.TraceLevel) + output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339, NoColor: true} + return zlog.Logger{zerolog.New(output).With().Timestamp().Logger().Level(logLvl)} +} diff --git a/services/storage-users/pkg/command/trash_bin_test.go b/services/storage-users/pkg/command/trash_bin_test.go new file mode 100644 index 00000000000..26cfd4bc156 --- /dev/null +++ b/services/storage-users/pkg/command/trash_bin_test.go @@ -0,0 +1,65 @@ +package command + +import ( + "testing" +) + +func Test_modifyFilename(t *testing.T) { + type args struct { + filename string + mod int + } + tests := []struct { + name string + args args + want string + }{ + { + name: "file", + args: args{filename: "file.txt", mod: 1}, + want: "file (1).txt", + }, + { + name: "file with path", + args: args{filename: "./file.txt", mod: 1}, + want: "./file (1).txt", + }, + { + name: "file with path 2", + args: args{filename: "./subdir/file.tar.gz", mod: 99}, + want: "./subdir/file (99).tar.gz", + }, + { + name: "file with path 3", + args: args{filename: "./sub dir/new file.tar.gz", mod: 99}, + want: "./sub dir/new file (99).tar.gz", + }, + { + name: "file without ext", + args: args{filename: "./subdir/file", mod: 2}, + want: "./subdir/file (2)", + }, + { + name: "file without ext 2", + args: args{filename: "./subdir/file 1", mod: 2}, + want: "./subdir/file 1 (2)", + }, + { + name: "file with emoji", + args: args{filename: "./subdir/file 🙂.tar.gz", mod: 3}, + want: "./subdir/file 🙂 (3).tar.gz", + }, + { + name: "file with emoji 2", + args: args{filename: "./subdir/file 🙂", mod: 2}, + want: "./subdir/file 🙂 (2)", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := modifyFilename(tt.args.filename, tt.args.mod); got != tt.want { + t.Errorf("modifyFilename() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/services/storage-users/pkg/config/config.go b/services/storage-users/pkg/config/config.go index e33a65dafa2..8e436959647 100644 --- a/services/storage-users/pkg/config/config.go +++ b/services/storage-users/pkg/config/config.go @@ -24,10 +24,11 @@ type Config struct { SkipUserGroupsInToken bool `yaml:"skip_user_groups_in_token" env:"STORAGE_USERS_SKIP_USER_GROUPS_IN_TOKEN" desc:"Disables the loading of user's group memberships from the reva access token."` GracefulShutdownTimeout int `yaml:"graceful_shutdown_timeout" env:"STORAGE_USERS_GRACEFUL_SHUTDOWN_TIMEOUT" desc:"The number of seconds to wait for the 'storage-users' service to shutdown cleanly before exiting with an error that gets logged. Note: This setting is only applicable when running the 'storage-users' service as a standalone service. See the text description for more details."` - Driver string `yaml:"driver" env:"STORAGE_USERS_DRIVER" desc:"The storage driver which should be used by the service. Defaults to 'ocis', Supported values are: 'ocis', 's3ng' and 'owncloudsql'. The 'ocis' driver stores all data (blob and meta data) in an POSIX compliant volume. The 's3ng' driver stores metadata in a POSIX compliant volume and uploads blobs to the s3 bucket."` - Drivers Drivers `yaml:"drivers"` - DataServerURL string `yaml:"data_server_url" env:"STORAGE_USERS_DATA_SERVER_URL" desc:"URL of the data server, needs to be reachable by the data gateway provided by the frontend service or the user if directly exposed."` - DataGatewayURL string `yaml:"data_gateway_url" env:"STORAGE_USERS_DATA_GATEWAY_URL" desc:"URL of the data gateway server"` + Driver string `yaml:"driver" env:"STORAGE_USERS_DRIVER" desc:"The storage driver which should be used by the service. Defaults to 'ocis', Supported values are: 'ocis', 's3ng' and 'owncloudsql'. The 'ocis' driver stores all data (blob and meta data) in an POSIX compliant volume. The 's3ng' driver stores metadata in a POSIX compliant volume and uploads blobs to the s3 bucket."` + Drivers Drivers `yaml:"drivers"` + DataServerURL string `yaml:"data_server_url" env:"STORAGE_USERS_DATA_SERVER_URL" desc:"URL of the data server, needs to be reachable by the data gateway provided by the frontend service or the user if directly exposed."` + DataGatewayURL string `yaml:"data_gateway_url" env:"STORAGE_USERS_DATA_GATEWAY_URL" desc:"URL of the data gateway server"` + TransferExpires int64 `yaml:"transfer_expires" env:"STORAGE_USERS_TRANSFER_EXPIRES" desc:"the time after which the token for upload postprocessing expires"` Events Events `yaml:"events"` StatCache StatCache `yaml:"stat_cache"` @@ -40,6 +41,11 @@ type Config struct { Tasks Tasks `yaml:"tasks"` ServiceAccount ServiceAccount `yaml:"service_account"` + // CLI + RevaGatewayGRPCAddr string `yaml:"gateway_addr" env:"OCIS_GATEWAY_GRPC_ADDR;STORAGE_USERS_GATEWAY_GRPC_ADDR" desc:"The bind address of the gateway GRPC address."` + MachineAuthAPIKey string `yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary for the access to resources from other services."` + CliMaxAttemptsRenameFile int `yaml:"max_attempts_rename_file" env:"STORAGE_USERS_CLI_MAX_ATTEMPTS_RENAME_FILE" desc:"The maximum number of attempts to rename a file when a user restores a file to an existing destination with the same name. The minimum value is 100."` + Supervised bool `yaml:"-"` Context context.Context `yaml:"-"` } diff --git a/services/storage-users/pkg/config/defaults/defaultconfig.go b/services/storage-users/pkg/config/defaults/defaultconfig.go index 993f4fd9a94..8bc070c6afc 100644 --- a/services/storage-users/pkg/config/defaults/defaultconfig.go +++ b/services/storage-users/pkg/config/defaults/defaultconfig.go @@ -44,6 +44,7 @@ func DefaultConfig() *config.Config { Reva: shared.DefaultRevaConfig(), DataServerURL: "http://localhost:9158/data", DataGatewayURL: "https://localhost:9200/data", + RevaGatewayGRPCAddr: "127.0.0.1:9142", TransferExpires: 86400, UploadExpiration: 24 * 60 * 60, GracefulShutdownTimeout: 30,