diff --git a/src/cli/print/print.go b/src/cli/print/print.go index 633171400..3c4cceb05 100644 --- a/src/cli/print/print.go +++ b/src/cli/print/print.go @@ -90,7 +90,7 @@ func Err(ctx context.Context, s ...any) { // if s is nil, prints nothing. // Prepends the message with "Error: " func Errf(ctx context.Context, tmpl string, s ...any) { - outf(getRootCmd(ctx).ErrOrStderr(), "Error: "+tmpl, s...) + outf(getRootCmd(ctx).ErrOrStderr(), "\nError: \n\t"+tmpl+"\n", s...) } // Out prints the params to cobra's output writer (stdOut by default) diff --git a/src/pkg/control/options.go b/src/pkg/control/options.go index 2c560870d..23375f229 100644 --- a/src/pkg/control/options.go +++ b/src/pkg/control/options.go @@ -1,15 +1,8 @@ package control import ( - "context" - "strings" - - "golang.org/x/exp/slices" - - "github.com/alcionai/corso/src/internal/common/dttm" "github.com/alcionai/corso/src/pkg/control/repository" "github.com/alcionai/corso/src/pkg/extensions" - "github.com/alcionai/corso/src/pkg/logger" ) // Options holds the optional configurations for a process @@ -54,87 +47,6 @@ func Defaults() Options { } } -// --------------------------------------------------------------------------- -// Restore Configuration -// --------------------------------------------------------------------------- - -const ( - defaultRestoreLocation = "Corso_Restore_" -) - -// CollisionPolicy describes how the datalayer behaves in case of a collision. -type CollisionPolicy string - -const ( - Unknown CollisionPolicy = "" - Skip CollisionPolicy = "skip" - Copy CollisionPolicy = "copy" - Replace CollisionPolicy = "replace" -) - -func ValidCollisionPolicies() map[CollisionPolicy]struct{} { - return map[CollisionPolicy]struct{}{ - Skip: {}, - Copy: {}, - Replace: {}, - } -} - -const RootLocation = "/" - -// RestoreConfig contains -type RestoreConfig struct { - // Defines the per-item collision handling policy. - // Defaults to Skip. - OnCollision CollisionPolicy - - // ProtectedResource specifies which resource the data will be restored to. - // If empty, restores to the same resource that was backed up. - // Defaults to empty. - ProtectedResource string - - // Location specifies the container into which the data will be restored. - // Only accepts container names, does not accept IDs. - // If empty or "/", data will get restored in place, beginning at the root. - // Defaults to "Corso_Restore_" - Location string - - // Drive specifies the drive into which the data will be restored. - // If empty, data is restored to the same drive that was backed up. - // Defaults to empty. - Drive string -} - -func DefaultRestoreConfig(timeFormat dttm.TimeFormat) RestoreConfig { - return RestoreConfig{ - OnCollision: Skip, - Location: defaultRestoreLocation + dttm.FormatNow(timeFormat), - } -} - -// EnsureRestoreConfigDefaults sets all non-supported values in the config -// struct to the default value. -func EnsureRestoreConfigDefaults( - ctx context.Context, - rc RestoreConfig, -) RestoreConfig { - if !slices.Contains([]CollisionPolicy{Skip, Copy, Replace}, rc.OnCollision) { - logger.Ctx(ctx). - With( - "bad_collision_policy", rc.OnCollision, - "default_collision_policy", Skip). - Info("setting collision policy to default") - - rc.OnCollision = Skip - } - - if strings.TrimSpace(rc.Location) == RootLocation { - rc.Location = "" - } - - return rc -} - // --------------------------------------------------------------------------- // Feature Flags and Toggles // --------------------------------------------------------------------------- diff --git a/src/pkg/control/restore.go b/src/pkg/control/restore.go new file mode 100644 index 000000000..5fc5f7be8 --- /dev/null +++ b/src/pkg/control/restore.go @@ -0,0 +1,87 @@ +package control + +import ( + "context" + "strings" + + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" + + "github.com/alcionai/corso/src/internal/common/dttm" + "github.com/alcionai/corso/src/pkg/logger" +) + +const ( + defaultRestoreLocation = "Corso_Restore_" +) + +// CollisionPolicy describes how the datalayer behaves in case of a collision. +type CollisionPolicy string + +const ( + Unknown CollisionPolicy = "" + Skip CollisionPolicy = "skip" + Copy CollisionPolicy = "copy" + Replace CollisionPolicy = "replace" +) + +func ValidCollisionPolicies() map[CollisionPolicy]struct{} { + return map[CollisionPolicy]struct{}{ + Skip: {}, + Copy: {}, + Replace: {}, + } +} + +const RootLocation = "/" + +// RestoreConfig contains +type RestoreConfig struct { + // Defines the per-item collision handling policy. + // Defaults to Skip. + OnCollision CollisionPolicy + + // ProtectedResource specifies which resource the data will be restored to. + // If empty, restores to the same resource that was backed up. + // Defaults to empty. + ProtectedResource string + + // Location specifies the container into which the data will be restored. + // Only accepts container names, does not accept IDs. + // If empty or "/", data will get restored in place, beginning at the root. + // Defaults to "Corso_Restore_" + Location string + + // Drive specifies the drive into which the data will be restored. + // If empty, data is restored to the same drive that was backed up. + // Defaults to empty. + Drive string +} + +func DefaultRestoreConfig(timeFormat dttm.TimeFormat) RestoreConfig { + return RestoreConfig{ + OnCollision: Skip, + Location: defaultRestoreLocation + dttm.FormatNow(timeFormat), + } +} + +// EnsureRestoreConfigDefaults sets all non-supported values in the config +// struct to the default value. +func EnsureRestoreConfigDefaults( + ctx context.Context, + rc RestoreConfig, +) RestoreConfig { + if !slices.Contains(maps.Keys(ValidCollisionPolicies()), rc.OnCollision) { + logger.Ctx(ctx). + With( + "bad_collision_policy", rc.OnCollision, + "default_collision_policy", Skip). + Info("setting collision policy to default") + + rc.OnCollision = Skip + } + + rc.Location = strings.TrimPrefix(strings.TrimSpace(rc.Location), "/") + + return rc +} diff --git a/src/pkg/control/test/options_test.go b/src/pkg/control/restore_test.go similarity index 72% rename from src/pkg/control/test/options_test.go rename to src/pkg/control/restore_test.go index fba4e3cac..0690d1fda 100644 --- a/src/pkg/control/test/options_test.go +++ b/src/pkg/control/restore_test.go @@ -1,4 +1,4 @@ -package test +package control_test import ( "testing" @@ -10,15 +10,15 @@ import ( "github.com/alcionai/corso/src/pkg/control" ) -type OptionsUnitSuite struct { +type RestoreUnitSuite struct { tester.Suite } -func TestOptionsUnitSuite(t *testing.T) { - suite.Run(t, &OptionsUnitSuite{Suite: tester.NewUnitSuite(t)}) +func TestRestoreUnitSuite(t *testing.T) { + suite.Run(t, &RestoreUnitSuite{Suite: tester.NewUnitSuite(t)}) } -func (suite *OptionsUnitSuite) TestEnsureRestoreConfigDefaults() { +func (suite *RestoreUnitSuite) TestEnsureRestoreConfigDefaults() { table := []struct { name string input control.RestoreConfig @@ -69,6 +69,21 @@ func (suite *OptionsUnitSuite) TestEnsureRestoreConfigDefaults() { Drive: "", }, }, + { + name: "populated with slash prefix, then modified", + input: control.RestoreConfig{ + OnCollision: control.CollisionPolicy("batman"), + ProtectedResource: "", + Location: "/smarfs", + Drive: "", + }, + expect: control.RestoreConfig{ + OnCollision: control.Skip, + ProtectedResource: "", + Location: "smarfs", + Drive: "", + }, + }, } for _, test := range table { suite.Run(test.name, func() {