github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/operations/copy_test.go (about)

     1  package operations_test
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"errors"
     7  	"fmt"
     8  	"os"
     9  	"path"
    10  	"sort"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/rclone/rclone/fs"
    15  	"github.com/rclone/rclone/fs/accounting"
    16  	"github.com/rclone/rclone/fs/operations"
    17  	"github.com/rclone/rclone/fstest"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  func TestTruncateString(t *testing.T) {
    23  	for _, test := range []struct {
    24  		in   string
    25  		n    int
    26  		want string
    27  	}{
    28  		{
    29  			in:   "",
    30  			n:    0,
    31  			want: "",
    32  		}, {
    33  			in:   "Hello World",
    34  			n:    5,
    35  			want: "Hello",
    36  		}, {
    37  			in:   "ááááá",
    38  			n:    5,
    39  			want: "áá",
    40  		}, {
    41  			in:   "ááááá\xFF\xFF",
    42  			n:    5,
    43  			want: "áá\xc3",
    44  		}, {
    45  			in:   "世世世世世",
    46  			n:    7,
    47  			want: "世世",
    48  		}, {
    49  			in:   "🙂🙂🙂🙂🙂",
    50  			n:    16,
    51  			want: "🙂🙂🙂🙂",
    52  		}, {
    53  			in:   "🙂🙂🙂🙂🙂",
    54  			n:    15,
    55  			want: "🙂🙂🙂",
    56  		}, {
    57  			in:   "🙂🙂🙂🙂🙂",
    58  			n:    14,
    59  			want: "🙂🙂🙂",
    60  		}, {
    61  			in:   "🙂🙂🙂🙂🙂",
    62  			n:    13,
    63  			want: "🙂🙂🙂",
    64  		}, {
    65  			in:   "🙂🙂🙂🙂🙂",
    66  			n:    12,
    67  			want: "🙂🙂🙂",
    68  		}, {
    69  			in:   "🙂🙂🙂🙂🙂",
    70  			n:    11,
    71  			want: "🙂🙂",
    72  		}, {
    73  			in:   "𝓝𝓸𝓫𝓸𝓭𝔂 𝓲𝓼 𝓱𝓸𝓶𝓮 ᴬ ⱽⁱˢⁱᵗ ᶠʳᵒᵐ ᵗʰᵉ ⱽⁱˢⁱᵒⁿᵃʳʸ",
    74  			n:    100,
    75  			want: "𝓝𝓸𝓫𝓸𝓭𝔂 𝓲𝓼 𝓱𝓸𝓶𝓮 ᴬ ⱽⁱˢⁱᵗ ᶠʳᵒᵐ ᵗʰᵉ ⱽⁱˢ",
    76  		}, {
    77  			in:   "a𝓝𝓸𝓫𝓸𝓭𝔂 𝓲𝓼 𝓱𝓸𝓶𝓮 ᴬ ⱽⁱˢⁱᵗ ᶠʳᵒᵐ ᵗʰᵉ ⱽⁱˢⁱᵒⁿᵃʳʸ",
    78  			n:    100,
    79  			want: "a𝓝𝓸𝓫𝓸𝓭𝔂 𝓲𝓼 𝓱𝓸𝓶𝓮 ᴬ ⱽⁱˢⁱᵗ ᶠʳᵒᵐ ᵗʰᵉ ⱽⁱˢ",
    80  		}, {
    81  			in:   "aa𝓝𝓸𝓫𝓸𝓭𝔂 𝓲𝓼 𝓱𝓸𝓶𝓮 ᴬ ⱽⁱˢⁱᵗ ᶠʳᵒᵐ ᵗʰᵉ ⱽⁱˢⁱᵒⁿᵃʳʸ",
    82  			n:    100,
    83  			want: "aa𝓝𝓸𝓫𝓸𝓭𝔂 𝓲𝓼 𝓱𝓸𝓶𝓮 ᴬ ⱽⁱˢⁱᵗ ᶠʳᵒᵐ ᵗʰᵉ ⱽⁱ",
    84  		}, {
    85  			in:   "aaa𝓝𝓸𝓫𝓸𝓭𝔂 𝓲𝓼 𝓱𝓸𝓶𝓮 ᴬ ⱽⁱˢⁱᵗ ᶠʳᵒᵐ ᵗʰᵉ ⱽⁱˢⁱᵒⁿᵃʳʸ",
    86  			n:    100,
    87  			want: "aaa𝓝𝓸𝓫𝓸𝓭𝔂 𝓲𝓼 𝓱𝓸𝓶𝓮 ᴬ ⱽⁱˢⁱᵗ ᶠʳᵒᵐ ᵗʰᵉ ⱽⁱ",
    88  		}, {
    89  			in:   "aaaa𝓝𝓸𝓫𝓸𝓭𝔂 𝓲𝓼 𝓱𝓸𝓶𝓮 ᴬ ⱽⁱˢⁱᵗ ᶠʳᵒᵐ ᵗʰᵉ ⱽⁱˢⁱᵒⁿᵃʳʸ",
    90  			n:    100,
    91  			want: "aaaa𝓝𝓸𝓫𝓸𝓭𝔂 𝓲𝓼 𝓱𝓸𝓶𝓮 ᴬ ⱽⁱˢⁱᵗ ᶠʳᵒᵐ ᵗʰᵉ ⱽ",
    92  		},
    93  	} {
    94  		got := operations.TruncateString(test.in, test.n)
    95  		assert.Equal(t, test.want, got, fmt.Sprintf("In %q", test.in))
    96  		assert.LessOrEqual(t, len(got), test.n)
    97  		assert.GreaterOrEqual(t, len(got), test.n-3)
    98  	}
    99  }
   100  
   101  func TestCopyFile(t *testing.T) {
   102  	ctx := context.Background()
   103  	r := fstest.NewRun(t)
   104  
   105  	file1 := r.WriteFile("file1", "file1 contents", t1)
   106  	r.CheckLocalItems(t, file1)
   107  
   108  	file2 := file1
   109  	file2.Path = "sub/file2"
   110  
   111  	err := operations.CopyFile(ctx, r.Fremote, r.Flocal, file2.Path, file1.Path)
   112  	require.NoError(t, err)
   113  	r.CheckLocalItems(t, file1)
   114  	r.CheckRemoteItems(t, file2)
   115  
   116  	err = operations.CopyFile(ctx, r.Fremote, r.Flocal, file2.Path, file1.Path)
   117  	require.NoError(t, err)
   118  	r.CheckLocalItems(t, file1)
   119  	r.CheckRemoteItems(t, file2)
   120  
   121  	err = operations.CopyFile(ctx, r.Fremote, r.Fremote, file2.Path, file2.Path)
   122  	require.NoError(t, err)
   123  	r.CheckLocalItems(t, file1)
   124  	r.CheckRemoteItems(t, file2)
   125  }
   126  
   127  // Find the longest file name for writing to local
   128  func maxLengthFileName(t *testing.T, r *fstest.Run) string {
   129  	require.NoError(t, r.Flocal.Mkdir(context.Background(), "")) // create the root
   130  	const maxLen = 16 * 1024
   131  	name := strings.Repeat("A", maxLen)
   132  	i := sort.Search(len(name), func(i int) (fail bool) {
   133  		filePath := path.Join(r.LocalName, name[:i])
   134  		err := os.WriteFile(filePath, []byte{0}, 0777)
   135  		if err != nil {
   136  			return true
   137  		}
   138  		err = os.Remove(filePath)
   139  		if err != nil {
   140  			t.Logf("Failed to remove test file: %v", err)
   141  		}
   142  		return false
   143  	})
   144  	return name[:i-1]
   145  }
   146  
   147  // Check we can copy a file of maximum name length
   148  func TestCopyLongFile(t *testing.T) {
   149  	ctx := context.Background()
   150  	r := fstest.NewRun(t)
   151  	if !r.Fremote.Features().IsLocal {
   152  		t.Skip("Test only runs on local")
   153  	}
   154  
   155  	// Find the maximum length of file we can write
   156  	name := maxLengthFileName(t, r)
   157  	t.Logf("Max length of file name is %d", len(name))
   158  	file1 := r.WriteFile(name, "file1 contents", t1)
   159  	r.CheckLocalItems(t, file1)
   160  
   161  	err := operations.CopyFile(ctx, r.Fremote, r.Flocal, file1.Path, file1.Path)
   162  	require.NoError(t, err)
   163  	r.CheckLocalItems(t, file1)
   164  	r.CheckRemoteItems(t, file1)
   165  }
   166  
   167  func TestCopyFileBackupDir(t *testing.T) {
   168  	ctx := context.Background()
   169  	ctx, ci := fs.AddConfig(ctx)
   170  	r := fstest.NewRun(t)
   171  	if !operations.CanServerSideMove(r.Fremote) {
   172  		t.Skip("Skipping test as remote does not support server-side move or copy")
   173  	}
   174  
   175  	ci.BackupDir = r.FremoteName + "/backup"
   176  
   177  	file1 := r.WriteFile("dst/file1", "file1 contents", t1)
   178  	r.CheckLocalItems(t, file1)
   179  
   180  	file1old := r.WriteObject(ctx, "dst/file1", "file1 contents old", t1)
   181  	r.CheckRemoteItems(t, file1old)
   182  
   183  	err := operations.CopyFile(ctx, r.Fremote, r.Flocal, file1.Path, file1.Path)
   184  	require.NoError(t, err)
   185  	r.CheckLocalItems(t, file1)
   186  	file1old.Path = "backup/dst/file1"
   187  	r.CheckRemoteItems(t, file1old, file1)
   188  }
   189  
   190  // Test with CompareDest set
   191  func TestCopyFileCompareDest(t *testing.T) {
   192  	ctx := context.Background()
   193  	ctx, ci := fs.AddConfig(ctx)
   194  	r := fstest.NewRun(t)
   195  
   196  	ci.CompareDest = []string{r.FremoteName + "/CompareDest"}
   197  	fdst, err := fs.NewFs(ctx, r.FremoteName+"/dst")
   198  	require.NoError(t, err)
   199  
   200  	// check empty dest, empty compare
   201  	file1 := r.WriteFile("one", "one", t1)
   202  	r.CheckLocalItems(t, file1)
   203  
   204  	err = operations.CopyFile(ctx, fdst, r.Flocal, file1.Path, file1.Path)
   205  	require.NoError(t, err)
   206  
   207  	file1dst := file1
   208  	file1dst.Path = "dst/one"
   209  
   210  	r.CheckRemoteItems(t, file1dst)
   211  
   212  	// check old dest, empty compare
   213  	file1b := r.WriteFile("one", "onet2", t2)
   214  	r.CheckRemoteItems(t, file1dst)
   215  	r.CheckLocalItems(t, file1b)
   216  
   217  	err = operations.CopyFile(ctx, fdst, r.Flocal, file1b.Path, file1b.Path)
   218  	require.NoError(t, err)
   219  
   220  	file1bdst := file1b
   221  	file1bdst.Path = "dst/one"
   222  
   223  	r.CheckRemoteItems(t, file1bdst)
   224  
   225  	// check old dest, new compare
   226  	file3 := r.WriteObject(ctx, "dst/one", "one", t1)
   227  	file2 := r.WriteObject(ctx, "CompareDest/one", "onet2", t2)
   228  	file1c := r.WriteFile("one", "onet2", t2)
   229  	r.CheckRemoteItems(t, file2, file3)
   230  	r.CheckLocalItems(t, file1c)
   231  
   232  	err = operations.CopyFile(ctx, fdst, r.Flocal, file1c.Path, file1c.Path)
   233  	require.NoError(t, err)
   234  
   235  	r.CheckRemoteItems(t, file2, file3)
   236  
   237  	// check empty dest, new compare
   238  	file4 := r.WriteObject(ctx, "CompareDest/two", "two", t2)
   239  	file5 := r.WriteFile("two", "two", t2)
   240  	r.CheckRemoteItems(t, file2, file3, file4)
   241  	r.CheckLocalItems(t, file1c, file5)
   242  
   243  	err = operations.CopyFile(ctx, fdst, r.Flocal, file5.Path, file5.Path)
   244  	require.NoError(t, err)
   245  
   246  	r.CheckRemoteItems(t, file2, file3, file4)
   247  
   248  	// check new dest, new compare
   249  	err = operations.CopyFile(ctx, fdst, r.Flocal, file5.Path, file5.Path)
   250  	require.NoError(t, err)
   251  
   252  	r.CheckRemoteItems(t, file2, file3, file4)
   253  
   254  	// check empty dest, old compare
   255  	file5b := r.WriteFile("two", "twot3", t3)
   256  	r.CheckRemoteItems(t, file2, file3, file4)
   257  	r.CheckLocalItems(t, file1c, file5b)
   258  
   259  	err = operations.CopyFile(ctx, fdst, r.Flocal, file5b.Path, file5b.Path)
   260  	require.NoError(t, err)
   261  
   262  	file5bdst := file5b
   263  	file5bdst.Path = "dst/two"
   264  
   265  	r.CheckRemoteItems(t, file2, file3, file4, file5bdst)
   266  }
   267  
   268  // Test with CopyDest set
   269  func TestCopyFileCopyDest(t *testing.T) {
   270  	ctx := context.Background()
   271  	ctx, ci := fs.AddConfig(ctx)
   272  	r := fstest.NewRun(t)
   273  
   274  	if r.Fremote.Features().Copy == nil {
   275  		t.Skip("Skipping test as remote does not support server-side copy")
   276  	}
   277  
   278  	ci.CopyDest = []string{r.FremoteName + "/CopyDest"}
   279  
   280  	fdst, err := fs.NewFs(ctx, r.FremoteName+"/dst")
   281  	require.NoError(t, err)
   282  
   283  	// check empty dest, empty copy
   284  	file1 := r.WriteFile("one", "one", t1)
   285  	r.CheckLocalItems(t, file1)
   286  
   287  	err = operations.CopyFile(ctx, fdst, r.Flocal, file1.Path, file1.Path)
   288  	require.NoError(t, err)
   289  
   290  	file1dst := file1
   291  	file1dst.Path = "dst/one"
   292  
   293  	r.CheckRemoteItems(t, file1dst)
   294  
   295  	// check old dest, empty copy
   296  	file1b := r.WriteFile("one", "onet2", t2)
   297  	r.CheckRemoteItems(t, file1dst)
   298  	r.CheckLocalItems(t, file1b)
   299  
   300  	err = operations.CopyFile(ctx, fdst, r.Flocal, file1b.Path, file1b.Path)
   301  	require.NoError(t, err)
   302  
   303  	file1bdst := file1b
   304  	file1bdst.Path = "dst/one"
   305  
   306  	r.CheckRemoteItems(t, file1bdst)
   307  
   308  	// check old dest, new copy, backup-dir
   309  
   310  	ci.BackupDir = r.FremoteName + "/BackupDir"
   311  
   312  	file3 := r.WriteObject(ctx, "dst/one", "one", t1)
   313  	file2 := r.WriteObject(ctx, "CopyDest/one", "onet2", t2)
   314  	file1c := r.WriteFile("one", "onet2", t2)
   315  	r.CheckRemoteItems(t, file2, file3)
   316  	r.CheckLocalItems(t, file1c)
   317  
   318  	err = operations.CopyFile(ctx, fdst, r.Flocal, file1c.Path, file1c.Path)
   319  	require.NoError(t, err)
   320  
   321  	file2dst := file2
   322  	file2dst.Path = "dst/one"
   323  	file3.Path = "BackupDir/one"
   324  
   325  	r.CheckRemoteItems(t, file2, file2dst, file3)
   326  	ci.BackupDir = ""
   327  
   328  	// check empty dest, new copy
   329  	file4 := r.WriteObject(ctx, "CopyDest/two", "two", t2)
   330  	file5 := r.WriteFile("two", "two", t2)
   331  	r.CheckRemoteItems(t, file2, file2dst, file3, file4)
   332  	r.CheckLocalItems(t, file1c, file5)
   333  
   334  	err = operations.CopyFile(ctx, fdst, r.Flocal, file5.Path, file5.Path)
   335  	require.NoError(t, err)
   336  
   337  	file4dst := file4
   338  	file4dst.Path = "dst/two"
   339  
   340  	r.CheckRemoteItems(t, file2, file2dst, file3, file4, file4dst)
   341  
   342  	// check new dest, new copy
   343  	err = operations.CopyFile(ctx, fdst, r.Flocal, file5.Path, file5.Path)
   344  	require.NoError(t, err)
   345  
   346  	r.CheckRemoteItems(t, file2, file2dst, file3, file4, file4dst)
   347  
   348  	// check empty dest, old copy
   349  	file6 := r.WriteObject(ctx, "CopyDest/three", "three", t2)
   350  	file7 := r.WriteFile("three", "threet3", t3)
   351  	r.CheckRemoteItems(t, file2, file2dst, file3, file4, file4dst, file6)
   352  	r.CheckLocalItems(t, file1c, file5, file7)
   353  
   354  	err = operations.CopyFile(ctx, fdst, r.Flocal, file7.Path, file7.Path)
   355  	require.NoError(t, err)
   356  
   357  	file7dst := file7
   358  	file7dst.Path = "dst/three"
   359  
   360  	r.CheckRemoteItems(t, file2, file2dst, file3, file4, file4dst, file6, file7dst)
   361  }
   362  
   363  func TestCopyInplace(t *testing.T) {
   364  	ctx := context.Background()
   365  	ctx, ci := fs.AddConfig(ctx)
   366  	r := fstest.NewRun(t)
   367  
   368  	if !r.Fremote.Features().PartialUploads {
   369  		t.Skip("Partial uploads not supported")
   370  	}
   371  
   372  	ci.Inplace = true
   373  
   374  	file1 := r.WriteFile("file1", "file1 contents", t1)
   375  	r.CheckLocalItems(t, file1)
   376  
   377  	file2 := file1
   378  	file2.Path = "sub/file2"
   379  
   380  	err := operations.CopyFile(ctx, r.Fremote, r.Flocal, file2.Path, file1.Path)
   381  	require.NoError(t, err)
   382  	r.CheckLocalItems(t, file1)
   383  	r.CheckRemoteItems(t, file2)
   384  
   385  	err = operations.CopyFile(ctx, r.Fremote, r.Flocal, file2.Path, file1.Path)
   386  	require.NoError(t, err)
   387  	r.CheckLocalItems(t, file1)
   388  	r.CheckRemoteItems(t, file2)
   389  
   390  	err = operations.CopyFile(ctx, r.Fremote, r.Fremote, file2.Path, file2.Path)
   391  	require.NoError(t, err)
   392  	r.CheckLocalItems(t, file1)
   393  	r.CheckRemoteItems(t, file2)
   394  }
   395  
   396  func TestCopyLongFileName(t *testing.T) {
   397  	ctx := context.Background()
   398  	ctx, ci := fs.AddConfig(ctx)
   399  	r := fstest.NewRun(t)
   400  
   401  	if !r.Fremote.Features().PartialUploads {
   402  		t.Skip("Partial uploads not supported")
   403  	}
   404  
   405  	ci.Inplace = false // the default
   406  
   407  	file1 := r.WriteFile("file1", "file1 contents", t1)
   408  	r.CheckLocalItems(t, file1)
   409  
   410  	file2 := file1
   411  	file2.Path = "sub/" + strings.Repeat("file2", 30)
   412  
   413  	err := operations.CopyFile(ctx, r.Fremote, r.Flocal, file2.Path, file1.Path)
   414  	require.NoError(t, err)
   415  	r.CheckLocalItems(t, file1)
   416  	r.CheckRemoteItems(t, file2)
   417  
   418  	err = operations.CopyFile(ctx, r.Fremote, r.Flocal, file2.Path, file1.Path)
   419  	require.NoError(t, err)
   420  	r.CheckLocalItems(t, file1)
   421  	r.CheckRemoteItems(t, file2)
   422  
   423  	err = operations.CopyFile(ctx, r.Fremote, r.Fremote, file2.Path, file2.Path)
   424  	require.NoError(t, err)
   425  	r.CheckLocalItems(t, file1)
   426  	r.CheckRemoteItems(t, file2)
   427  }
   428  
   429  func TestCopyFileMaxTransfer(t *testing.T) {
   430  	ctx := context.Background()
   431  	ctx, ci := fs.AddConfig(ctx)
   432  	r := fstest.NewRun(t)
   433  	defer accounting.Stats(ctx).ResetCounters()
   434  
   435  	const sizeCutoff = 2048
   436  
   437  	// Make random incompressible data
   438  	randomData := make([]byte, sizeCutoff)
   439  	_, err := rand.Read(randomData)
   440  	require.NoError(t, err)
   441  	randomString := string(randomData)
   442  
   443  	file1 := r.WriteFile("TestCopyFileMaxTransfer/file1", "file1 contents", t1)
   444  	file2 := r.WriteFile("TestCopyFileMaxTransfer/file2", "file2 contents"+randomString, t2)
   445  	file3 := r.WriteFile("TestCopyFileMaxTransfer/file3", "file3 contents"+randomString, t2)
   446  	file4 := r.WriteFile("TestCopyFileMaxTransfer/file4", "file4 contents"+randomString, t2)
   447  
   448  	// Cutoff mode: Hard
   449  	ci.MaxTransfer = sizeCutoff
   450  	ci.CutoffMode = fs.CutoffModeHard
   451  
   452  	// file1: Show a small file gets transferred OK
   453  	accounting.Stats(ctx).ResetCounters()
   454  	err = operations.CopyFile(ctx, r.Fremote, r.Flocal, file1.Path, file1.Path)
   455  	require.NoError(t, err)
   456  	r.CheckLocalItems(t, file1, file2, file3, file4)
   457  	r.CheckRemoteItems(t, file1)
   458  
   459  	// file2: show a large file does not get transferred
   460  	accounting.Stats(ctx).ResetCounters()
   461  	err = operations.CopyFile(ctx, r.Fremote, r.Flocal, file2.Path, file2.Path)
   462  	require.NotNil(t, err, "Did not get expected max transfer limit error")
   463  	if !errors.Is(err, accounting.ErrorMaxTransferLimitReachedFatal) {
   464  		t.Log("Expecting error to contain accounting.ErrorMaxTransferLimitReachedFatal")
   465  		// Sometimes the backends or their SDKs don't pass the
   466  		// error through properly, so check that it at least
   467  		// has the text we expect in.
   468  		assert.Contains(t, err.Error(), "max transfer limit reached")
   469  	}
   470  	r.CheckLocalItems(t, file1, file2, file3, file4)
   471  	r.CheckRemoteItems(t, file1)
   472  
   473  	// Cutoff mode: Cautious
   474  	ci.CutoffMode = fs.CutoffModeCautious
   475  
   476  	// file3: show a large file does not get transferred
   477  	accounting.Stats(ctx).ResetCounters()
   478  	err = operations.CopyFile(ctx, r.Fremote, r.Flocal, file3.Path, file3.Path)
   479  	require.NotNil(t, err)
   480  	assert.True(t, errors.Is(err, accounting.ErrorMaxTransferLimitReachedGraceful))
   481  	r.CheckLocalItems(t, file1, file2, file3, file4)
   482  	r.CheckRemoteItems(t, file1)
   483  
   484  	if isChunker(r.Fremote) {
   485  		t.Log("skipping remainder of test for chunker as it involves multiple transfers")
   486  		return
   487  	}
   488  
   489  	// Cutoff mode: Soft
   490  	ci.CutoffMode = fs.CutoffModeSoft
   491  
   492  	// file4: show a large file does get transferred this time
   493  	accounting.Stats(ctx).ResetCounters()
   494  	err = operations.CopyFile(ctx, r.Fremote, r.Flocal, file4.Path, file4.Path)
   495  	require.NoError(t, err)
   496  	r.CheckLocalItems(t, file1, file2, file3, file4)
   497  	r.CheckRemoteItems(t, file1, file4)
   498  }