package operations_test import ( "context" "testing" "time" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/hash" "github.com/rclone/rclone/fs/operations" "github.com/rclone/rclone/fs/walk" "github.com/rclone/rclone/fstest" "github.com/rclone/rclone/lib/random" "github.com/spf13/pflag" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Check flag satisfies the interface var _ pflag.Value = (*operations.DeduplicateMode)(nil) func skipIfCantDedupe(t *testing.T, f fs.Fs) { if !f.Features().DuplicateFiles { t.Skip("Can't test deduplicate - no duplicate files possible") } if f.Features().PutUnchecked == nil { t.Skip("Can't test deduplicate - no PutUnchecked") } if f.Features().MergeDirs == nil { t.Skip("Can't test deduplicate - no MergeDirs") } } func skipIfNoHash(t *testing.T, f fs.Fs) { if f.Hashes().GetOne() == hash.None { t.Skip("Can't run this test without a hash") } } func skipIfNoModTime(t *testing.T, f fs.Fs) { if f.Precision() >= fs.ModTimeNotSupported { t.Skip("Can't run this test without modtimes") } } func TestDeduplicateInteractive(t *testing.T) { r := fstest.NewRun(t) defer r.Finalise() skipIfCantDedupe(t, r.Fremote) skipIfNoHash(t, r.Fremote) file1 := r.WriteUncheckedObject(context.Background(), "one", "This is one", t1) file2 := r.WriteUncheckedObject(context.Background(), "one", "This is one", t1) file3 := r.WriteUncheckedObject(context.Background(), "one", "This is one", t1) r.CheckWithDuplicates(t, file1, file2, file3) err := operations.Deduplicate(context.Background(), r.Fremote, operations.DeduplicateInteractive, false) require.NoError(t, err) fstest.CheckItems(t, r.Fremote, file1) } func TestDeduplicateSkip(t *testing.T) { r := fstest.NewRun(t) defer r.Finalise() skipIfCantDedupe(t, r.Fremote) haveHash := r.Fremote.Hashes().GetOne() != hash.None file1 := r.WriteUncheckedObject(context.Background(), "one", "This is one", t1) files := []fstest.Item{file1} if haveHash { file2 := r.WriteUncheckedObject(context.Background(), "one", "This is one", t1) files = append(files, file2) } file3 := r.WriteUncheckedObject(context.Background(), "one", "This is another one", t1) files = append(files, file3) r.CheckWithDuplicates(t, files...) err := operations.Deduplicate(context.Background(), r.Fremote, operations.DeduplicateSkip, false) require.NoError(t, err) r.CheckWithDuplicates(t, file1, file3) } func TestDeduplicateSizeOnly(t *testing.T) { r := fstest.NewRun(t) defer r.Finalise() skipIfCantDedupe(t, r.Fremote) ctx := context.Background() ci := fs.GetConfig(ctx) file1 := r.WriteUncheckedObject(context.Background(), "one", "This is one", t1) file2 := r.WriteUncheckedObject(context.Background(), "one", "THIS IS ONE", t1) file3 := r.WriteUncheckedObject(context.Background(), "one", "This is another one", t1) r.CheckWithDuplicates(t, file1, file2, file3) ci.SizeOnly = true defer func() { ci.SizeOnly = false }() err := operations.Deduplicate(context.Background(), r.Fremote, operations.DeduplicateSkip, false) require.NoError(t, err) r.CheckWithDuplicates(t, file1, file3) } func TestDeduplicateFirst(t *testing.T) { r := fstest.NewRun(t) defer r.Finalise() skipIfCantDedupe(t, r.Fremote) file1 := r.WriteUncheckedObject(context.Background(), "one", "This is one", t1) file2 := r.WriteUncheckedObject(context.Background(), "one", "This is one A", t1) file3 := r.WriteUncheckedObject(context.Background(), "one", "This is one BB", t1) r.CheckWithDuplicates(t, file1, file2, file3) err := operations.Deduplicate(context.Background(), r.Fremote, operations.DeduplicateFirst, false) require.NoError(t, err) // list until we get one object var objects, size int64 for try := 1; try <= *fstest.ListRetries; try++ { objects, size, err = operations.Count(context.Background(), r.Fremote) require.NoError(t, err) if objects == 1 { break } time.Sleep(time.Second) } assert.Equal(t, int64(1), objects) if size != file1.Size && size != file2.Size && size != file3.Size { t.Errorf("Size not one of the object sizes %d", size) } } func TestDeduplicateNewest(t *testing.T) { r := fstest.NewRun(t) defer r.Finalise() skipIfCantDedupe(t, r.Fremote) skipIfNoModTime(t, r.Fremote) file1 := r.WriteUncheckedObject(context.Background(), "one", "This is one", t1) file2 := r.WriteUncheckedObject(context.Background(), "one", "This is one too", t2) file3 := r.WriteUncheckedObject(context.Background(), "one", "This is another one", t3) r.CheckWithDuplicates(t, file1, file2, file3) err := operations.Deduplicate(context.Background(), r.Fremote, operations.DeduplicateNewest, false) require.NoError(t, err) fstest.CheckItems(t, r.Fremote, file3) } func TestDeduplicateNewestByHash(t *testing.T) { r := fstest.NewRun(t) defer r.Finalise() skipIfNoHash(t, r.Fremote) skipIfNoModTime(t, r.Fremote) contents := random.String(100) file1 := r.WriteObject(context.Background(), "one", contents, t1) file2 := r.WriteObject(context.Background(), "also/one", contents, t2) file3 := r.WriteObject(context.Background(), "another", contents, t3) file4 := r.WriteObject(context.Background(), "not-one", "stuff", t3) fstest.CheckItems(t, r.Fremote, file1, file2, file3, file4) err := operations.Deduplicate(context.Background(), r.Fremote, operations.DeduplicateNewest, true) require.NoError(t, err) fstest.CheckItems(t, r.Fremote, file3, file4) } func TestDeduplicateOldest(t *testing.T) { r := fstest.NewRun(t) defer r.Finalise() skipIfCantDedupe(t, r.Fremote) file1 := r.WriteUncheckedObject(context.Background(), "one", "This is one", t1) file2 := r.WriteUncheckedObject(context.Background(), "one", "This is one too", t2) file3 := r.WriteUncheckedObject(context.Background(), "one", "This is another one", t3) r.CheckWithDuplicates(t, file1, file2, file3) err := operations.Deduplicate(context.Background(), r.Fremote, operations.DeduplicateOldest, false) require.NoError(t, err) fstest.CheckItems(t, r.Fremote, file1) } func TestDeduplicateLargest(t *testing.T) { r := fstest.NewRun(t) defer r.Finalise() skipIfCantDedupe(t, r.Fremote) file1 := r.WriteUncheckedObject(context.Background(), "one", "This is one", t1) file2 := r.WriteUncheckedObject(context.Background(), "one", "This is one too", t2) file3 := r.WriteUncheckedObject(context.Background(), "one", "This is another one", t3) r.CheckWithDuplicates(t, file1, file2, file3) err := operations.Deduplicate(context.Background(), r.Fremote, operations.DeduplicateLargest, false) require.NoError(t, err) fstest.CheckItems(t, r.Fremote, file3) } func TestDeduplicateSmallest(t *testing.T) { r := fstest.NewRun(t) defer r.Finalise() skipIfCantDedupe(t, r.Fremote) file1 := r.WriteUncheckedObject(context.Background(), "one", "This is one", t1) file2 := r.WriteUncheckedObject(context.Background(), "one", "This is one too", t2) file3 := r.WriteUncheckedObject(context.Background(), "one", "This is another one", t3) r.CheckWithDuplicates(t, file1, file2, file3) err := operations.Deduplicate(context.Background(), r.Fremote, operations.DeduplicateSmallest, false) require.NoError(t, err) fstest.CheckItems(t, r.Fremote, file1) } func TestDeduplicateRename(t *testing.T) { r := fstest.NewRun(t) defer r.Finalise() skipIfCantDedupe(t, r.Fremote) file1 := r.WriteUncheckedObject(context.Background(), "one.txt", "This is one", t1) file2 := r.WriteUncheckedObject(context.Background(), "one.txt", "This is one too", t2) file3 := r.WriteUncheckedObject(context.Background(), "one.txt", "This is another one", t3) file4 := r.WriteUncheckedObject(context.Background(), "one-1.txt", "This is not a duplicate", t1) r.CheckWithDuplicates(t, file1, file2, file3, file4) err := operations.Deduplicate(context.Background(), r.Fremote, operations.DeduplicateRename, false) require.NoError(t, err) require.NoError(t, walk.ListR(context.Background(), r.Fremote, "", true, -1, walk.ListObjects, func(entries fs.DirEntries) error { entries.ForObject(func(o fs.Object) { remote := o.Remote() if remote != "one-1.txt" && remote != "one-2.txt" && remote != "one-3.txt" && remote != "one-4.txt" { t.Errorf("Bad file name after rename %q", remote) } size := o.Size() if size != file1.Size && size != file2.Size && size != file3.Size && size != file4.Size { t.Errorf("Size not one of the object sizes %d", size) } if remote == "one-1.txt" && size != file4.Size { t.Errorf("Existing non-duplicate file modified %q", remote) } }) return nil })) } // This should really be a unit test, but the test framework there // doesn't have enough tools to make it easy func TestMergeDirs(t *testing.T) { r := fstest.NewRun(t) defer r.Finalise() mergeDirs := r.Fremote.Features().MergeDirs if mergeDirs == nil { t.Skip("Can't merge directories") } file1 := r.WriteObject(context.Background(), "dupe1/one.txt", "This is one", t1) file2 := r.WriteObject(context.Background(), "dupe2/two.txt", "This is one too", t2) file3 := r.WriteObject(context.Background(), "dupe3/three.txt", "This is another one", t3) objs, dirs, err := walk.GetAll(context.Background(), r.Fremote, "", true, 1) require.NoError(t, err) assert.Equal(t, 3, len(dirs)) assert.Equal(t, 0, len(objs)) err = mergeDirs(context.Background(), dirs) require.NoError(t, err) file2.Path = "dupe1/two.txt" file3.Path = "dupe1/three.txt" fstest.CheckItems(t, r.Fremote, file1, file2, file3) objs, dirs, err = walk.GetAll(context.Background(), r.Fremote, "", true, 1) require.NoError(t, err) assert.Equal(t, 1, len(dirs)) assert.Equal(t, 0, len(objs)) assert.Equal(t, "dupe1", dirs[0].Remote()) }