github.com/artpar/rclone@v1.67.3/backend/local/local_internal_test.go (about) 1 package local 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "os" 9 "path" 10 "path/filepath" 11 "runtime" 12 "sort" 13 "testing" 14 "time" 15 16 "github.com/artpar/rclone/fs" 17 "github.com/artpar/rclone/fs/accounting" 18 "github.com/artpar/rclone/fs/config/configmap" 19 "github.com/artpar/rclone/fs/filter" 20 "github.com/artpar/rclone/fs/hash" 21 "github.com/artpar/rclone/fs/object" 22 "github.com/artpar/rclone/fs/operations" 23 "github.com/artpar/rclone/fstest" 24 "github.com/artpar/rclone/lib/file" 25 "github.com/artpar/rclone/lib/readers" 26 "github.com/stretchr/testify/assert" 27 "github.com/stretchr/testify/require" 28 ) 29 30 // TestMain drives the tests 31 func TestMain(m *testing.M) { 32 fstest.TestMain(m) 33 } 34 35 // Test copy with source file that's updating 36 func TestUpdatingCheck(t *testing.T) { 37 r := fstest.NewRun(t) 38 filePath := "sub dir/local test" 39 r.WriteFile(filePath, "content", time.Now()) 40 41 fd, err := file.Open(path.Join(r.LocalName, filePath)) 42 if err != nil { 43 t.Fatalf("failed opening file %q: %v", filePath, err) 44 } 45 defer func() { 46 require.NoError(t, fd.Close()) 47 }() 48 49 fi, err := fd.Stat() 50 require.NoError(t, err) 51 o := &Object{size: fi.Size(), modTime: fi.ModTime(), fs: &Fs{}} 52 wrappedFd := readers.NewLimitedReadCloser(fd, -1) 53 hash, err := hash.NewMultiHasherTypes(hash.Supported()) 54 require.NoError(t, err) 55 in := localOpenFile{ 56 o: o, 57 in: wrappedFd, 58 hash: hash, 59 fd: fd, 60 } 61 62 buf := make([]byte, 1) 63 _, err = in.Read(buf) 64 require.NoError(t, err) 65 66 r.WriteFile(filePath, "content updated", time.Now()) 67 _, err = in.Read(buf) 68 require.Errorf(t, err, "can't copy - source file is being updated") 69 70 // turn the checking off and try again 71 in.o.fs.opt.NoCheckUpdated = true 72 73 r.WriteFile(filePath, "content updated", time.Now()) 74 _, err = in.Read(buf) 75 require.NoError(t, err) 76 77 } 78 79 // Test corrupted on transfer 80 // should error due to size/hash mismatch 81 func TestVerifyCopy(t *testing.T) { 82 t.Skip("FIXME this test is unreliable") 83 r := fstest.NewRun(t) 84 filePath := "sub dir/local test" 85 r.WriteFile(filePath, "some content", time.Now()) 86 src, err := r.Flocal.NewObject(context.Background(), filePath) 87 require.NoError(t, err) 88 src.(*Object).fs.opt.NoCheckUpdated = true 89 90 for i := 0; i < 100; i++ { 91 go r.WriteFile(src.Remote(), fmt.Sprintf("some new content %d", i), src.ModTime(context.Background())) 92 } 93 _, err = operations.Copy(context.Background(), r.Fremote, nil, filePath+"2", src) 94 assert.Error(t, err) 95 } 96 97 func TestSymlink(t *testing.T) { 98 ctx := context.Background() 99 r := fstest.NewRun(t) 100 f := r.Flocal.(*Fs) 101 dir := f.root 102 103 // Write a file 104 modTime1 := fstest.Time("2001-02-03T04:05:10.123123123Z") 105 file1 := r.WriteFile("file.txt", "hello", modTime1) 106 107 // Write a symlink 108 modTime2 := fstest.Time("2002-02-03T04:05:10.123123123Z") 109 symlinkPath := filepath.Join(dir, "symlink.txt") 110 require.NoError(t, os.Symlink("file.txt", symlinkPath)) 111 require.NoError(t, lChtimes(symlinkPath, modTime2, modTime2)) 112 113 // Object viewed as symlink 114 file2 := fstest.NewItem("symlink.txt"+linkSuffix, "file.txt", modTime2) 115 116 // Object viewed as destination 117 file2d := fstest.NewItem("symlink.txt", "hello", modTime1) 118 119 // Check with no symlink flags 120 r.CheckLocalItems(t, file1) 121 r.CheckRemoteItems(t) 122 123 // Set fs into "-L" mode 124 f.opt.FollowSymlinks = true 125 f.opt.TranslateSymlinks = false 126 f.lstat = os.Stat 127 128 r.CheckLocalItems(t, file1, file2d) 129 r.CheckRemoteItems(t) 130 131 // Set fs into "-l" mode 132 f.opt.FollowSymlinks = false 133 f.opt.TranslateSymlinks = true 134 f.lstat = os.Lstat 135 136 fstest.CheckListingWithPrecision(t, r.Flocal, []fstest.Item{file1, file2}, nil, fs.ModTimeNotSupported) 137 if haveLChtimes { 138 r.CheckLocalItems(t, file1, file2) 139 } 140 141 // Create a symlink 142 modTime3 := fstest.Time("2002-03-03T04:05:10.123123123Z") 143 file3 := r.WriteObjectTo(ctx, r.Flocal, "symlink2.txt"+linkSuffix, "file.txt", modTime3, false) 144 fstest.CheckListingWithPrecision(t, r.Flocal, []fstest.Item{file1, file2, file3}, nil, fs.ModTimeNotSupported) 145 if haveLChtimes { 146 r.CheckLocalItems(t, file1, file2, file3) 147 } 148 149 // Check it got the correct contents 150 symlinkPath = filepath.Join(dir, "symlink2.txt") 151 fi, err := os.Lstat(symlinkPath) 152 require.NoError(t, err) 153 assert.False(t, fi.Mode().IsRegular()) 154 linkText, err := os.Readlink(symlinkPath) 155 require.NoError(t, err) 156 assert.Equal(t, "file.txt", linkText) 157 158 // Check that NewObject gets the correct object 159 o, err := r.Flocal.NewObject(ctx, "symlink2.txt"+linkSuffix) 160 require.NoError(t, err) 161 assert.Equal(t, "symlink2.txt"+linkSuffix, o.Remote()) 162 assert.Equal(t, int64(8), o.Size()) 163 164 // Check that NewObject doesn't see the non suffixed version 165 _, err = r.Flocal.NewObject(ctx, "symlink2.txt") 166 require.Equal(t, fs.ErrorObjectNotFound, err) 167 168 // Check that NewFs works with the suffixed version and --links 169 f2, err := NewFs(ctx, "local", filepath.Join(dir, "symlink2.txt"+linkSuffix), configmap.Simple{ 170 "links": "true", 171 }) 172 require.Equal(t, fs.ErrorIsFile, err) 173 require.Equal(t, dir, f2.(*Fs).root) 174 175 // Check that NewFs doesn't see the non suffixed version with --links 176 f2, err = NewFs(ctx, "local", filepath.Join(dir, "symlink2.txt"), configmap.Simple{ 177 "links": "true", 178 }) 179 require.Equal(t, errLinksNeedsSuffix, err) 180 require.Nil(t, f2) 181 182 // Check reading the object 183 in, err := o.Open(ctx) 184 require.NoError(t, err) 185 contents, err := io.ReadAll(in) 186 require.NoError(t, err) 187 require.Equal(t, "file.txt", string(contents)) 188 require.NoError(t, in.Close()) 189 190 // Check reading the object with range 191 in, err = o.Open(ctx, &fs.RangeOption{Start: 2, End: 5}) 192 require.NoError(t, err) 193 contents, err = io.ReadAll(in) 194 require.NoError(t, err) 195 require.Equal(t, "file.txt"[2:5+1], string(contents)) 196 require.NoError(t, in.Close()) 197 } 198 199 func TestSymlinkError(t *testing.T) { 200 m := configmap.Simple{ 201 "links": "true", 202 "copy_links": "true", 203 } 204 _, err := NewFs(context.Background(), "local", "/", m) 205 assert.Equal(t, errLinksAndCopyLinks, err) 206 } 207 208 // Test hashes on updating an object 209 func TestHashOnUpdate(t *testing.T) { 210 ctx := context.Background() 211 r := fstest.NewRun(t) 212 const filePath = "file.txt" 213 when := time.Now() 214 r.WriteFile(filePath, "content", when) 215 f := r.Flocal.(*Fs) 216 217 // Get the object 218 o, err := f.NewObject(ctx, filePath) 219 require.NoError(t, err) 220 221 // Test the hash is as we expect 222 md5, err := o.Hash(ctx, hash.MD5) 223 require.NoError(t, err) 224 assert.Equal(t, "9a0364b9e99bb480dd25e1f0284c8555", md5) 225 226 // Reupload it with different contents but same size and timestamp 227 var b = bytes.NewBufferString("CONTENT") 228 src := object.NewStaticObjectInfo(filePath, when, int64(b.Len()), true, nil, f) 229 err = o.Update(ctx, b, src) 230 require.NoError(t, err) 231 232 // Check the hash is as expected 233 md5, err = o.Hash(ctx, hash.MD5) 234 require.NoError(t, err) 235 assert.Equal(t, "45685e95985e20822fb2538a522a5ccf", md5) 236 } 237 238 // Test hashes on deleting an object 239 func TestHashOnDelete(t *testing.T) { 240 ctx := context.Background() 241 r := fstest.NewRun(t) 242 const filePath = "file.txt" 243 when := time.Now() 244 r.WriteFile(filePath, "content", when) 245 f := r.Flocal.(*Fs) 246 247 // Get the object 248 o, err := f.NewObject(ctx, filePath) 249 require.NoError(t, err) 250 251 // Test the hash is as we expect 252 md5, err := o.Hash(ctx, hash.MD5) 253 require.NoError(t, err) 254 assert.Equal(t, "9a0364b9e99bb480dd25e1f0284c8555", md5) 255 256 // Delete the object 257 require.NoError(t, o.Remove(ctx)) 258 259 // Test the hash cache is empty 260 require.Nil(t, o.(*Object).hashes) 261 262 // Test the hash returns an error 263 _, err = o.Hash(ctx, hash.MD5) 264 require.Error(t, err) 265 } 266 267 func TestMetadata(t *testing.T) { 268 ctx := context.Background() 269 r := fstest.NewRun(t) 270 const filePath = "metafile.txt" 271 when := time.Now() 272 const dayLength = len("2001-01-01") 273 whenRFC := when.Format(time.RFC3339Nano) 274 r.WriteFile(filePath, "metadata file contents", when) 275 f := r.Flocal.(*Fs) 276 277 // Get the object 278 obj, err := f.NewObject(ctx, filePath) 279 require.NoError(t, err) 280 o := obj.(*Object) 281 282 features := f.Features() 283 284 var hasXID, hasAtime, hasBtime bool 285 switch runtime.GOOS { 286 case "darwin", "freebsd", "netbsd", "linux": 287 hasXID, hasAtime, hasBtime = true, true, true 288 case "openbsd", "solaris": 289 hasXID, hasAtime = true, true 290 case "windows": 291 hasAtime, hasBtime = true, true 292 case "plan9", "js": 293 // nada 294 default: 295 t.Errorf("No test cases for OS %q", runtime.GOOS) 296 } 297 298 assert.True(t, features.ReadMetadata) 299 assert.True(t, features.WriteMetadata) 300 assert.Equal(t, xattrSupported, features.UserMetadata) 301 302 t.Run("Xattr", func(t *testing.T) { 303 if !xattrSupported { 304 t.Skip() 305 } 306 m, err := o.getXattr() 307 require.NoError(t, err) 308 assert.Nil(t, m) 309 310 inM := fs.Metadata{ 311 "potato": "chips", 312 "cabbage": "soup", 313 } 314 err = o.setXattr(inM) 315 require.NoError(t, err) 316 317 m, err = o.getXattr() 318 require.NoError(t, err) 319 assert.NotNil(t, m) 320 assert.Equal(t, inM, m) 321 }) 322 323 checkTime := func(m fs.Metadata, key string, when time.Time) { 324 mt, ok := o.parseMetadataTime(m, key) 325 assert.True(t, ok) 326 dt := mt.Sub(when) 327 precision := time.Second 328 assert.True(t, dt >= -precision && dt <= precision, fmt.Sprintf("%s: dt %v outside +/- precision %v", key, dt, precision)) 329 } 330 331 checkInt := func(m fs.Metadata, key string, base int) int { 332 value, ok := o.parseMetadataInt(m, key, base) 333 assert.True(t, ok) 334 return value 335 } 336 t.Run("Read", func(t *testing.T) { 337 m, err := o.Metadata(ctx) 338 require.NoError(t, err) 339 assert.NotNil(t, m) 340 341 // All OSes have these 342 checkInt(m, "mode", 8) 343 checkTime(m, "mtime", when) 344 345 assert.Equal(t, len(whenRFC), len(m["mtime"])) 346 assert.Equal(t, whenRFC[:dayLength], m["mtime"][:dayLength]) 347 348 if hasAtime { 349 checkTime(m, "atime", when) 350 } 351 if hasBtime { 352 checkTime(m, "btime", when) 353 } 354 if hasXID { 355 checkInt(m, "uid", 10) 356 checkInt(m, "gid", 10) 357 } 358 }) 359 360 t.Run("Write", func(t *testing.T) { 361 newAtimeString := "2011-12-13T14:15:16.999999999Z" 362 newAtime := fstest.Time(newAtimeString) 363 newMtimeString := "2011-12-12T14:15:16.999999999Z" 364 newMtime := fstest.Time(newMtimeString) 365 newBtimeString := "2011-12-11T14:15:16.999999999Z" 366 newBtime := fstest.Time(newBtimeString) 367 newM := fs.Metadata{ 368 "mtime": newMtimeString, 369 "atime": newAtimeString, 370 "btime": newBtimeString, 371 // Can't test uid, gid without being root 372 "mode": "0767", 373 "potato": "wedges", 374 } 375 err := o.writeMetadata(newM) 376 require.NoError(t, err) 377 378 m, err := o.Metadata(ctx) 379 require.NoError(t, err) 380 assert.NotNil(t, m) 381 382 mode := checkInt(m, "mode", 8) 383 if runtime.GOOS != "windows" { 384 assert.Equal(t, 0767, mode&0777, fmt.Sprintf("mode wrong - expecting 0767 got 0%o", mode&0777)) 385 } 386 387 checkTime(m, "mtime", newMtime) 388 if hasAtime { 389 checkTime(m, "atime", newAtime) 390 } 391 if haveSetBTime { 392 checkTime(m, "btime", newBtime) 393 } 394 if xattrSupported { 395 assert.Equal(t, "wedges", m["potato"]) 396 } 397 }) 398 399 } 400 401 func TestFilter(t *testing.T) { 402 ctx := context.Background() 403 r := fstest.NewRun(t) 404 when := time.Now() 405 r.WriteFile("included", "included file", when) 406 r.WriteFile("excluded", "excluded file", when) 407 f := r.Flocal.(*Fs) 408 409 // Check set up for filtering 410 assert.True(t, f.Features().FilterAware) 411 412 // Add a filter 413 ctx, fi := filter.AddConfig(ctx) 414 require.NoError(t, fi.AddRule("+ included")) 415 require.NoError(t, fi.AddRule("- *")) 416 417 // Check listing without use filter flag 418 entries, err := f.List(ctx, "") 419 require.NoError(t, err) 420 sort.Sort(entries) 421 require.Equal(t, "[excluded included]", fmt.Sprint(entries)) 422 423 // Add user filter flag 424 ctx = filter.SetUseFilter(ctx, true) 425 426 // Check listing with use filter flag 427 entries, err = f.List(ctx, "") 428 require.NoError(t, err) 429 sort.Sort(entries) 430 require.Equal(t, "[included]", fmt.Sprint(entries)) 431 } 432 433 func testFilterSymlink(t *testing.T, copyLinks bool) { 434 ctx := context.Background() 435 r := fstest.NewRun(t) 436 defer r.Finalise() 437 when := time.Now() 438 f := r.Flocal.(*Fs) 439 440 // Create a file, a directory, a symlink to a file, a symlink to a directory and a dangling symlink 441 r.WriteFile("included.file", "included file", when) 442 r.WriteFile("included.dir/included.sub.file", "included sub file", when) 443 require.NoError(t, os.Symlink("included.file", filepath.Join(r.LocalName, "included.file.link"))) 444 require.NoError(t, os.Symlink("included.dir", filepath.Join(r.LocalName, "included.dir.link"))) 445 require.NoError(t, os.Symlink("dangling", filepath.Join(r.LocalName, "dangling.link"))) 446 447 defer func() { 448 // Reset -L/-l mode 449 f.opt.FollowSymlinks = false 450 f.opt.TranslateSymlinks = false 451 f.lstat = os.Lstat 452 }() 453 if copyLinks { 454 // Set fs into "-L" mode 455 f.opt.FollowSymlinks = true 456 f.opt.TranslateSymlinks = false 457 f.lstat = os.Stat 458 } else { 459 // Set fs into "-l" mode 460 f.opt.FollowSymlinks = false 461 f.opt.TranslateSymlinks = true 462 f.lstat = os.Lstat 463 } 464 465 // Check set up for filtering 466 assert.True(t, f.Features().FilterAware) 467 468 // Reset global error count 469 accounting.Stats(ctx).ResetErrors() 470 assert.Equal(t, int64(0), accounting.Stats(ctx).GetErrors(), "global errors found") 471 472 // Add a filter 473 ctx, fi := filter.AddConfig(ctx) 474 require.NoError(t, fi.AddRule("+ included.file")) 475 require.NoError(t, fi.AddRule("+ included.dir/**")) 476 if copyLinks { 477 require.NoError(t, fi.AddRule("+ included.file.link")) 478 require.NoError(t, fi.AddRule("+ included.dir.link/**")) 479 } else { 480 require.NoError(t, fi.AddRule("+ included.file.link.rclonelink")) 481 require.NoError(t, fi.AddRule("+ included.dir.link.rclonelink")) 482 } 483 require.NoError(t, fi.AddRule("- *")) 484 485 // Check listing without use filter flag 486 entries, err := f.List(ctx, "") 487 require.NoError(t, err) 488 489 if copyLinks { 490 // Check 1 global errors one for each dangling symlink 491 assert.Equal(t, int64(1), accounting.Stats(ctx).GetErrors(), "global errors found") 492 } else { 493 // Check 0 global errors as dangling symlink copied properly 494 assert.Equal(t, int64(0), accounting.Stats(ctx).GetErrors(), "global errors found") 495 } 496 accounting.Stats(ctx).ResetErrors() 497 498 sort.Sort(entries) 499 if copyLinks { 500 require.Equal(t, "[included.dir included.dir.link included.file included.file.link]", fmt.Sprint(entries)) 501 } else { 502 require.Equal(t, "[dangling.link.rclonelink included.dir included.dir.link.rclonelink included.file included.file.link.rclonelink]", fmt.Sprint(entries)) 503 } 504 505 // Add user filter flag 506 ctx = filter.SetUseFilter(ctx, true) 507 508 // Check listing with use filter flag 509 entries, err = f.List(ctx, "") 510 require.NoError(t, err) 511 assert.Equal(t, int64(0), accounting.Stats(ctx).GetErrors(), "global errors found") 512 513 sort.Sort(entries) 514 if copyLinks { 515 require.Equal(t, "[included.dir included.dir.link included.file included.file.link]", fmt.Sprint(entries)) 516 } else { 517 require.Equal(t, "[included.dir included.dir.link.rclonelink included.file included.file.link.rclonelink]", fmt.Sprint(entries)) 518 } 519 520 // Check listing through a symlink still works 521 entries, err = f.List(ctx, "included.dir") 522 require.NoError(t, err) 523 assert.Equal(t, int64(0), accounting.Stats(ctx).GetErrors(), "global errors found") 524 525 sort.Sort(entries) 526 require.Equal(t, "[included.dir/included.sub.file]", fmt.Sprint(entries)) 527 } 528 529 func TestFilterSymlinkCopyLinks(t *testing.T) { 530 testFilterSymlink(t, true) 531 } 532 533 func TestFilterSymlinkLinks(t *testing.T) { 534 testFilterSymlink(t, false) 535 } 536 537 func TestCopySymlink(t *testing.T) { 538 ctx := context.Background() 539 r := fstest.NewRun(t) 540 defer r.Finalise() 541 when := time.Now() 542 f := r.Flocal.(*Fs) 543 544 // Create a file and a symlink to it 545 r.WriteFile("src/file.txt", "hello world", when) 546 require.NoError(t, os.Symlink("file.txt", filepath.Join(r.LocalName, "src", "link.txt"))) 547 defer func() { 548 // Reset -L/-l mode 549 f.opt.FollowSymlinks = false 550 f.opt.TranslateSymlinks = false 551 f.lstat = os.Lstat 552 }() 553 554 // Set fs into "-l/--links" mode 555 f.opt.FollowSymlinks = false 556 f.opt.TranslateSymlinks = true 557 f.lstat = os.Lstat 558 559 // Create dst 560 require.NoError(t, f.Mkdir(ctx, "dst")) 561 562 // Do copy from src into dst 563 src, err := f.NewObject(ctx, "src/link.txt.rclonelink") 564 require.NoError(t, err) 565 require.NotNil(t, src) 566 dst, err := operations.Copy(ctx, f, nil, "dst/link.txt.rclonelink", src) 567 require.NoError(t, err) 568 require.NotNil(t, dst) 569 570 // Test that we made a symlink and it has the right contents 571 dstPath := filepath.Join(r.LocalName, "dst", "link.txt") 572 linkContents, err := os.Readlink(dstPath) 573 require.NoError(t, err) 574 assert.Equal(t, "file.txt", linkContents) 575 }