Skip to content

Commit 184dbfd

Browse files
committed
filesystem: add lockfile write function
Add 'WriteLockFileFunc()' to provide a mechanism for updating files semi-atomically. The function takes a filename and a function (the logic perform the write to a given file pointer, executes the write function on '<filename>.lock', then returns a 'LockFile' object. That returned 'LockFile' can then either invoke 'Commit()' (to rename '<filename>.lock' to '<filename>') or 'Rollback()' (to delete '<filename>.lock'). If 'WriteLockFileFunc()' encounters any error in its operation, it tries to rollback the lockfile before returning the error. This function is mainly useful for writing files that may be read concurrently with the write operation. Writing the file to a lockfile, then atomically renaming the file avoids avoids the possibility of another process reading a half-written update to the file. Example usage: --- lockFile, err := fileSystem.WriteLockFileFunc("myfile.txt", func(f io.Writer) error { f.Write([]byte("my text")) }) if err != nil { return err } lockFile.Commit() --- Signed-off-by: Victoria Dye <[email protected]>
1 parent 7a3b9a5 commit 184dbfd

File tree

3 files changed

+59
-0
lines changed

3 files changed

+59
-0
lines changed

cmd/utils/container-helpers.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func BuildGitBundleServerContainer(logger log.TraceLogger) *DependencyContainer
3333
registerDependency(container, func(ctx context.Context) bundles.BundleProvider {
3434
return bundles.NewBundleProvider(
3535
logger,
36+
GetDependency[common.FileSystem](ctx, container),
3637
GetDependency[git.GitHelper](ctx, container),
3738
)
3839
})

internal/common/filesystem.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bufio"
55
"errors"
66
"fmt"
7+
"io"
78
"io/fs"
89
"os"
910
"path"
@@ -16,11 +17,30 @@ const (
1617
DefaultDirPermissions fs.FileMode = 0o755
1718
)
1819

20+
type LockFile interface {
21+
Commit() error
22+
Rollback() error
23+
}
24+
25+
type lockFile struct {
26+
filename string
27+
lockFilename string
28+
}
29+
30+
func (l *lockFile) Commit() error {
31+
return os.Rename(l.lockFilename, l.filename)
32+
}
33+
34+
func (l *lockFile) Rollback() error {
35+
return os.Remove(l.lockFilename)
36+
}
37+
1938
type FileSystem interface {
2039
GetLocalExecutable(name string) (string, error)
2140

2241
FileExists(filename string) (bool, error)
2342
WriteFile(filename string, content []byte) error
43+
WriteLockFileFunc(filename string, writeFunc func(io.Writer) error) (LockFile, error)
2444
DeleteFile(filename string) (bool, error)
2545
ReadFileLines(filename string) ([]string, error)
2646
}
@@ -86,6 +106,37 @@ func (f *fileSystem) WriteFile(filename string, content []byte) error {
86106
return nil
87107
}
88108

109+
func (f *fileSystem) WriteLockFileFunc(filename string, writeFunc func(io.Writer) error) (LockFile, error) {
110+
err := f.createLeadingDirs(filename)
111+
if err != nil {
112+
return nil, err
113+
}
114+
115+
lockFilename := filename + ".lock"
116+
lock, err := os.OpenFile(lockFilename, os.O_WRONLY|os.O_CREATE, DefaultFilePermissions)
117+
if err != nil {
118+
return nil, fmt.Errorf("failed to open file: %w", err)
119+
}
120+
lockFile := &lockFile{filename: filename, lockFilename: lockFilename}
121+
122+
err = writeFunc(lock)
123+
if err != nil {
124+
// Try to close & rollback - don't worry about errors, we're already failing.
125+
lock.Close()
126+
lockFile.Rollback()
127+
return nil, err
128+
}
129+
130+
err = lock.Close()
131+
if err != nil {
132+
// Try to rollback - don't worry about errors, we're already failing.
133+
lockFile.Rollback()
134+
return nil, fmt.Errorf("failed to close lock file: %w", err)
135+
}
136+
137+
return lockFile, nil
138+
}
139+
89140
func (f *fileSystem) DeleteFile(filename string) (bool, error) {
90141
err := os.Remove(filename)
91142
if err == nil {

internal/testhelpers/mocks.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package testhelpers
33
import (
44
"context"
55
"fmt"
6+
"io"
67
"os/exec"
78
"os/user"
89
"runtime"
910

1011
"github.com/github/git-bundle-server/internal/cmd"
12+
"github.com/github/git-bundle-server/internal/common"
1113
"github.com/stretchr/testify/mock"
1214
)
1315

@@ -165,6 +167,11 @@ func (m *MockFileSystem) WriteFile(filename string, content []byte) error {
165167
return fnArgs.Error(0)
166168
}
167169

170+
func (m *MockFileSystem) WriteLockFileFunc(filename string, writeFunc func(io.Writer) error) (common.LockFile, error) {
171+
fnArgs := m.Called(filename, writeFunc)
172+
return fnArgs.Get(0).(common.LockFile), fnArgs.Error(1)
173+
}
174+
168175
func (m *MockFileSystem) DeleteFile(filename string) (bool, error) {
169176
fnArgs := m.Called(filename)
170177
return fnArgs.Bool(0), fnArgs.Error(1)

0 commit comments

Comments
 (0)