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{}, } }