aboutsummaryrefslogtreecommitdiff
path: root/fs/operations/operations_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'fs/operations/operations_test.go')
-rw-r--r--fs/operations/operations_test.go1968
1 files changed, 1968 insertions, 0 deletions
diff --git a/fs/operations/operations_test.go b/fs/operations/operations_test.go
new file mode 100644
index 0000000..3fb5992
--- /dev/null
+++ b/fs/operations/operations_test.go
@@ -0,0 +1,1968 @@
+// Integration tests - test rclone by doing real transactions to a
+// storage provider to and from the local disk.
+//
+// By default it will use a local fs, however you can provide a
+// -remote option to use a different remote. The test_all.go script
+// is a wrapper to call this for all the test remotes.
+//
+// FIXME not safe for concurrent running of tests until fs.Config is
+// no longer a global
+//
+// NB When writing tests
+//
+// Make sure every series of writes to the remote has a
+// fstest.CheckItems() before use. This make sure the directory
+// listing is now consistent and stops cascading errors.
+//
+// Call accounting.GlobalStats().ResetCounters() before every fs.Sync() as it
+// uses the error count internally.
+
+package operations_test
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "regexp"
+ "strings"
+ "testing"
+ "time"
+
+ _ "github.com/mewsen/rclone-studip-backend-oot/backend/studip"
+ _ "github.com/rclone/rclone/backend/all" // import all backends
+ "github.com/rclone/rclone/fs"
+ "github.com/rclone/rclone/fs/accounting"
+ "github.com/rclone/rclone/fs/filter"
+ "github.com/rclone/rclone/fs/fshttp"
+ "github.com/rclone/rclone/fs/hash"
+ "github.com/rclone/rclone/fs/operations"
+ "github.com/rclone/rclone/fstest"
+ "github.com/rclone/rclone/fstest/fstests"
+ "github.com/rclone/rclone/lib/pacer"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
+)
+
+// Some times used in the tests
+var (
+ t1 = fstest.Time("2001-02-03T04:05:06.499999999Z")
+ t2 = fstest.Time("2011-12-25T12:59:59.123456789Z")
+ t3 = fstest.Time("2011-12-30T12:59:59.000000000Z")
+)
+
+// TestMain drives the tests
+func TestMain(m *testing.M) {
+ fstest.TestMain(m)
+}
+
+func TestMkdir(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+
+ err := operations.Mkdir(ctx, r.Fremote, "")
+ require.NoError(t, err)
+ fstest.CheckListing(t, r.Fremote, []fstest.Item{})
+
+ err = operations.Mkdir(ctx, r.Fremote, "")
+ require.NoError(t, err)
+}
+
+func TestLsd(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+ file1 := r.WriteObject(ctx, "sub dir/hello world", "hello world", t1)
+
+ r.CheckRemoteItems(t, file1)
+
+ var buf bytes.Buffer
+ err := operations.ListDir(ctx, r.Fremote, &buf)
+ require.NoError(t, err)
+ res := buf.String()
+ assert.Contains(t, res, "sub dir\n")
+}
+
+func TestLs(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+ file1 := r.WriteBoth(ctx, "potato2", "------------------------------------------------------------", t1)
+ file2 := r.WriteBoth(ctx, "empty space", "-", t2)
+
+ r.CheckRemoteItems(t, file1, file2)
+
+ var buf bytes.Buffer
+ err := operations.List(ctx, r.Fremote, &buf)
+ require.NoError(t, err)
+ res := buf.String()
+ assert.Contains(t, res, " 1 empty space\n")
+ assert.Contains(t, res, " 60 potato2\n")
+}
+
+func TestLsWithFilesFrom(t *testing.T) {
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ r := fstest.NewRun(t)
+ file1 := r.WriteBoth(ctx, "potato2", "------------------------------------------------------------", t1)
+ file2 := r.WriteBoth(ctx, "empty space", "-", t2)
+
+ r.CheckRemoteItems(t, file1, file2)
+
+ // Set the --files-from equivalent
+ f, err := filter.NewFilter(nil)
+ require.NoError(t, err)
+ require.NoError(t, f.AddFile("potato2"))
+ require.NoError(t, f.AddFile("notfound"))
+
+ // Change the active filter
+ ctx = filter.ReplaceConfig(ctx, f)
+
+ var buf bytes.Buffer
+ err = operations.List(ctx, r.Fremote, &buf)
+ require.NoError(t, err)
+ assert.Equal(t, " 60 potato2\n", buf.String())
+
+ // Now try with --no-traverse
+ ci.NoTraverse = true
+
+ buf.Reset()
+ err = operations.List(ctx, r.Fremote, &buf)
+ require.NoError(t, err)
+ assert.Equal(t, " 60 potato2\n", buf.String())
+}
+
+func TestLsLong(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+ file1 := r.WriteBoth(ctx, "potato2", "------------------------------------------------------------", t1)
+ file2 := r.WriteBoth(ctx, "empty space", "-", t2)
+
+ r.CheckRemoteItems(t, file1, file2)
+
+ var buf bytes.Buffer
+ err := operations.ListLong(ctx, r.Fremote, &buf)
+ require.NoError(t, err)
+ res := buf.String()
+ lines := strings.Split(strings.Trim(res, "\n"), "\n")
+ assert.Equal(t, 2, len(lines))
+
+ timeFormat := "2006-01-02 15:04:05.000000000"
+ precision := r.Fremote.Precision()
+ location := time.Now().Location()
+ checkTime := func(m, filename string, expected time.Time) {
+ modTime, err := time.ParseInLocation(timeFormat, m, location) // parse as localtime
+ if err != nil {
+ t.Errorf("Error parsing %q: %v", m, err)
+ } else {
+ fstest.AssertTimeEqualWithPrecision(t, filename, expected, modTime, precision)
+ }
+ }
+
+ m1 := regexp.MustCompile(`(?m)^ 1 (\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d{9}) empty space$`)
+ if ms := m1.FindStringSubmatch(res); ms == nil {
+ t.Errorf("empty space missing: %q", res)
+ } else {
+ checkTime(ms[1], "empty space", t2.Local())
+ }
+
+ m2 := regexp.MustCompile(`(?m)^ 60 (\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d{9}) potato2$`)
+ if ms := m2.FindStringSubmatch(res); ms == nil {
+ t.Errorf("potato2 missing: %q", res)
+ } else {
+ checkTime(ms[1], "potato2", t1.Local())
+ }
+}
+
+func TestHashSums(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+ file1 := r.WriteBoth(ctx, "potato2", "------------------------------------------------------------", t1)
+ file2 := r.WriteBoth(ctx, "empty space", "-", t2)
+
+ r.CheckRemoteItems(t, file1, file2)
+
+ hashes := r.Fremote.Hashes()
+
+ var quickXorHash hash.Type
+ err := quickXorHash.Set("QuickXorHash")
+ require.NoError(t, err)
+
+ for _, test := range []struct {
+ name string
+ download bool
+ base64 bool
+ ht hash.Type
+ want []string
+ }{
+ {
+ ht: hash.MD5,
+ want: []string{
+ "336d5ebc5436534e61d16e63ddfca327 empty space\n",
+ "d6548b156ea68a4e003e786df99eee76 potato2\n",
+ },
+ },
+ {
+ ht: hash.MD5,
+ download: true,
+ want: []string{
+ "336d5ebc5436534e61d16e63ddfca327 empty space\n",
+ "d6548b156ea68a4e003e786df99eee76 potato2\n",
+ },
+ },
+ {
+ ht: hash.SHA1,
+ want: []string{
+ "3bc15c8aae3e4124dd409035f32ea2fd6835efc9 empty space\n",
+ "9dc7f7d3279715991a22853f5981df582b7f9f6d potato2\n",
+ },
+ },
+ {
+ ht: hash.SHA1,
+ download: true,
+ want: []string{
+ "3bc15c8aae3e4124dd409035f32ea2fd6835efc9 empty space\n",
+ "9dc7f7d3279715991a22853f5981df582b7f9f6d potato2\n",
+ },
+ },
+ {
+ ht: quickXorHash,
+ want: []string{
+ "2d00000000000000000000000100000000000000 empty space\n",
+ "4001dad296b6b4a52d6d694b67dad296b6b4a52d potato2\n",
+ },
+ },
+ {
+ ht: quickXorHash,
+ download: true,
+ want: []string{
+ "2d00000000000000000000000100000000000000 empty space\n",
+ "4001dad296b6b4a52d6d694b67dad296b6b4a52d potato2\n",
+ },
+ },
+ {
+ ht: quickXorHash,
+ base64: true,
+ want: []string{
+ "LQAAAAAAAAAAAAAAAQAAAAAAAAA= empty space\n",
+ "QAHa0pa2tKUtbWlLZ9rSlra0pS0= potato2\n",
+ },
+ },
+ {
+ ht: quickXorHash,
+ base64: true,
+ download: true,
+ want: []string{
+ "LQAAAAAAAAAAAAAAAQAAAAAAAAA= empty space\n",
+ "QAHa0pa2tKUtbWlLZ9rSlra0pS0= potato2\n",
+ },
+ },
+ } {
+ if !hashes.Contains(test.ht) {
+ continue
+ }
+ name := cases.Title(language.Und, cases.NoLower).String(test.ht.String())
+ if test.download {
+ name += "Download"
+ }
+ if test.base64 {
+ name += "Base64"
+ }
+ t.Run(name, func(t *testing.T) {
+ var buf bytes.Buffer
+ err := operations.HashLister(ctx, test.ht, test.base64, test.download, r.Fremote, &buf)
+ require.NoError(t, err)
+ res := buf.String()
+ for _, line := range test.want {
+ assert.Contains(t, res, line)
+ }
+ })
+ }
+}
+
+func TestHashSumsWithErrors(t *testing.T) {
+ ctx := context.Background()
+ memFs, err := fs.NewFs(ctx, ":memory:")
+ require.NoError(t, err)
+
+ // Make a test file
+ content := "-"
+ item1 := fstest.NewItem("file1", content, t1)
+ _ = fstests.PutTestContents(ctx, t, memFs, &item1, content, true)
+
+ // MemoryFS supports MD5
+ buf := &bytes.Buffer{}
+ err = operations.HashLister(ctx, hash.MD5, false, false, memFs, buf)
+ require.NoError(t, err)
+ assert.Contains(t, buf.String(), "336d5ebc5436534e61d16e63ddfca327 file1\n")
+
+ // MemoryFS can't do SHA1, but UNSUPPORTED must not appear in the output
+ buf.Reset()
+ err = operations.HashLister(ctx, hash.SHA1, false, false, memFs, buf)
+ require.NoError(t, err)
+ assert.NotContains(t, buf.String(), " UNSUPPORTED ")
+
+ // ERROR must not appear in the output either
+ assert.NotContains(t, buf.String(), " ERROR ")
+ // TODO mock an unreadable file
+}
+
+func TestHashStream(t *testing.T) {
+ reader := strings.NewReader("")
+ in := io.NopCloser(reader)
+ out := &bytes.Buffer{}
+ for _, test := range []struct {
+ input string
+ ht hash.Type
+ wantHex string
+ wantBase64 string
+ }{
+ {
+ input: "",
+ ht: hash.MD5,
+ wantHex: "d41d8cd98f00b204e9800998ecf8427e -\n",
+ wantBase64: "1B2M2Y8AsgTpgAmY7PhCfg== -\n",
+ },
+ {
+ input: "",
+ ht: hash.SHA1,
+ wantHex: "da39a3ee5e6b4b0d3255bfef95601890afd80709 -\n",
+ wantBase64: "2jmj7l5rSw0yVb_vlWAYkK_YBwk= -\n",
+ },
+ {
+ input: "Hello world!",
+ ht: hash.MD5,
+ wantHex: "86fb269d190d2c85f6e0468ceca42a20 -\n",
+ wantBase64: "hvsmnRkNLIX24EaM7KQqIA== -\n",
+ },
+ {
+ input: "Hello world!",
+ ht: hash.SHA1,
+ wantHex: "d3486ae9136e7856bc42212385ea797094475802 -\n",
+ wantBase64: "00hq6RNueFa8QiEjhep5cJRHWAI= -\n",
+ },
+ } {
+ reader.Reset(test.input)
+ require.NoError(t, operations.HashSumStream(test.ht, false, in, out))
+ assert.Equal(t, test.wantHex, out.String())
+ _, _ = reader.Seek(0, io.SeekStart)
+ out.Reset()
+ require.NoError(t, operations.HashSumStream(test.ht, true, in, out))
+ assert.Equal(t, test.wantBase64, out.String())
+ out.Reset()
+ }
+}
+
+func TestSuffixName(t *testing.T) {
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ for _, test := range []struct {
+ remote string
+ suffix string
+ keepExt bool
+ want string
+ }{
+ {"test.txt", "", false, "test.txt"},
+ {"test.txt", "", true, "test.txt"},
+ {"test.txt", "-suffix", false, "test.txt-suffix"},
+ {"test.txt", "-suffix", true, "test-suffix.txt"},
+ {"test.txt.csv", "-suffix", false, "test.txt.csv-suffix"},
+ {"test.txt.csv", "-suffix", true, "test-suffix.txt.csv"},
+ {"test", "-suffix", false, "test-suffix"},
+ {"test", "-suffix", true, "test-suffix"},
+ {"test.html", "-suffix", true, "test-suffix.html"},
+ {"test.html.txt", "-suffix", true, "test-suffix.html.txt"},
+ {"test.csv.html.txt", "-suffix", true, "test-suffix.csv.html.txt"},
+ {"test.badext.csv.html.txt", "-suffix", true, "test.badext-suffix.csv.html.txt"},
+ {"test.badext", "-suffix", true, "test-suffix.badext"},
+ } {
+ ci.Suffix = test.suffix
+ ci.SuffixKeepExtension = test.keepExt
+ got := operations.SuffixName(ctx, test.remote)
+ assert.Equal(t, test.want, got, fmt.Sprintf("%+v", test))
+ }
+}
+
+func TestCount(t *testing.T) {
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ r := fstest.NewRun(t)
+ file1 := r.WriteBoth(ctx, "potato2", "------------------------------------------------------------", t1)
+ file2 := r.WriteBoth(ctx, "empty space", "-", t2)
+ file3 := r.WriteBoth(ctx, "sub dir/potato3", "hello", t2)
+
+ r.CheckRemoteItems(t, file1, file2, file3)
+
+ // Check the MaxDepth too
+ ci.MaxDepth = 1
+
+ objects, size, sizeless, err := operations.Count(ctx, r.Fremote)
+ require.NoError(t, err)
+ assert.Equal(t, int64(2), objects)
+ assert.Equal(t, int64(61), size)
+ assert.Equal(t, int64(0), sizeless)
+}
+
+func TestDelete(t *testing.T) {
+ ctx := context.Background()
+ fi, err := filter.NewFilter(nil)
+ require.NoError(t, err)
+ fi.Opt.MaxSize = 60
+ ctx = filter.ReplaceConfig(ctx, fi)
+ r := fstest.NewRun(t)
+ file1 := r.WriteObject(ctx, "small", "1234567890", t2) // 10 bytes
+ file2 := r.WriteObject(ctx, "medium", "------------------------------------------------------------", t1) // 60 bytes
+ file3 := r.WriteObject(ctx, "large", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", t1) // 100 bytes
+ r.CheckRemoteItems(t, file1, file2, file3)
+
+ err = operations.Delete(ctx, r.Fremote)
+ require.NoError(t, err)
+ r.CheckRemoteItems(t, file3)
+}
+
+func isChunker(f fs.Fs) bool {
+ return strings.HasPrefix(f.Name(), "TestChunker")
+}
+
+func skipIfChunker(t *testing.T, f fs.Fs) {
+ if isChunker(f) {
+ t.Skip("Skipping test on chunker backend")
+ }
+}
+
+func TestMaxDelete(t *testing.T) {
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ r := fstest.NewRun(t)
+ accounting.GlobalStats().ResetCounters()
+ ci.MaxDelete = 2
+ defer r.Finalise()
+ skipIfChunker(t, r.Fremote) // chunker does copy/delete on s3
+ file1 := r.WriteObject(ctx, "small", "1234567890", t2) // 10 bytes
+ file2 := r.WriteObject(ctx, "medium", "------------------------------------------------------------", t1) // 60 bytes
+ file3 := r.WriteObject(ctx, "large", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", t1) // 100 bytes
+ r.CheckRemoteItems(t, file1, file2, file3)
+ err := operations.Delete(ctx, r.Fremote)
+
+ require.Error(t, err)
+ objects, _, _, err := operations.Count(ctx, r.Fremote)
+ require.NoError(t, err)
+ assert.Equal(t, int64(1), objects)
+}
+
+// TestMaxDeleteSizeLargeFile one of the files is larger than allowed
+func TestMaxDeleteSizeLargeFile(t *testing.T) {
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ r := fstest.NewRun(t)
+ accounting.GlobalStats().ResetCounters()
+ ci.MaxDeleteSize = 70
+ defer r.Finalise()
+ skipIfChunker(t, r.Fremote) // chunker does copy/delete on s3
+ file1 := r.WriteObject(ctx, "small", "1234567890", t2) // 10 bytes
+ file2 := r.WriteObject(ctx, "medium", "------------------------------------------------------------", t1) // 60 bytes
+ file3 := r.WriteObject(ctx, "large", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", t1) // 100 bytes
+ r.CheckRemoteItems(t, file1, file2, file3)
+
+ err := operations.Delete(ctx, r.Fremote)
+ require.Error(t, err)
+ r.CheckRemoteItems(t, file3)
+}
+
+func TestMaxDeleteSize(t *testing.T) {
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ r := fstest.NewRun(t)
+ accounting.GlobalStats().ResetCounters()
+ ci.MaxDeleteSize = 160
+ defer r.Finalise()
+ skipIfChunker(t, r.Fremote) // chunker does copy/delete on s3
+ file1 := r.WriteObject(ctx, "small", "1234567890", t2) // 10 bytes
+ file2 := r.WriteObject(ctx, "medium", "------------------------------------------------------------", t1) // 60 bytes
+ file3 := r.WriteObject(ctx, "large", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", t1) // 100 bytes
+ r.CheckRemoteItems(t, file1, file2, file3)
+
+ err := operations.Delete(ctx, r.Fremote)
+ require.Error(t, err)
+ objects, _, _, err := operations.Count(ctx, r.Fremote)
+ require.NoError(t, err)
+ assert.Equal(t, int64(1), objects) // 10 or 100 bytes
+}
+
+func TestReadFile(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+ defer r.Finalise()
+
+ contents := "A file to read the contents."
+ file := r.WriteObject(ctx, "ReadFile", contents, t1)
+ r.CheckRemoteItems(t, file)
+
+ o, err := r.Fremote.NewObject(ctx, file.Path)
+ require.NoError(t, err)
+
+ buf, err := operations.ReadFile(ctx, o)
+ require.NoError(t, err)
+ assert.Equal(t, contents, string(buf))
+}
+
+func TestRetry(t *testing.T) {
+ ctx := context.Background()
+
+ var i int
+ var err error
+ fn := func() error {
+ i--
+ if i <= 0 {
+ return nil
+ }
+ return err
+ }
+
+ i, err = 3, fmt.Errorf("Wrapped EOF is retriable: %w", io.EOF)
+ assert.Equal(t, nil, operations.Retry(ctx, nil, 5, fn))
+ assert.Equal(t, 0, i)
+
+ i, err = 10, pacer.RetryAfterError(errors.New("BANG"), 10*time.Millisecond)
+ assert.Equal(t, err, operations.Retry(ctx, nil, 5, fn))
+ assert.Equal(t, 5, i)
+
+ i, err = 10, fs.ErrorObjectNotFound
+ assert.Equal(t, fs.ErrorObjectNotFound, operations.Retry(ctx, nil, 5, fn))
+ assert.Equal(t, 9, i)
+
+}
+
+func TestCat(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+ file1 := r.WriteBoth(ctx, "file1", "ABCDEFGHIJ", t1)
+ file2 := r.WriteBoth(ctx, "file2", "012345678", t2)
+
+ r.CheckRemoteItems(t, file1, file2)
+
+ for _, test := range []struct {
+ offset int64
+ count int64
+ separator string
+ a string
+ b string
+ }{
+ {0, -1, "", "ABCDEFGHIJ", "012345678"},
+ {0, 5, "", "ABCDE", "01234"},
+ {-3, -1, "", "HIJ", "678"},
+ {1, 3, "", "BCD", "123"},
+ {0, -1, "\n", "ABCDEFGHIJ", "012345678"},
+ } {
+ var buf bytes.Buffer
+ err := operations.Cat(ctx, r.Fremote, &buf, test.offset, test.count, []byte(test.separator))
+ require.NoError(t, err)
+ res := buf.String()
+
+ if res != test.a+test.separator+test.b+test.separator && res != test.b+test.separator+test.a+test.separator {
+ t.Errorf("Incorrect output from Cat(%d,%d,%s): %q", test.offset, test.count, test.separator, res)
+ }
+ }
+}
+
+func TestPurge(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRunIndividual(t) // make new container (azureblob has delayed mkdir after rmdir)
+ r.Mkdir(ctx, r.Fremote)
+
+ // Make some files and dirs
+ r.ForceMkdir(ctx, r.Fremote)
+ file1 := r.WriteObject(ctx, "A1/B1/C1/one", "aaa", t1)
+ //..and dirs we expect to delete
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A2"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1/B2"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1/B2/C2"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1/B1/C3"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A3"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A3/B3"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A3/B3/C4"))
+ //..and one more file at the end
+ file2 := r.WriteObject(ctx, "A1/two", "bbb", t2)
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ []fstest.Item{
+ file1, file2,
+ },
+ []string{
+ "A1",
+ "A1/B1",
+ "A1/B1/C1",
+ "A2",
+ "A1/B2",
+ "A1/B2/C2",
+ "A1/B1/C3",
+ "A3",
+ "A3/B3",
+ "A3/B3/C4",
+ },
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+
+ require.NoError(t, operations.Purge(ctx, r.Fremote, "A1/B1"))
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ []fstest.Item{
+ file2,
+ },
+ []string{
+ "A1",
+ "A2",
+ "A1/B2",
+ "A1/B2/C2",
+ "A3",
+ "A3/B3",
+ "A3/B3/C4",
+ },
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+
+ require.NoError(t, operations.Purge(ctx, r.Fremote, ""))
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ []fstest.Item{},
+ []string{},
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+
+}
+
+func TestRmdirsNoLeaveRoot(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+ r.Mkdir(ctx, r.Fremote)
+
+ // Make some files and dirs we expect to keep
+ r.ForceMkdir(ctx, r.Fremote)
+ file1 := r.WriteObject(ctx, "A1/B1/C1/one", "aaa", t1)
+ //..and dirs we expect to delete
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A2"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1/B2"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1/B2/C2"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1/B1/C3"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A3"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A3/B3"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A3/B3/C4"))
+ //..and one more file at the end
+ file2 := r.WriteObject(ctx, "A1/two", "bbb", t2)
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ []fstest.Item{
+ file1, file2,
+ },
+ []string{
+ "A1",
+ "A1/B1",
+ "A1/B1/C1",
+ "A2",
+ "A1/B2",
+ "A1/B2/C2",
+ "A1/B1/C3",
+ "A3",
+ "A3/B3",
+ "A3/B3/C4",
+ },
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+
+ require.NoError(t, operations.Rmdirs(ctx, r.Fremote, "A3/B3/C4", false))
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ []fstest.Item{
+ file1, file2,
+ },
+ []string{
+ "A1",
+ "A1/B1",
+ "A1/B1/C1",
+ "A2",
+ "A1/B2",
+ "A1/B2/C2",
+ "A1/B1/C3",
+ "A3",
+ "A3/B3",
+ },
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+
+ require.NoError(t, operations.Rmdirs(ctx, r.Fremote, "", false))
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ []fstest.Item{
+ file1, file2,
+ },
+ []string{
+ "A1",
+ "A1/B1",
+ "A1/B1/C1",
+ },
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+
+ // Delete the files so we can remove everything including the root
+ for _, file := range []fstest.Item{file1, file2} {
+ o, err := r.Fremote.NewObject(ctx, file.Path)
+ require.NoError(t, err)
+ require.NoError(t, o.Remove(ctx))
+ }
+
+ require.NoError(t, operations.Rmdirs(ctx, r.Fremote, "", false))
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ []fstest.Item{},
+ []string{},
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+}
+
+func TestRmdirsLeaveRoot(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+ r.Mkdir(ctx, r.Fremote)
+
+ r.ForceMkdir(ctx, r.Fremote)
+
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1/B1"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1/B1/C1"))
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ []fstest.Item{},
+ []string{
+ "A1",
+ "A1/B1",
+ "A1/B1/C1",
+ },
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+
+ require.NoError(t, operations.Rmdirs(ctx, r.Fremote, "A1", true))
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ []fstest.Item{},
+ []string{
+ "A1",
+ },
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+}
+
+func TestRmdirsWithFilter(t *testing.T) {
+ ctx := context.Background()
+ ctx, fi := filter.AddConfig(ctx)
+ require.NoError(t, fi.AddRule("+ /A1/B1/**"))
+ require.NoError(t, fi.AddRule("- *"))
+ r := fstest.NewRun(t)
+ r.Mkdir(ctx, r.Fremote)
+
+ r.ForceMkdir(ctx, r.Fremote)
+
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1/B1"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1/B1/C1"))
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ []fstest.Item{},
+ []string{
+ "A1",
+ "A1/B1",
+ "A1/B1/C1",
+ },
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+
+ require.NoError(t, operations.Rmdirs(ctx, r.Fremote, "", false))
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ []fstest.Item{},
+ []string{
+ "A1",
+ },
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+}
+
+func TestCopyURL(t *testing.T) {
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ r := fstest.NewRun(t)
+
+ contents := "file contents\n"
+ file1 := r.WriteFile("file1", contents, t1)
+ file2 := r.WriteFile("file2", contents, t1)
+ r.Mkdir(ctx, r.Fremote)
+ r.CheckRemoteItems(t)
+
+ // check when reading from regular HTTP server
+ status := 0
+ nameHeader := false
+ headerFilename := "headerfilename.txt"
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if status != 0 {
+ http.Error(w, "an error occurred", status)
+ }
+ if nameHeader {
+ w.Header().Set("Content-Disposition", `attachment; filename="folder\`+headerFilename+`"`)
+ }
+ _, err := w.Write([]byte(contents))
+ assert.NoError(t, err)
+ })
+ ts := httptest.NewServer(handler)
+ defer ts.Close()
+
+ o, err := operations.CopyURL(ctx, r.Fremote, "file1", ts.URL, false, false, false)
+ require.NoError(t, err)
+ assert.Equal(t, int64(len(contents)), o.Size())
+
+ fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{file1}, nil, fs.ModTimeNotSupported)
+
+ // Check file clobbering
+ _, err = operations.CopyURL(ctx, r.Fremote, "file1", ts.URL, false, false, true)
+ require.Error(t, err)
+
+ // Check auto file naming
+ status = 0
+ urlFileName := "filename.txt"
+ o, err = operations.CopyURL(ctx, r.Fremote, "", ts.URL+"/"+urlFileName, true, false, false)
+ require.NoError(t, err)
+ assert.Equal(t, int64(len(contents)), o.Size())
+ assert.Equal(t, urlFileName, o.Remote())
+
+ // Check header file naming
+ nameHeader = true
+ o, err = operations.CopyURL(ctx, r.Fremote, "", ts.URL, true, true, false)
+ require.NoError(t, err)
+ assert.Equal(t, int64(len(contents)), o.Size())
+ assert.Equal(t, headerFilename, o.Remote())
+
+ // Check auto file naming when url without file name
+ _, err = operations.CopyURL(ctx, r.Fremote, "file1", ts.URL, true, false, false)
+ require.Error(t, err)
+
+ // Check header file naming without header set
+ nameHeader = false
+ _, err = operations.CopyURL(ctx, r.Fremote, "file1", ts.URL, true, true, false)
+ require.Error(t, err)
+
+ // Check an error is returned for a 404
+ status = http.StatusNotFound
+ o, err = operations.CopyURL(ctx, r.Fremote, "file1", ts.URL, false, false, false)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "Not Found")
+ assert.Nil(t, o)
+ status = 0
+
+ // check when reading from unverified HTTPS server
+ ci.InsecureSkipVerify = true
+ fshttp.ResetTransport()
+ defer fshttp.ResetTransport()
+ tss := httptest.NewTLSServer(handler)
+ defer tss.Close()
+
+ o, err = operations.CopyURL(ctx, r.Fremote, "file2", tss.URL, false, false, false)
+ require.NoError(t, err)
+ assert.Equal(t, int64(len(contents)), o.Size())
+ fstest.CheckListingWithPrecision(t, r.Fremote, []fstest.Item{file1, file2, fstest.NewItem(urlFileName, contents, t1), fstest.NewItem(headerFilename, contents, t1)}, nil, fs.ModTimeNotSupported)
+}
+
+func TestCopyURLToWriter(t *testing.T) {
+ ctx := context.Background()
+ contents := "file contents\n"
+
+ // check when reading from regular HTTP server
+ status := 0
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if status != 0 {
+ http.Error(w, "an error occurred", status)
+ return
+ }
+ _, err := w.Write([]byte(contents))
+ assert.NoError(t, err)
+ })
+ ts := httptest.NewServer(handler)
+ defer ts.Close()
+
+ // test normal fetch
+ var buf bytes.Buffer
+ err := operations.CopyURLToWriter(ctx, ts.URL, &buf)
+ require.NoError(t, err)
+ assert.Equal(t, contents, buf.String())
+
+ // test fetch with error
+ status = http.StatusNotFound
+ buf.Reset()
+ err = operations.CopyURLToWriter(ctx, ts.URL, &buf)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "Not Found")
+ assert.Equal(t, 0, len(buf.String()))
+}
+
+func TestMoveFile(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+
+ file1 := r.WriteFile("file1", "file1 contents", t1)
+ r.CheckLocalItems(t, file1)
+
+ file2 := file1
+ file2.Path = "sub/file2"
+
+ err := operations.MoveFile(ctx, r.Fremote, r.Flocal, file2.Path, file1.Path)
+ require.NoError(t, err)
+ r.CheckLocalItems(t)
+ r.CheckRemoteItems(t, file2)
+
+ r.WriteFile("file1", "file1 contents", t1)
+ r.CheckLocalItems(t, file1)
+
+ err = operations.MoveFile(ctx, r.Fremote, r.Flocal, file2.Path, file1.Path)
+ require.NoError(t, err)
+ r.CheckLocalItems(t)
+ r.CheckRemoteItems(t, file2)
+
+ err = operations.MoveFile(ctx, r.Fremote, r.Fremote, file2.Path, file2.Path)
+ require.NoError(t, err)
+ r.CheckLocalItems(t)
+ r.CheckRemoteItems(t, file2)
+}
+
+func TestMoveFileWithIgnoreExisting(t *testing.T) {
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ r := fstest.NewRun(t)
+ file1 := r.WriteFile("file1", "file1 contents", t1)
+ r.CheckLocalItems(t, file1)
+
+ ci.IgnoreExisting = true
+
+ err := operations.MoveFile(ctx, r.Fremote, r.Flocal, file1.Path, file1.Path)
+ require.NoError(t, err)
+ r.CheckLocalItems(t)
+ r.CheckRemoteItems(t, file1)
+
+ // Recreate file with updated content
+ file1b := r.WriteFile("file1", "file1 modified", t2)
+ r.CheckLocalItems(t, file1b)
+
+ // Ensure modified file did not transfer and was not deleted
+ err = operations.MoveFile(ctx, r.Fremote, r.Flocal, file1.Path, file1b.Path)
+ require.NoError(t, err)
+ r.CheckLocalItems(t, file1b)
+ r.CheckRemoteItems(t, file1)
+}
+
+func TestCaseInsensitiveMoveFile(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+ if !r.Fremote.Features().CaseInsensitive {
+ return
+ }
+
+ file1 := r.WriteFile("file1", "file1 contents", t1)
+ r.CheckLocalItems(t, file1)
+
+ file2 := file1
+ file2.Path = "sub/file2"
+
+ err := operations.MoveFile(ctx, r.Fremote, r.Flocal, file2.Path, file1.Path)
+ require.NoError(t, err)
+ r.CheckLocalItems(t)
+ r.CheckRemoteItems(t, file2)
+
+ r.WriteFile("file1", "file1 contents", t1)
+ r.CheckLocalItems(t, file1)
+
+ err = operations.MoveFile(ctx, r.Fremote, r.Flocal, file2.Path, file1.Path)
+ require.NoError(t, err)
+ r.CheckLocalItems(t)
+ r.CheckRemoteItems(t, file2)
+
+ file2Capitalized := file2
+ file2Capitalized.Path = "sub/File2"
+
+ err = operations.MoveFile(ctx, r.Fremote, r.Fremote, file2Capitalized.Path, file2.Path)
+ require.NoError(t, err)
+ r.CheckLocalItems(t)
+ r.CheckRemoteItems(t, file2Capitalized)
+}
+
+func TestCaseInsensitiveMoveFileDryRun(t *testing.T) {
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ r := fstest.NewRun(t)
+ if !r.Fremote.Features().CaseInsensitive {
+ return
+ }
+
+ file1 := r.WriteObject(ctx, "hello", "world", t1)
+ r.CheckRemoteItems(t, file1)
+
+ ci.DryRun = true
+ err := operations.MoveFile(ctx, r.Fremote, r.Fremote, "HELLO", file1.Path)
+ require.NoError(t, err)
+ r.CheckRemoteItems(t, file1)
+}
+
+func TestMoveFileBackupDir(t *testing.T) {
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ r := fstest.NewRun(t)
+ if !operations.CanServerSideMove(r.Fremote) {
+ t.Skip("Skipping test as remote does not support server-side move or copy")
+ }
+
+ ci.BackupDir = r.FremoteName + "/backup"
+
+ file1 := r.WriteFile("dst/file1", "file1 contents", t1)
+ r.CheckLocalItems(t, file1)
+
+ file1old := r.WriteObject(ctx, "dst/file1", "file1 contents old", t1)
+ r.CheckRemoteItems(t, file1old)
+
+ err := operations.MoveFile(ctx, r.Fremote, r.Flocal, file1.Path, file1.Path)
+ require.NoError(t, err)
+ r.CheckLocalItems(t)
+ file1old.Path = "backup/dst/file1"
+ r.CheckRemoteItems(t, file1old, file1)
+}
+
+// testFsInfo is for unit testing fs.Info
+type testFsInfo struct {
+ name string
+ root string
+ stringVal string
+ precision time.Duration
+ hashes hash.Set
+ features fs.Features
+}
+
+// Name of the remote (as passed into NewFs)
+func (i *testFsInfo) Name() string { return i.name }
+
+// Root of the remote (as passed into NewFs)
+func (i *testFsInfo) Root() string { return i.root }
+
+// String returns a description of the FS
+func (i *testFsInfo) String() string { return i.stringVal }
+
+// Precision of the ModTimes in this Fs
+func (i *testFsInfo) Precision() time.Duration { return i.precision }
+
+// Returns the supported hash types of the filesystem
+func (i *testFsInfo) Hashes() hash.Set { return i.hashes }
+
+// Returns the supported hash types of the filesystem
+func (i *testFsInfo) Features() *fs.Features { return &i.features }
+
+func TestSameConfig(t *testing.T) {
+ a := &testFsInfo{name: "name", root: "root"}
+ for _, test := range []struct {
+ name string
+ root string
+ expected bool
+ }{
+ {"name", "root", true},
+ {"name", "rooty", true},
+ {"namey", "root", false},
+ {"namey", "roott", false},
+ } {
+ b := &testFsInfo{name: test.name, root: test.root}
+ actual := operations.SameConfig(a, b)
+ assert.Equal(t, test.expected, actual)
+ actual = operations.SameConfig(b, a)
+ assert.Equal(t, test.expected, actual)
+ }
+}
+
+func TestSame(t *testing.T) {
+ a := &testFsInfo{name: "name", root: "root"}
+ for _, test := range []struct {
+ name string
+ root string
+ expected bool
+ }{
+ {"name", "root", true},
+ {"name", "rooty", false},
+ {"namey", "root", false},
+ {"namey", "roott", false},
+ } {
+ b := &testFsInfo{name: test.name, root: test.root}
+ actual := operations.Same(a, b)
+ assert.Equal(t, test.expected, actual)
+ actual = operations.Same(b, a)
+ assert.Equal(t, test.expected, actual)
+ }
+}
+
+// testFs is for unit testing fs.Fs
+type testFs struct {
+ testFsInfo
+}
+
+func (i *testFs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
+ return nil, nil
+}
+
+func (i *testFs) NewObject(ctx context.Context, remote string) (fs.Object, error) { return nil, nil }
+
+func (i *testFs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
+ return nil, nil
+}
+
+func (i *testFs) Mkdir(ctx context.Context, dir string) error { return nil }
+
+func (i *testFs) Rmdir(ctx context.Context, dir string) error { return nil }
+
+// copied from TestOverlapping because the behavior of OverlappingFilterCheck should be identical to Overlapping
+// when no filters are set
+func TestOverlappingFilterCheckWithoutFilter(t *testing.T) {
+ ctx := context.Background()
+ src := &testFs{testFsInfo{name: "name", root: "root"}}
+ slash := string(os.PathSeparator) // native path separator
+ for _, test := range []struct {
+ name string
+ root string
+ expected bool
+ }{
+ {"name", "root", true},
+ {"name", "/root", true},
+ {"namey", "root", false},
+ {"name", "rooty", false},
+ {"namey", "rooty", false},
+ {"name", "roo", false},
+ {"name", "root/toot", true},
+ {"name", "root/toot/", true},
+ {"name", "root" + slash + "toot", true},
+ {"name", "root" + slash + "toot" + slash, true},
+ {"name", "", true},
+ {"name", "/", true},
+ } {
+ dst := &testFs{testFsInfo{name: test.name, root: test.root}}
+ what := fmt.Sprintf("(%q,%q) vs (%q,%q)", src.name, src.root, dst.name, dst.root)
+ actual := operations.OverlappingFilterCheck(ctx, src, dst)
+ assert.Equal(t, test.expected, actual, what)
+ actual = operations.OverlappingFilterCheck(ctx, dst, src)
+ assert.Equal(t, test.expected, actual, what)
+ }
+}
+
+func TestOverlappingFilterCheckWithFilter(t *testing.T) {
+ ctx := context.Background()
+ fi, err := filter.NewFilter(nil)
+ require.NoError(t, err)
+ require.NoError(t, fi.Add(false, "/exclude/"))
+ require.NoError(t, fi.Add(false, "/Exclude2/"))
+ require.NoError(t, fi.Add(true, "*"))
+ ctx = filter.ReplaceConfig(ctx, fi)
+
+ src := &testFs{testFsInfo{name: "name", root: "root"}}
+ src.features.CaseInsensitive = true
+ slash := string(os.PathSeparator) // native path separator
+ for _, test := range []struct {
+ name string
+ root string
+ expected bool
+ }{
+ {"name", "root", true},
+ {"name", "ROOT", true}, // case insensitive is set
+ {"name", "/root", true},
+ {"name", "root/", true},
+ {"name", "root" + slash, true},
+ {"name", "root/exclude", false},
+ {"name", "root/Exclude2", false},
+ {"name", "root/include", true},
+ {"name", "root/exclude/", false},
+ {"name", "root/Exclude2/", false},
+ {"name", "root/exclude/sub", false},
+ {"name", "root/Exclude2/sub", false},
+ {"name", "/root/exclude/", false},
+ {"name", "root" + slash + "exclude", false},
+ {"name", "root" + slash + "exclude" + slash, false},
+ {"namey", "root/include", false},
+ {"namey", "root/include/", false},
+ {"namey", "root" + slash + "include", false},
+ {"namey", "root" + slash + "include" + slash, false},
+ } {
+ dst := &testFs{testFsInfo{name: test.name, root: test.root}}
+ dst.features.CaseInsensitive = true
+ what := fmt.Sprintf("(%q,%q) vs (%q,%q)", src.name, src.root, dst.name, dst.root)
+ actual := operations.OverlappingFilterCheck(ctx, dst, src)
+ assert.Equal(t, test.expected, actual, what)
+ actual = operations.OverlappingFilterCheck(ctx, src, dst)
+ assert.Equal(t, test.expected, actual, what)
+ }
+}
+
+func TestListFormat(t *testing.T) {
+ item0 := &operations.ListJSONItem{
+ Path: "a",
+ Name: "a",
+ Encrypted: "encryptedFileName",
+ Size: 1,
+ MimeType: "application/octet-stream",
+ ModTime: operations.Timestamp{
+ When: t1,
+ Format: "2006-01-02T15:04:05.000000000Z07:00"},
+ IsDir: false,
+ Hashes: map[string]string{
+ "md5": "0cc175b9c0f1b6a831c399e269772661",
+ "sha1": "86f7e437faa5a7fce15d1ddcb9eaeaea377667b8",
+ "dropbox": "bf5d3affb73efd2ec6c36ad3112dd933efed63c4e1cbffcfa88e2759c144f2d8",
+ "quickxor": "6100000000000000000000000100000000000000"},
+ ID: "fileID",
+ OrigID: "fileOrigID",
+ }
+
+ item1 := &operations.ListJSONItem{
+ Path: "subdir",
+ Name: "subdir",
+ Encrypted: "encryptedDirName",
+ Size: -1,
+ MimeType: "inode/directory",
+ ModTime: operations.Timestamp{
+ When: t2,
+ Format: "2006-01-02T15:04:05.000000000Z07:00"},
+ IsDir: true,
+ Hashes: map[string]string(nil),
+ ID: "dirID",
+ OrigID: "dirOrigID",
+ }
+
+ var list operations.ListFormat
+ list.AddPath()
+ list.SetDirSlash(false)
+ assert.Equal(t, "subdir", list.Format(item1))
+
+ list.SetDirSlash(true)
+ assert.Equal(t, "subdir/", list.Format(item1))
+
+ list.SetOutput(nil)
+ assert.Equal(t, "", list.Format(item1))
+
+ list.AppendOutput(func(item *operations.ListJSONItem) string { return "a" })
+ list.AppendOutput(func(item *operations.ListJSONItem) string { return "b" })
+ assert.Equal(t, "ab", list.Format(item1))
+ list.SetSeparator(":::")
+ assert.Equal(t, "a:::b", list.Format(item1))
+
+ list.SetOutput(nil)
+ list.AddModTime("")
+ assert.Equal(t, t1.Local().Format("2006-01-02 15:04:05"), list.Format(item0))
+
+ list.SetOutput(nil)
+ list.AddModTime("unix")
+ assert.Equal(t, fmt.Sprint(t1.Local().Unix()), list.Format(item0))
+
+ list.SetOutput(nil)
+ list.AddModTime("unixnano")
+ assert.Equal(t, fmt.Sprint(t1.Local().UnixNano()), list.Format(item0))
+
+ list.SetOutput(nil)
+ list.SetSeparator("|")
+ list.AddID()
+ list.AddOrigID()
+ assert.Equal(t, "fileID|fileOrigID", list.Format(item0))
+ assert.Equal(t, "dirID|dirOrigID", list.Format(item1))
+
+ list.SetOutput(nil)
+ list.AddMimeType()
+ assert.Contains(t, list.Format(item0), "/")
+ assert.Equal(t, "inode/directory", list.Format(item1))
+
+ list.SetOutput(nil)
+ list.AddMetadata()
+ assert.Equal(t, "{}", list.Format(item0))
+ assert.Equal(t, "{}", list.Format(item1))
+
+ list.SetOutput(nil)
+ list.AddPath()
+ list.SetAbsolute(true)
+ assert.Equal(t, "/a", list.Format(item0))
+ list.SetAbsolute(false)
+ assert.Equal(t, "a", list.Format(item0))
+
+ list.SetOutput(nil)
+ list.AddSize()
+ assert.Equal(t, "1", list.Format(item0))
+
+ list.AddPath()
+ list.AddModTime("")
+ list.SetDirSlash(true)
+ list.SetSeparator("__SEP__")
+ assert.Equal(t, "1__SEP__a__SEP__"+t1.Local().Format("2006-01-02 15:04:05"), list.Format(item0))
+ assert.Equal(t, "-1__SEP__subdir/__SEP__"+t2.Local().Format("2006-01-02 15:04:05"), list.Format(item1))
+
+ for _, test := range []struct {
+ ht hash.Type
+ want string
+ }{
+ {hash.MD5, "0cc175b9c0f1b6a831c399e269772661"},
+ {hash.SHA1, "86f7e437faa5a7fce15d1ddcb9eaeaea377667b8"},
+ } {
+ list.SetOutput(nil)
+ list.AddHash(test.ht)
+ assert.Equal(t, test.want, list.Format(item0))
+ }
+
+ list.SetOutput(nil)
+ list.SetSeparator("|")
+ list.SetCSV(true)
+ list.AddSize()
+ list.AddPath()
+ list.AddModTime("")
+ list.SetDirSlash(true)
+ assert.Equal(t, "1|a|"+t1.Local().Format("2006-01-02 15:04:05"), list.Format(item0))
+ assert.Equal(t, "-1|subdir/|"+t2.Local().Format("2006-01-02 15:04:05"), list.Format(item1))
+
+ list.SetOutput(nil)
+ list.SetSeparator("|")
+ list.AddPath()
+ list.AddEncrypted()
+ assert.Equal(t, "a|encryptedFileName", list.Format(item0))
+ assert.Equal(t, "subdir/|encryptedDirName/", list.Format(item1))
+
+}
+
+func TestDirMove(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+
+ r.Mkdir(ctx, r.Fremote)
+
+ // Make some files and dirs
+ r.ForceMkdir(ctx, r.Fremote)
+ files := []fstest.Item{
+ r.WriteObject(ctx, "A1/one", "one", t1),
+ r.WriteObject(ctx, "A1/two", "two", t2),
+ r.WriteObject(ctx, "A1/B1/three", "three", t3),
+ r.WriteObject(ctx, "A1/B1/C1/four", "four", t1),
+ r.WriteObject(ctx, "A1/B1/C2/five", "five", t2),
+ }
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1/B2"))
+ require.NoError(t, operations.Mkdir(ctx, r.Fremote, "A1/B1/C3"))
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ files,
+ []string{
+ "A1",
+ "A1/B1",
+ "A1/B2",
+ "A1/B1/C1",
+ "A1/B1/C2",
+ "A1/B1/C3",
+ },
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+
+ require.NoError(t, operations.DirMove(ctx, r.Fremote, "A1", "A2"))
+
+ for i := range files {
+ files[i].Path = strings.ReplaceAll(files[i].Path, "A1/", "A2/")
+ }
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ files,
+ []string{
+ "A2",
+ "A2/B1",
+ "A2/B2",
+ "A2/B1/C1",
+ "A2/B1/C2",
+ "A2/B1/C3",
+ },
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+
+ // Disable DirMove
+ features := r.Fremote.Features()
+ features.DirMove = nil
+
+ require.NoError(t, operations.DirMove(ctx, r.Fremote, "A2", "A3"))
+
+ for i := range files {
+ files[i].Path = strings.ReplaceAll(files[i].Path, "A2/", "A3/")
+ }
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ files,
+ []string{
+ "A3",
+ "A3/B1",
+ "A3/B2",
+ "A3/B1/C1",
+ "A3/B1/C2",
+ "A3/B1/C3",
+ },
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+
+ // Try with a DirMove method that exists but returns fs.ErrorCantDirMove (ex. combine moving across upstreams)
+ // Should fall back to manual move (copy + delete)
+
+ features.DirMove = func(ctx context.Context, src fs.Fs, srcRemote string, dstRemote string) error {
+ return fs.ErrorCantDirMove
+ }
+
+ assert.NoError(t, operations.DirMove(ctx, r.Fremote, "A3", "A4"))
+
+ for i := range files {
+ files[i].Path = strings.ReplaceAll(files[i].Path, "A3/", "A4/")
+ }
+
+ fstest.CheckListingWithPrecision(
+ t,
+ r.Fremote,
+ files,
+ []string{
+ "A4",
+ "A4/B1",
+ "A4/B2",
+ "A4/B1/C1",
+ "A4/B1/C2",
+ "A4/B1/C3",
+ },
+ fs.GetModifyWindow(ctx, r.Fremote),
+ )
+}
+
+func TestGetFsInfo(t *testing.T) {
+ r := fstest.NewRun(t)
+
+ f := r.Fremote
+ info := operations.GetFsInfo(f)
+ assert.Equal(t, f.Name(), info.Name)
+ assert.Equal(t, f.Root(), info.Root)
+ assert.Equal(t, f.String(), info.String)
+ assert.Equal(t, f.Precision(), info.Precision)
+ hashSet := hash.NewHashSet()
+ for _, hashName := range info.Hashes {
+ var ht hash.Type
+ require.NoError(t, ht.Set(hashName))
+ hashSet.Add(ht)
+ }
+ assert.Equal(t, f.Hashes(), hashSet)
+ assert.Equal(t, f.Features().Enabled(), info.Features)
+}
+
+func TestRcat(t *testing.T) {
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ check := func(t *testing.T, withChecksum, ignoreChecksum bool) {
+ ci.CheckSum, ci.IgnoreChecksum = withChecksum, ignoreChecksum
+
+ var prefix string
+ if withChecksum {
+ prefix = "with_checksum_"
+ } else {
+ prefix = "no_checksum_"
+ }
+ if ignoreChecksum {
+ prefix = "ignore_checksum_"
+ }
+
+ r := fstest.NewRun(t)
+
+ if *fstest.SizeLimit > 0 && int64(ci.StreamingUploadCutoff) > *fstest.SizeLimit {
+ savedCutoff := ci.StreamingUploadCutoff
+ ci.StreamingUploadCutoff = fs.SizeSuffix(*fstest.SizeLimit)
+ t.Logf("Adjust StreamingUploadCutoff to size limit %s (was %s)", ci.StreamingUploadCutoff, savedCutoff)
+ }
+
+ fstest.CheckListing(t, r.Fremote, []fstest.Item{})
+
+ data1 := "this is some really nice test data"
+ path1 := prefix + "small_file_from_pipe"
+
+ data2 := string(make([]byte, ci.StreamingUploadCutoff+1))
+ path2 := prefix + "big_file_from_pipe"
+
+ in := io.NopCloser(strings.NewReader(data1))
+ _, err := operations.Rcat(ctx, r.Fremote, path1, in, t1, nil)
+ require.NoError(t, err)
+
+ in = io.NopCloser(strings.NewReader(data2))
+ _, err = operations.Rcat(ctx, r.Fremote, path2, in, t2, nil)
+ require.NoError(t, err)
+
+ file1 := fstest.NewItem(path1, data1, t1)
+ file2 := fstest.NewItem(path2, data2, t2)
+ r.CheckRemoteItems(t, file1, file2)
+ }
+
+ for i := range 4 {
+ withChecksum := (i & 1) != 0
+ ignoreChecksum := (i & 2) != 0
+ t.Run(fmt.Sprintf("withChecksum=%v,ignoreChecksum=%v", withChecksum, ignoreChecksum), func(t *testing.T) {
+ check(t, withChecksum, ignoreChecksum)
+ })
+ }
+}
+
+func TestRcatMetadata(t *testing.T) {
+ r := fstest.NewRun(t)
+
+ if !r.Fremote.Features().UserMetadata {
+ t.Skip("Skipping as destination doesn't support user metadata")
+ }
+
+ test := func(disableUploadCutoff bool) {
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ ci.Metadata = true
+ data := "this is some really nice test data with metadata"
+ path := "rcat_metadata"
+
+ meta := fs.Metadata{
+ "key": "value",
+ "sausage": "potato",
+ }
+
+ if disableUploadCutoff {
+ ci.StreamingUploadCutoff = 0
+ data += " uploadCutoff=0"
+ path += "_uploadcutoff0"
+ }
+
+ fstest.CheckListing(t, r.Fremote, []fstest.Item{})
+
+ in := io.NopCloser(strings.NewReader(data))
+ _, err := operations.Rcat(ctx, r.Fremote, path, in, t1, meta)
+ require.NoError(t, err)
+
+ file := fstest.NewItem(path, data, t1)
+ r.CheckRemoteItems(t, file)
+
+ o, err := r.Fremote.NewObject(ctx, path)
+ require.NoError(t, err)
+ gotMeta, err := fs.GetMetadata(ctx, o)
+ require.NoError(t, err)
+ // Check the specific user data we set is set
+ // Likely there will be other values
+ assert.Equal(t, "value", gotMeta["key"])
+ assert.Equal(t, "potato", gotMeta["sausage"])
+
+ // Delete the test file
+ require.NoError(t, o.Remove(ctx))
+ }
+
+ t.Run("Normal", func(t *testing.T) {
+ test(false)
+ })
+ t.Run("ViaDisk", func(t *testing.T) {
+ test(true)
+ })
+}
+
+func TestRcatSize(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+
+ const body = "------------------------------------------------------------"
+ file1 := r.WriteFile("potato1", body, t1)
+ file2 := r.WriteFile("potato2", body, t2)
+ // Test with known length
+ bodyReader := io.NopCloser(strings.NewReader(body))
+ obj, err := operations.RcatSize(ctx, r.Fremote, file1.Path, bodyReader, int64(len(body)), file1.ModTime, nil)
+ require.NoError(t, err)
+ assert.Equal(t, int64(len(body)), obj.Size())
+ assert.Equal(t, file1.Path, obj.Remote())
+
+ // Test with unknown length
+ bodyReader = io.NopCloser(strings.NewReader(body)) // reset Reader
+ io.NopCloser(strings.NewReader(body))
+ obj, err = operations.RcatSize(ctx, r.Fremote, file2.Path, bodyReader, -1, file2.ModTime, nil)
+ require.NoError(t, err)
+ assert.Equal(t, int64(len(body)), obj.Size())
+ assert.Equal(t, file2.Path, obj.Remote())
+
+ // Check files exist
+ r.CheckRemoteItems(t, file1, file2)
+}
+
+func TestRcatSizeMetadata(t *testing.T) {
+ r := fstest.NewRun(t)
+
+ if !r.Fremote.Features().UserMetadata {
+ t.Skip("Skipping as destination doesn't support user metadata")
+ }
+
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ ci.Metadata = true
+
+ meta := fs.Metadata{
+ "key": "value",
+ "sausage": "potato",
+ }
+
+ const body = "------------------------------------------------------------"
+ file1 := r.WriteFile("potato1", body, t1)
+ file2 := r.WriteFile("potato2", body, t2)
+
+ // Test with known length
+ bodyReader := io.NopCloser(strings.NewReader(body))
+ obj, err := operations.RcatSize(ctx, r.Fremote, file1.Path, bodyReader, int64(len(body)), file1.ModTime, meta)
+ require.NoError(t, err)
+ assert.Equal(t, int64(len(body)), obj.Size())
+ assert.Equal(t, file1.Path, obj.Remote())
+
+ // Test with unknown length
+ bodyReader = io.NopCloser(strings.NewReader(body)) // reset Reader
+ io.NopCloser(strings.NewReader(body))
+ obj, err = operations.RcatSize(ctx, r.Fremote, file2.Path, bodyReader, -1, file2.ModTime, meta)
+ require.NoError(t, err)
+ assert.Equal(t, int64(len(body)), obj.Size())
+ assert.Equal(t, file2.Path, obj.Remote())
+
+ // Check files exist
+ r.CheckRemoteItems(t, file1, file2)
+
+ // Check metadata OK
+ for _, path := range []string{file1.Path, file2.Path} {
+ o, err := r.Fremote.NewObject(ctx, path)
+ require.NoError(t, err)
+ gotMeta, err := fs.GetMetadata(ctx, o)
+ require.NoError(t, err)
+ // Check the specific user data we set is set
+ // Likely there will be other values
+ assert.Equal(t, "value", gotMeta["key"])
+ assert.Equal(t, "potato", gotMeta["sausage"])
+ }
+}
+
+func TestTouchDir(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+
+ if r.Fremote.Precision() == fs.ModTimeNotSupported {
+ t.Skip("Skipping test as remote does not support modtime")
+ }
+
+ file1 := r.WriteBoth(ctx, "potato2", "------------------------------------------------------------", t1)
+ file2 := r.WriteBoth(ctx, "empty space", "-", t2)
+ file3 := r.WriteBoth(ctx, "sub dir/potato3", "hello", t2)
+ r.CheckRemoteItems(t, file1, file2, file3)
+
+ accounting.GlobalStats().ResetCounters()
+ timeValue := time.Date(2010, 9, 8, 7, 6, 5, 4, time.UTC)
+ err := operations.TouchDir(ctx, r.Fremote, "", timeValue, true)
+ require.NoError(t, err)
+ if accounting.Stats(ctx).GetErrors() != 0 {
+ err = accounting.Stats(ctx).GetLastError()
+ require.True(t, errors.Is(err, fs.ErrorCantSetModTime) || errors.Is(err, fs.ErrorCantSetModTimeWithoutDelete))
+ } else {
+ file1.ModTime = timeValue
+ file2.ModTime = timeValue
+ file3.ModTime = timeValue
+ r.CheckRemoteItems(t, file1, file2, file3)
+ }
+}
+
+var testMetadata = fs.Metadata{
+ // System metadata supported by all backends
+ "mtime": t1.Format(time.RFC3339Nano),
+ // User metadata
+ "potato": "jersey",
+}
+
+func TestMkdirMetadata(t *testing.T) {
+ const name = "dir with metadata"
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ ci.Metadata = true
+ r := fstest.NewRun(t)
+ features := r.Fremote.Features()
+ if features.MkdirMetadata == nil {
+ t.Skip("Skipping test as remote does not support MkdirMetadata")
+ }
+
+ newDst, err := operations.MkdirMetadata(ctx, r.Fremote, name, testMetadata)
+ require.NoError(t, err)
+ require.NotNil(t, newDst)
+
+ require.True(t, features.ReadDirMetadata, "Expecting ReadDirMetadata to be supported if MkdirMetadata is supported")
+
+ // Check the returned directory and one read from the listing
+ fstest.CheckEntryMetadata(ctx, t, r.Fremote, newDst, testMetadata)
+ fstest.CheckEntryMetadata(ctx, t, r.Fremote, fstest.NewDirectory(ctx, t, r.Fremote, name), testMetadata)
+}
+
+func TestMkdirModTime(t *testing.T) {
+ const name = "directory with modtime"
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+ if r.Fremote.Features().DirSetModTime == nil && r.Fremote.Features().MkdirMetadata == nil {
+ t.Skip("Skipping test as remote does not support DirSetModTime or MkdirMetadata")
+ }
+ newDst, err := operations.MkdirModTime(ctx, r.Fremote, name, t2)
+ require.NoError(t, err)
+
+ // Check the returned directory and one read from the listing
+ // newDst may be nil here depending on how the modtime was set
+ if newDst != nil {
+ fstest.CheckDirModTime(ctx, t, r.Fremote, newDst, t2)
+ }
+ fstest.CheckDirModTime(ctx, t, r.Fremote, fstest.NewDirectory(ctx, t, r.Fremote, name), t2)
+}
+
+func TestCopyDirMetadata(t *testing.T) {
+ const nameNonExistent = "non existent directory"
+ const nameExistent = "existing directory"
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ ci.Metadata = true
+ r := fstest.NewRun(t)
+ if !r.Fremote.Features().WriteDirMetadata && r.Fremote.Features().MkdirMetadata == nil {
+ t.Skip("Skipping test as remote does not support WriteDirMetadata or MkdirMetadata")
+ }
+
+ // Create a source local directory with metadata
+ newSrc, err := operations.MkdirMetadata(ctx, r.Flocal, "dir with metadata to be copied", testMetadata)
+ require.NoError(t, err)
+ require.NotNil(t, newSrc)
+
+ // First try with the directory not existing
+ newDst, err := operations.CopyDirMetadata(ctx, r.Fremote, nil, nameNonExistent, newSrc)
+ require.NoError(t, err)
+ require.NotNil(t, newDst)
+
+ // Check the returned directory and one read from the listing
+ fstest.CheckEntryMetadata(ctx, t, r.Fremote, newDst, testMetadata)
+ fstest.CheckEntryMetadata(ctx, t, r.Fremote, fstest.NewDirectory(ctx, t, r.Fremote, nameNonExistent), testMetadata)
+
+ // Then try with the directory existing
+ require.NoError(t, r.Fremote.Rmdir(ctx, nameNonExistent))
+ require.NoError(t, r.Fremote.Mkdir(ctx, nameExistent))
+ existingDir := fstest.NewDirectory(ctx, t, r.Fremote, nameExistent)
+
+ newDst, err = operations.CopyDirMetadata(ctx, r.Fremote, existingDir, "SHOULD BE IGNORED", newSrc)
+ require.NoError(t, err)
+ require.NotNil(t, newDst)
+
+ // Check the returned directory and one read from the listing
+ fstest.CheckEntryMetadata(ctx, t, r.Fremote, newDst, testMetadata)
+ fstest.CheckEntryMetadata(ctx, t, r.Fremote, fstest.NewDirectory(ctx, t, r.Fremote, nameExistent), testMetadata)
+}
+
+func TestSetDirModTime(t *testing.T) {
+ const name = "set modtime on existing directory"
+ ctx, ci := fs.AddConfig(context.Background())
+ r := fstest.NewRun(t)
+ if r.Fremote.Features().DirSetModTime == nil && !r.Fremote.Features().WriteDirSetModTime {
+ t.Skip("Skipping test as remote does not support DirSetModTime or WriteDirSetModTime")
+ }
+
+ // Check that we obey --no-update-dir-modtime - this should return nil, nil
+ ci.NoUpdateDirModTime = true
+ newDst, err := operations.SetDirModTime(ctx, r.Fremote, nil, "set modtime on non existent directory", t2)
+ require.NoError(t, err)
+ require.Nil(t, newDst)
+ ci.NoUpdateDirModTime = false
+
+ // First try with the directory not existing - should return an error
+ newDst, err = operations.SetDirModTime(ctx, r.Fremote, nil, "set modtime on non existent directory", t2)
+ require.Error(t, err)
+ require.Nil(t, newDst)
+
+ // Then try with the directory existing
+ require.NoError(t, r.Fremote.Mkdir(ctx, name))
+ existingDir := fstest.NewDirectory(ctx, t, r.Fremote, name)
+
+ newDst, err = operations.SetDirModTime(ctx, r.Fremote, existingDir, "SHOULD BE IGNORED", t2)
+ require.NoError(t, err)
+ require.NotNil(t, newDst)
+
+ // Check the returned directory and one read from the listing
+ // The modtime will only be correct on newDst if it had a SetModTime method
+ if _, ok := newDst.(fs.SetModTimer); ok {
+ fstest.CheckDirModTime(ctx, t, r.Fremote, newDst, t2)
+ }
+ fstest.CheckDirModTime(ctx, t, r.Fremote, fstest.NewDirectory(ctx, t, r.Fremote, name), t2)
+
+ // Now wrap the directory to make the SetModTime method return fs.ErrorNotImplemented and check that it falls back correctly
+ wrappedDir := fs.NewDirWrapper(existingDir.Remote(), fs.NewDir(existingDir.Remote(), existingDir.ModTime(ctx)))
+ newDst, err = operations.SetDirModTime(ctx, r.Fremote, wrappedDir, "SHOULD BE IGNORED", t1)
+ require.NoError(t, err)
+ require.NotNil(t, newDst)
+ fstest.CheckDirModTime(ctx, t, r.Fremote, fstest.NewDirectory(ctx, t, r.Fremote, name), t1)
+}
+
+func TestDirsEqual(t *testing.T) {
+ ctx := context.Background()
+ ctx, ci := fs.AddConfig(ctx)
+ ci.Metadata = true
+ r := fstest.NewRun(t)
+ if !r.Fremote.Features().WriteDirMetadata && r.Fremote.Features().MkdirMetadata == nil {
+ t.Skip("Skipping test as remote does not support WriteDirMetadata or MkdirMetadata")
+ }
+
+ opt := operations.DirsEqualOpt{
+ ModifyWindow: fs.GetModifyWindow(ctx, r.Flocal, r.Fremote),
+ SetDirModtime: true,
+ SetDirMetadata: true,
+ }
+
+ // Create a source local directory with metadata
+ src, err := operations.MkdirMetadata(ctx, r.Flocal, "dir with metadata to be copied", testMetadata)
+ require.NoError(t, err)
+ require.NotNil(t, src)
+
+ // try with nil dst -- should be false
+ equal := operations.DirsEqual(ctx, src, nil, opt)
+ assert.False(t, equal)
+
+ // make a dest with an equal modtime
+ dst, err := operations.MkdirModTime(ctx, r.Fremote, "dst", src.ModTime(ctx))
+ require.NoError(t, err)
+
+ // try with equal modtimes -- should be true
+ equal = operations.DirsEqual(ctx, src, dst, opt)
+ assert.True(t, equal)
+
+ // try with unequal modtimes -- should be false
+ dst, err = operations.SetDirModTime(ctx, r.Fremote, dst, "", t2)
+ require.NoError(t, err)
+ equal = operations.DirsEqual(ctx, src, dst, opt)
+ assert.False(t, equal)
+
+ // try with unequal modtimes that are within modify window -- should be true
+ halfWindow := opt.ModifyWindow / 2
+ dst, err = operations.SetDirModTime(ctx, r.Fremote, dst, "", src.ModTime(ctx).Add(halfWindow))
+ require.NoError(t, err)
+ equal = operations.DirsEqual(ctx, src, dst, opt)
+ assert.True(t, equal)
+
+ // test ignoretimes -- should be false
+ ci.IgnoreTimes = true
+ equal = operations.DirsEqual(ctx, src, dst, opt)
+ assert.False(t, equal)
+
+ // test immutable -- should be true
+ ci.IgnoreTimes = false
+ ci.Immutable = true
+ dst, err = operations.SetDirModTime(ctx, r.Fremote, dst, "", t3)
+ require.NoError(t, err)
+ equal = operations.DirsEqual(ctx, src, dst, opt)
+ assert.True(t, equal)
+
+ // test dst newer than src with --update -- should be true
+ ci.Immutable = false
+ ci.UpdateOlder = true
+ equal = operations.DirsEqual(ctx, src, dst, opt)
+ assert.True(t, equal)
+
+ // test no SetDirModtime or SetDirMetadata -- should be true
+ ci.UpdateOlder = false
+ opt.SetDirMetadata, opt.SetDirModtime = false, false
+ equal = operations.DirsEqual(ctx, src, dst, opt)
+ assert.True(t, equal)
+}
+
+func TestRemoveExisting(t *testing.T) {
+ ctx := context.Background()
+ r := fstest.NewRun(t)
+ if r.Fremote.Features().Move == nil {
+ t.Skip("Skipping as remote can't Move")
+ }
+
+ file1 := r.WriteObject(ctx, "sub dir/test remove existing", "hello world", t1)
+ file2 := r.WriteObject(ctx, "sub dir/test remove existing with long name 123456789012345678901234567890123456789012345678901234567890123456789", "hello long name world", t1)
+
+ r.CheckRemoteItems(t, file1, file2)
+
+ var returnedError error
+
+ // Check not found first
+ cleanup, err := operations.RemoveExisting(ctx, r.Fremote, "not found", "TEST")
+ assert.Equal(t, err, nil)
+ r.CheckRemoteItems(t, file1, file2)
+ cleanup(&returnedError)
+ r.CheckRemoteItems(t, file1, file2)
+
+ // Remove file1
+ cleanup, err = operations.RemoveExisting(ctx, r.Fremote, file1.Path, "TEST")
+ assert.Equal(t, err, nil)
+ //r.CheckRemoteItems(t, file1, file2)
+
+ // Check file1 with temporary name exists
+ var buf bytes.Buffer
+ err = operations.List(ctx, r.Fremote, &buf)
+ require.NoError(t, err)
+ res := buf.String()
+ assert.NotContains(t, res, " 11 "+file1.Path+"\n")
+ assert.Contains(t, res, " 11 "+file1.Path+".")
+ assert.Contains(t, res, " 21 "+file2.Path+"\n")
+
+ cleanup(&returnedError)
+ r.CheckRemoteItems(t, file2)
+
+ // Remove file2 with an error
+ cleanup, err = operations.RemoveExisting(ctx, r.Fremote, file2.Path, "TEST")
+ assert.Equal(t, err, nil)
+
+ // Check file2 with truncated temporary name exists
+ buf.Reset()
+ err = operations.List(ctx, r.Fremote, &buf)
+ require.NoError(t, err)
+ res = buf.String()
+ assert.NotContains(t, res, " 21 "+file2.Path+"\n")
+ assert.NotContains(t, res, " 21 "+file2.Path+".")
+ assert.Contains(t, res, " 21 "+file2.Path[:100])
+
+ returnedError = errors.New("BOOM")
+ cleanup(&returnedError)
+ r.CheckRemoteItems(t, file2)
+
+ // Remove file2
+ cleanup, err = operations.RemoveExisting(ctx, r.Fremote, file2.Path, "TEST")
+ assert.Equal(t, err, nil)
+
+ // Check file2 with truncated temporary name exists
+ buf.Reset()
+ err = operations.List(ctx, r.Fremote, &buf)
+ require.NoError(t, err)
+ res = buf.String()
+ assert.NotContains(t, res, " 21 "+file2.Path+"\n")
+ assert.NotContains(t, res, " 21 "+file2.Path+".")
+ assert.Contains(t, res, " 21 "+file2.Path[:100])
+
+ returnedError = nil
+ cleanup(&returnedError)
+ r.CheckRemoteItems(t)
+}