Skip to content

Commit

Permalink
cli: when migrating snapshot honor destination policies (#1433)
Browse files Browse the repository at this point in the history
In particular this applies compression based on destination repository
policies.

By default ignore rules are disabled during migration to preserve all
files, but that can be optionally enabled using '--apply-ignore-rules'.

Fixes #1429
  • Loading branch information
jkowalski committed Oct 22, 2021
1 parent 39a195c commit c8c433f
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 3 deletions.
9 changes: 8 additions & 1 deletion cli/command_snapshot_migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type commandSnapshotMigrate struct {
migrateOverwritePolicies bool
migrateLatestOnly bool
migrateParallel int
applyIgnoreRules bool

svc advancedAppServices
out textOutput
Expand All @@ -36,6 +37,7 @@ func (c *commandSnapshotMigrate) setup(svc advancedAppServices, parent commandPa
cmd.Flag("overwrite-policies", "Overwrite policies").BoolVar(&c.migrateOverwritePolicies)
cmd.Flag("latest-only", "Only migrate the latest snapshot").BoolVar(&c.migrateLatestOnly)
cmd.Flag("parallel", "Number of sources to migrate in parallel").Default("1").IntVar(&c.migrateParallel)
cmd.Flag("apply-ignore-rules", "When migrating also apply current ignore rules").BoolVar(&c.applyIgnoreRules)
cmd.Action(svc.repositoryWriterAction(c.run))

c.svc = svc
Expand Down Expand Up @@ -269,7 +271,12 @@ func (c *commandSnapshotMigrate) migrateSingleSourceSnapshot(ctx context.Context
return err
}

var policyTree *policy.Tree
policyTree, err := policy.TreeForSource(ctx, destRepo, m.Source)
if err != nil {
return errors.Wrap(err, "error generating policy tree")
}

uploader.DisableIgnoreRules = !c.applyIgnoreRules

newm, err := uploader.Upload(ctx, sourceEntry, policyTree, m.Source, previous...)
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions snapshot/snapshotfs/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ type Uploader struct {
// How frequently to create checkpoint snapshot entries.
CheckpointInterval time.Duration

// When set to true, do not ignore any files, regardless of policy settings.
DisableIgnoreRules bool

repo repo.RepositoryWriter

// stats must be allocated on heap to enforce 64-bit alignment due to atomic access on ARM.
Expand Down Expand Up @@ -1206,6 +1209,10 @@ func (u *Uploader) Upload(
}

func (u *Uploader) wrapIgnorefs(entry fs.Directory, policyTree *policy.Tree) fs.Directory {
if u.DisableIgnoreRules {
return entry
}

return ignorefs.New(entry, policyTree, ignorefs.ReportIgnoredFiles(func(fname string, md fs.Entry) {
if md.IsDir() {
u.Progress.ExcludedDir(fname)
Expand Down
109 changes: 107 additions & 2 deletions tests/end_to_end_test/snapshot_migrate_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package endtoend_test

import (
"os"
"path/filepath"
"testing"

"github.com/google/uuid"
"github.com/stretchr/testify/require"

"github.com/kopia/kopia/internal/testutil"
"github.com/kopia/kopia/snapshot"
"github.com/kopia/kopia/tests/testenv"
)

Expand All @@ -16,6 +22,7 @@ func (s *formatSpecificTestSuite) TestSnapshotMigrate(t *testing.T) {
defer e.RunAndExpectSuccess(t, "repo", "disconnect")

e.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", e.RepoDir)
e.RunAndExpectSuccess(t, "policy", "set", "--global", "--compression=pgzip")
e.RunAndExpectSuccess(t, "snapshot", "create", ".")

e.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir1)
Expand All @@ -28,19 +35,117 @@ func (s *formatSpecificTestSuite) TestSnapshotMigrate(t *testing.T) {
e.RunAndExpectSuccess(t, "snapshot", "create", sharedTestDataDir3)
e.RunAndExpectSuccess(t, "policy", "set", sharedTestDataDir3, "--keep-daily=88")

compressibleDir := testutil.TempDirectory(t)

for i := 0; i < 10; i++ {
require.NoError(t, writeCompressibleFile(filepath.Join(compressibleDir, uuid.NewString())))
}

e.RunAndExpectSuccess(t, "snapshot", "create", compressibleDir)

dirSize1 := mustGetTotalDirSize(t, e.RepoDir)

sourceSnapshotCount := len(e.RunAndExpectSuccess(t, "snapshot", "list", ".", "-a"))
sourcePolicyCount := len(e.RunAndExpectSuccess(t, "policy", "list"))

dstenv := testenv.NewCLITest(t, s.formatFlags, runner)

dstenv.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", dstenv.RepoDir)

dstenv.RunAndExpectSuccess(t, "snapshot", "migrate", "--source-config", filepath.Join(e.ConfigDir, ".kopia.config"), "--all", "--parallel=5")
dstenv.RunAndExpectSuccess(t, "snapshot", "migrate", "--source-config", filepath.Join(e.ConfigDir, ".kopia.config"), "--all", "--parallel=5", "--overwrite-policies")
dstenv.RunAndVerifyOutputLineCount(t, sourceSnapshotCount, "snapshot", "list", ".", "-a")
dstenv.RunAndVerifyOutputLineCount(t, sourcePolicyCount, "policy", "list")

// migrate again, which should be a no-op, and should not create any more policies/snapshots
dstenv.RunAndExpectSuccess(t, "snapshot", "migrate", "--source-config", filepath.Join(e.ConfigDir, ".kopia.config"), "--all")
dstenv.RunAndExpectSuccess(t, "snapshot", "migrate", "--source-config", filepath.Join(e.ConfigDir, ".kopia.config"), "--all", "--overwrite-policies")
dstenv.RunAndVerifyOutputLineCount(t, sourceSnapshotCount, "snapshot", "list", ".", "-a")
dstenv.RunAndVerifyOutputLineCount(t, sourcePolicyCount, "policy", "list")

// make sure compression was applied during migration
dirSize2 := mustGetTotalDirSize(t, dstenv.RepoDir)

require.Less(t, dirSize2, dirSize1*110/100)
}

func (s *formatSpecificTestSuite) TestSnapshotMigrateWithIgnores(t *testing.T) {
t.Parallel()

runner := testenv.NewInProcRunner(t)
e := testenv.NewCLITest(t, s.formatFlags, runner)

defer e.RunAndExpectSuccess(t, "repo", "disconnect")

sd := testutil.TempDirectory(t)

require.NoError(t, os.WriteFile(filepath.Join(sd, "file1.txt"), []byte{1, 2, 3}, 0o666))
require.NoError(t, os.WriteFile(filepath.Join(sd, "file2.txt"), []byte{1, 2, 3}, 0o666))

e.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", e.RepoDir)
e.RunAndExpectSuccess(t, "snapshot", "create", sd)

dstenv := testenv.NewCLITest(t, s.formatFlags, runner)
dstenv.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", dstenv.RepoDir)

// now set policy to ignore file2.txt and migrate
dstenv.RunAndExpectSuccess(t, "policy", "set", sd, "--add-ignore", "file2.txt")
dstenv.RunAndExpectSuccess(t, "snapshot", "migrate", "--source-config", filepath.Join(e.ConfigDir, ".kopia.config"), "--all", "--apply-ignore-rules")

var manifests []snapshot.Manifest

testutil.MustParseJSONLines(t, dstenv.RunAndExpectSuccess(t, "snapshot", "list", "-a", sd, "--json"), &manifests)

if got, want := len(manifests), 1; got != want {
t.Fatalf("unexpected number of snapshots %v want %v", got, want)
}

lines := dstenv.RunAndExpectSuccess(t, "ls", string(manifests[0].RootObjectID()))

// make sure file2.txt was not migrated.
require.Contains(t, lines, "file1.txt")
require.NotContains(t, lines, "file2.txt")
}

func mustGetTotalDirSize(t *testing.T, dirpath string) int64 {
t.Helper()

ent, err := os.ReadDir(dirpath)
require.NoError(t, err)

var total int64

for _, e := range ent {
fi, err := e.Info()

require.NoError(t, err)

if !fi.IsDir() {
total += fi.Size()
} else {
total += mustGetTotalDirSize(t, filepath.Join(dirpath, e.Name()))
}
}

return total
}

func writeCompressibleFile(fname string) error {
f, err := os.Create(fname)
if err != nil {
return err
}

defer f.Close()

// 1000 x 64000
for i := 0; i < 1000; i++ {
val := uuid.NewString()

for j := 0; j < 100; j++ {
if _, err := f.WriteString(val); err != nil {
return err
}
}
}

return nil
}

0 comments on commit c8c433f

Please sign in to comment.