fix restore to dest with leading slash (#3713)

restoring to "--destination /foo" errors, since
drive folders aren't allowed to contain leading
slashes in their name.  This PR oversteps the
correction a little to remove the leading slash
prefix from all destination declarations.

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🐛 Bugfix

#### Issue(s)

* #3562

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-07-06 12:25:18 -06:00 committed by GitHub
parent 597a51c6bc
commit f58cc90958
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 108 additions and 94 deletions

View File

@ -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)

View File

@ -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_<current_dttm>"
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
// ---------------------------------------------------------------------------

View File

@ -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_<current_dttm>"
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
}

View File

@ -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() {