123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- package filesystem
- import (
- "context"
- "path/filepath"
- "reflect"
- "sort"
- "testing"
- "time"
- "github.com/stretchr/testify/require"
- "github.com/kopia/kopia/internal/blobtesting"
- "github.com/kopia/kopia/internal/gather"
- "github.com/kopia/kopia/internal/providervalidation"
- "github.com/kopia/kopia/internal/testlogging"
- "github.com/kopia/kopia/internal/testutil"
- "github.com/kopia/kopia/repo/blob"
- "github.com/kopia/kopia/repo/blob/sharded"
- )
- func TestFileStorage(t *testing.T) {
- t.Parallel()
- ctx := testlogging.Context(t)
- // Test various shard configurations.
- for _, shardSpec := range [][]int{
- {0},
- {1},
- {3, 3},
- {2},
- {1, 1},
- {1, 2},
- {2, 2, 2},
- } {
- path := testutil.TempDirectory(t)
- newctx, cancel := context.WithCancel(ctx)
- // use context that gets canceled after opening storage to ensure it's not used beyond New().
- r, err := New(newctx, &Options{
- Path: path,
- Options: sharded.Options{
- DirectoryShards: shardSpec,
- },
- }, true)
- cancel()
- require.NoError(t, err)
- require.NotNil(t, r)
- blobtesting.VerifyStorage(ctx, t, r, blob.PutOptions{})
- blobtesting.AssertConnectionInfoRoundTrips(ctx, t, r)
- require.NoError(t, r.Close(ctx))
- }
- }
- func TestFileStorageValidate(t *testing.T) {
- t.Parallel()
- testutil.ProviderTest(t)
- ctx := testlogging.Context(t)
- path := testutil.TempDirectory(t)
- r, err := New(ctx, &Options{
- Path: path,
- Options: sharded.Options{},
- }, true)
- require.NoError(t, err)
- require.NotNil(t, r)
- blobtesting.VerifyStorage(ctx, t, r, blob.PutOptions{})
- blobtesting.AssertConnectionInfoRoundTrips(ctx, t, r)
- require.NoError(t, providervalidation.ValidateProvider(ctx, r, blobtesting.TestValidationOptions))
- require.NoError(t, r.Close(ctx))
- }
- const (
- t1 = "392ee1bc299db9f235e046a62625afb84902"
- t2 = "2a7ff4f29eddbcd4c18fa9e73fec20bbb71f"
- t3 = "0dae5918f83e6a24c8b3e274ca1026e43f24"
- )
- func TestFileStorageTouch(t *testing.T) {
- t.Parallel()
- ctx := testlogging.Context(t)
- path := testutil.TempDirectory(t)
- r, err := New(ctx, &Options{
- Path: path,
- }, true)
- if r == nil || err != nil {
- t.Errorf("unexpected result: %v %v", r, err)
- }
- fs := testutil.EnsureType[*fsStorage](t, r)
- assertNoError(t, fs.PutBlob(ctx, t1, gather.FromSlice([]byte{1}), blob.PutOptions{}))
- time.Sleep(2 * time.Second) // sleep a bit to accommodate Apple filesystems with low timestamp resolution
- assertNoError(t, fs.PutBlob(ctx, t2, gather.FromSlice([]byte{1}), blob.PutOptions{}))
- time.Sleep(2 * time.Second)
- assertNoError(t, fs.PutBlob(ctx, t3, gather.FromSlice([]byte{1}), blob.PutOptions{}))
- time.Sleep(2 * time.Second) // sleep a bit to accommodate Apple filesystems with low timestamp resolution
- verifyBlobTimestampOrder(t, fs, t1, t2, t3)
- _, err = fs.TouchBlob(ctx, t2, 1*time.Hour)
- assertNoError(t, err) // has no effect, all timestamps are very new
- verifyBlobTimestampOrder(t, fs, t1, t2, t3)
- time.Sleep(2 * time.Second) // sleep a bit to accommodate Apple filesystems with low timestamp resolution
- _, err = fs.TouchBlob(ctx, t1, 0)
- assertNoError(t, err) // moves t1 to the top of the pile
- verifyBlobTimestampOrder(t, fs, t2, t3, t1)
- time.Sleep(2 * time.Second) // sleep a bit to accommodate Apple filesystems with low timestamp resolution
- _, err = fs.TouchBlob(ctx, t2, 0)
- assertNoError(t, err) // moves t2 to the top of the pile
- verifyBlobTimestampOrder(t, fs, t3, t1, t2)
- time.Sleep(2 * time.Second) // sleep a bit to accommodate Apple filesystems with low timestamp resolution
- _, err = fs.TouchBlob(ctx, t1, 0)
- assertNoError(t, err) // moves t1 to the top of the pile
- verifyBlobTimestampOrder(t, fs, t3, t2, t1)
- }
- func TestFileStorageConcurrency(t *testing.T) {
- t.Parallel()
- testutil.ProviderTest(t)
- path := testutil.TempDirectory(t)
- ctx := testlogging.Context(t)
- st, err := New(ctx, &Options{
- Path: path,
- }, true)
- require.NoError(t, err)
- blobtesting.VerifyConcurrentAccess(t, st, blobtesting.ConcurrentAccessOptions{
- NumBlobs: 16,
- Getters: 8,
- Putters: 8,
- Deleters: 8,
- Listers: 8,
- Iterations: 500,
- RangeGetPercentage: 10,
- NonExistentListPrefixPercentage: 10,
- })
- }
- func TestFilesystemStorageDirectoryShards(t *testing.T) {
- t.Parallel()
- ctx := testlogging.Context(t)
- dataDir := testutil.TempDirectory(t)
- st, err := New(ctx, &Options{
- Path: dataDir,
- Options: sharded.Options{
- DirectoryShards: []int{5, 2},
- },
- }, true)
- require.NoError(t, err)
- defer st.Close(ctx)
- require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{}))
- require.FileExists(t, filepath.Join(dataDir, "someb", "lo", "b1234567812345678.f"))
- }
- func TestFileStorage_GetBlob_RetriesOnReadError(t *testing.T) {
- t.Parallel()
- ctx := testlogging.Context(t)
- dataDir := testutil.TempDirectory(t)
- osi := newMockOS()
- osi.readFileRemainingErrors.Store(1)
- st, err := New(ctx, &Options{
- Path: dataDir,
- Options: sharded.Options{
- DirectoryShards: []int{5, 2},
- },
- osInterfaceOverride: osi,
- }, true)
- require.NoError(t, err)
- defer st.Close(ctx)
- require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{}))
- var buf gather.WriteBuffer
- defer buf.Close()
- require.NoError(t, st.GetBlob(ctx, "someblob1234567812345678", 1, 2, &buf))
- require.Equal(t, []byte{2, 3}, buf.ToByteSlice())
- }
- func TestFileStorage_GetMetadata_RetriesOnError(t *testing.T) {
- t.Parallel()
- ctx := testlogging.Context(t)
- dataDir := testutil.TempDirectory(t)
- osi := newMockOS()
- osi.statRemainingErrors.Store(1)
- st, err := New(ctx, &Options{
- Path: dataDir,
- Options: sharded.Options{
- DirectoryShards: []int{5, 2},
- },
- }, true)
- require.NoError(t, err)
- defer st.Close(ctx)
- require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{}))
- asFsImpl(t, st).osi = osi
- _, err = st.GetMetadata(ctx, "someblob1234567812345678")
- require.NoError(t, err)
- }
- func asFsImpl(t *testing.T, st blob.Storage) *fsImpl {
- t.Helper()
- fsSt := testutil.EnsureType[*fsStorage](t, st)
- return testutil.EnsureType[*fsImpl](t, fsSt.Impl)
- }
- func TestFileStorage_PutBlob_RetriesOnErrors(t *testing.T) {
- t.Parallel()
- ctx := testlogging.Context(t)
- dataDir := testutil.TempDirectory(t)
- osi := newMockOS()
- osi.createNewFileRemainingErrors.Store(3)
- osi.mkdirAllRemainingErrors.Store(2)
- osi.writeFileRemainingErrors.Store(3)
- osi.writeFileCloseRemainingErrors.Store(2)
- osi.renameRemainingErrors.Store(1)
- osi.removeRemainingRetriableErrors.Store(3)
- osi.chownRemainingErrors.Store(3)
- osi.chtimesRemainingErrors.Store(3)
- fileUID := 3
- fileGID := 4
- st, err := New(ctx, &Options{
- Path: dataDir,
- FileUID: &fileUID,
- FileGID: &fileGID,
- Options: sharded.Options{
- DirectoryShards: []int{5, 2},
- },
- }, true)
- require.NoError(t, err)
- asFsImpl(t, st).osi = osi
- defer st.Close(ctx)
- require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{}))
- var buf gather.WriteBuffer
- defer buf.Close()
- require.NoError(t, st.GetBlob(ctx, "someblob1234567812345678", 1, 2, &buf))
- require.Equal(t, []byte{2, 3}, buf.ToByteSlice())
- var mt time.Time
- require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{
- GetModTime: &mt,
- }))
- require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{
- SetModTime: time.Date(2020, 1, 1, 12, 0, 0, 0, time.UTC),
- }))
- }
- func TestFileStorage_DeleteBlob_ErrorHandling(t *testing.T) {
- t.Parallel()
- ctx := testlogging.Context(t)
- dataDir := testutil.TempDirectory(t)
- osi := newMockOS()
- osi.removeRemainingNonRetriableErrors.Store(1)
- st, err := New(ctx, &Options{
- Path: dataDir,
- Options: sharded.Options{
- DirectoryShards: []int{5, 2},
- },
- }, true)
- require.NoError(t, err)
- asFsImpl(t, st).osi = osi
- defer st.Close(ctx)
- require.ErrorIs(t, st.DeleteBlob(ctx, "someblob1234567812345678"), errNonRetriable)
- }
- func TestFileStorage_New_MkdirAllFailureIsIgnored(t *testing.T) {
- t.Parallel()
- ctx := testlogging.Context(t)
- dataDir := testutil.TempDirectory(t)
- osi := newMockOS()
- osi.mkdirAllRemainingErrors.Store(1)
- st, err := New(ctx, &Options{
- Path: dataDir,
- Options: sharded.Options{
- DirectoryShards: []int{5, 2},
- },
- osInterfaceOverride: osi,
- }, true)
- require.NoError(t, err)
- st.Close(ctx)
- }
- func TestFileStorage_New_ChecksDirectoryExistence(t *testing.T) {
- t.Parallel()
- ctx := testlogging.Context(t)
- dataDir := testutil.TempDirectory(t)
- osi := newMockOS()
- osi.statRemainingErrors.Store(1)
- st, err := New(ctx, &Options{
- Path: dataDir,
- Options: sharded.Options{
- DirectoryShards: []int{5, 2},
- },
- osInterfaceOverride: osi,
- }, true)
- require.Error(t, err)
- require.Nil(t, st)
- }
- func TestFileStorage_ListBlobs_ErrorHandling(t *testing.T) {
- t.Parallel()
- ctx := testlogging.Context(t)
- dataDir := testutil.TempDirectory(t)
- osi := newMockOS()
- osi.readDirRemainingErrors.Store(3)
- osi.readDirRemainingFileDeletedDirEntry.Store(3)
- st, err := New(ctx, &Options{
- Path: dataDir,
- Options: sharded.Options{
- DirectoryShards: []int{5, 2},
- },
- }, true)
- require.NoError(t, err)
- asFsImpl(t, st).osi = osi
- defer st.Close(ctx)
- require.NoError(t, st.ListBlobs(ctx, "", func(bm blob.Metadata) error {
- return nil
- }))
- osi.readDirRemainingNonRetriableErrors.Store(1)
- require.ErrorIs(t, st.ListBlobs(ctx, "", func(bm blob.Metadata) error {
- return nil
- }), errNonRetriable)
- osi.readDirRemainingFatalDirEntry.Store(1)
- require.ErrorIs(t, st.ListBlobs(ctx, "", func(bm blob.Metadata) error {
- return nil
- }), errNonRetriable)
- }
- func TestFileStorage_TouchBlob_ErrorHandling(t *testing.T) {
- t.Parallel()
- ctx := testlogging.Context(t)
- dataDir := testutil.TempDirectory(t)
- osi := newMockOS()
- st, err := New(ctx, &Options{
- Path: dataDir,
- Options: sharded.Options{
- DirectoryShards: []int{5, 2},
- },
- }, true)
- require.NoError(t, err)
- asFsImpl(t, st).osi = osi
- defer st.Close(ctx)
- require.NoError(t, st.PutBlob(ctx, "someblob1234567812345678", gather.FromSlice([]byte{1, 2, 3}), blob.PutOptions{}))
- osi.statRemainingErrors.Store(1)
- _, err = testutil.EnsureType[*fsStorage](t, st).TouchBlob(ctx, "someblob1234567812345678", 0)
- require.NoError(t, err)
- }
- func TestFileStorage_Misc(t *testing.T) {
- t.Parallel()
- ctx := testlogging.Context(t)
- dataDir := testutil.TempDirectory(t)
- st, err := New(ctx, &Options{
- Path: dataDir,
- Options: sharded.Options{
- DirectoryShards: []int{5, 2},
- },
- }, true)
- require.NoError(t, err)
- defer st.Close(ctx)
- require.NoError(t, st.FlushCaches(ctx)) // this does nothing
- require.Equal(t, st.DisplayName(), "Filesystem: "+dataDir)
- }
- func verifyBlobTimestampOrder(t *testing.T, st blob.Storage, want ...blob.ID) {
- t.Helper()
- blobs, err := blob.ListAllBlobs(testlogging.Context(t), st, "")
- if err != nil {
- t.Errorf("error listing blobs: %v", err)
- return
- }
- sort.Slice(blobs, func(i, j int) bool {
- return blobs[i].Timestamp.Before(blobs[j].Timestamp)
- })
- var got []blob.ID
- for _, b := range blobs {
- got = append(got, b.BlobID)
- }
- if !reflect.DeepEqual(got, want) {
- t.Errorf("incorrect blob order: %v, wanted %v", blobs, want)
- }
- }
- func assertNoError(t *testing.T, err error) {
- t.Helper()
- if err != nil {
- t.Errorf("err: %v", err)
- }
- }
- func newMockOS() *mockOS {
- return &mockOS{
- osInterface: realOS{},
- }
- }
|