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 }