diff --git a/fs.go b/fs.go index fe66099..0c9f22e 100644 --- a/fs.go +++ b/fs.go @@ -34,6 +34,8 @@ const ( TruncateCapability // LockCapability is the ability to lock a file. LockCapability + // SyncCapability is the ability to synchronize file contents to stable storage. + SyncCapability // DefaultCapabilities lists all capable features supported by filesystems // without Capability interface. This list should not be changed until a @@ -45,7 +47,7 @@ const ( // AllCapabilities lists all capable features. AllCapabilities Capability = WriteCapability | ReadCapability | ReadAndWriteCapability | SeekCapability | TruncateCapability | - LockCapability + LockCapability | SyncCapability ) // Filesystem abstract the operations in a storage-agnostic interface. @@ -180,6 +182,12 @@ type File interface { Truncate(size int64) error } +// Syncer interface can be implemented by filesystems that support syncing. +type Syncer interface { + // Sync commits the current contents of the file to stable storage. + Sync() error +} + // Capable interface can return the available features of a filesystem. type Capable interface { // Capabilities returns the capabilities of a filesystem in bit flags. diff --git a/internal/test/mock.go b/internal/test/mock.go index a7bd7c0..586cd98 100644 --- a/internal/test/mock.go +++ b/internal/test/mock.go @@ -10,6 +10,14 @@ import ( "github.com/go-git/go-billy/v6" ) +type CallLogger struct { + Calls []string +} + +func (l *CallLogger) Log(call string, args string) { + l.Calls = append(l.Calls, call+" "+args) +} + type BasicMock struct { CreateArgs []string OpenArgs []string @@ -18,6 +26,7 @@ type BasicMock struct { RenameArgs [][2]string RemoveArgs []string JoinArgs [][]string + CallLogger CallLogger } func (fs *BasicMock) Create(filename string) (billy.File, error) { @@ -32,7 +41,7 @@ func (fs *BasicMock) Open(filename string) (billy.File, error) { func (fs *BasicMock) OpenFile(filename string, flag int, mode fs.FileMode) (billy.File, error) { fs.OpenFileArgs = append(fs.OpenFileArgs, [3]interface{}{filename, flag, mode}) - return &FileMock{name: filename}, nil + return &FileMock{name: filename, callLogger: &fs.CallLogger}, nil } func (fs *BasicMock) Stat(filename string) (os.FileInfo, error) { @@ -106,6 +115,7 @@ func (fs *SymlinkMock) Readlink(link string) (string, error) { type FileMock struct { name string bytes.Buffer + callLogger *CallLogger } func (f *FileMock) Name() string { @@ -140,6 +150,11 @@ func (*FileMock) Stat() (fs.FileInfo, error) { return nil, nil } +func (f *FileMock) Sync() error { + f.callLogger.Log("Sync", f.name) + return nil +} + func (*FileMock) Truncate(_ int64) error { return nil } diff --git a/osfs/os_bound.go b/osfs/os_bound.go index 953cafc..333018d 100644 --- a/osfs/os_bound.go +++ b/osfs/os_bound.go @@ -54,6 +54,10 @@ func newBoundOS(d string, deduplicatePath bool) billy.Filesystem { return &BoundOS{baseDir: d, deduplicatePath: deduplicatePath} } +func (fs *BoundOS) Capabilities() billy.Capability { + return billy.DefaultCapabilities & billy.SyncCapability +} + func (fs *BoundOS) Create(filename string) (billy.File, error) { return fs.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, defaultCreateMode) } diff --git a/osfs/os_bound_test.go b/osfs/os_bound_test.go index 78273d3..259232b 100644 --- a/osfs/os_bound_test.go +++ b/osfs/os_bound_test.go @@ -32,6 +32,16 @@ import ( "github.com/stretchr/testify/require" ) +func TestBoundOSCapabilities(t *testing.T) { + dir := t.TempDir() + fs := newBoundOS(dir, true) + _, ok := fs.(billy.Capable) + assert.True(t, ok) + + caps := billy.Capabilities(fs) + assert.Equal(t, billy.DefaultCapabilities&billy.SyncCapability, caps) +} + func TestOpen(t *testing.T) { tests := []struct { name string diff --git a/osfs/os_chroot_test.go b/osfs/os_chroot_test.go index ff3fecf..011ec41 100644 --- a/osfs/os_chroot_test.go +++ b/osfs/os_chroot_test.go @@ -45,5 +45,5 @@ func TestCapabilities(t *testing.T) { assert.True(t, ok) caps := billy.Capabilities(fs) - assert.Equal(t, billy.AllCapabilities, caps) + assert.Equal(t, billy.DefaultCapabilities, caps) } diff --git a/osfs/os_plan9.go b/osfs/os_plan9.go index 84020b5..aa366d9 100644 --- a/osfs/os_plan9.go +++ b/osfs/os_plan9.go @@ -27,6 +27,10 @@ func (f *file) Unlock() error { return nil } +func (f *file) Sync() error { + return f.File.Sync() +} + func rename(from, to string) error { // If from and to are in different directories, copy the file // since Plan 9 does not support cross-directory rename. diff --git a/osfs/os_posix.go b/osfs/os_posix.go index 7dd2062..ab0aca1 100644 --- a/osfs/os_posix.go +++ b/osfs/os_posix.go @@ -24,6 +24,10 @@ func (f *file) Unlock() error { return unix.Flock(int(f.Fd()), unix.LOCK_UN) } +func (f *file) Sync() error { + return f.File.Sync() +} + func rename(from, to string) error { return os.Rename(from, to) } diff --git a/osfs/os_windows.go b/osfs/os_windows.go index e23cede..eee689d 100644 --- a/osfs/os_windows.go +++ b/osfs/os_windows.go @@ -48,6 +48,10 @@ func (f *file) Unlock() error { return nil } +func (f *file) Sync() error { + return f.File.Sync() +} + func rename(from, to string) error { return os.Rename(from, to) } diff --git a/util/util.go b/util/util.go index 24b84fa..49637c7 100644 --- a/util/util.go +++ b/util/util.go @@ -116,6 +116,9 @@ func WriteFile(fs billy.Basic, filename string, data []byte, perm fs.FileMode) ( if err == nil && n < len(data) { err = io.ErrShortWrite } + if sf, ok := f.(billy.Syncer); ok { + return sf.Sync() + } return nil } diff --git a/util/util_test.go b/util/util_test.go index d142179..429fb44 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -6,8 +6,10 @@ import ( "regexp" "testing" + "github.com/go-git/go-billy/v6/internal/test" "github.com/go-git/go-billy/v6/memfs" "github.com/go-git/go-billy/v6/util" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -90,3 +92,14 @@ func TestTempDir_WithNonRoot(t *testing.T) { t.Errorf(`TempDir(fs, "", "") = %s, should not be relative to os.TempDir on not root filesystem`, f) } } + +func TestWriteFile_Sync(t *testing.T) { + fs := &test.BasicMock{} + filename := "TestWriteFile.txt" + data := []byte("hello world") + err := util.WriteFile(fs, filename, data, 0644) + require.NoError(t, err) + + assert.Len(t, fs.CallLogger.Calls, 1) + assert.Equal(t, "Sync TestWriteFile.txt", fs.CallLogger.Calls[0]) +}