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  }