Welcome to mirror list, hosted at ThFree Co, Russian Federation.

file_writer.go « safe « internal - gitlab.com/gitlab-org/gitaly.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 76ab0bcc27d978a355773f75edc61971ac78330a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
package safe

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sync"
)

// ErrAlreadyDone is returned when the safe file has already been closed
// or committed
var ErrAlreadyDone = errors.New("safe file was already committed or closed")

// FileWriter is a thread safe writer that does an atomic write to the target file. It allows one
// writer at a time to acquire a lock, write the file, and atomically replace the contents of the target file.
type FileWriter struct {
	tmpFile       *os.File
	path          string
	commitOrClose sync.Once
}

// FileWriterConfig contains configuration for the `NewFileWriter()` function.
type FileWriterConfig struct {
	// FileMode is the desired file mode of the committed target file. If left at its default
	// value, then no file mode will be explicitly set for the file.
	FileMode os.FileMode
}

// NewFileWriter takes path as an absolute path of the target file and creates a new FileWriter by
// attempting to create a tempfile. This function either takes no FileWriterConfig or exactly one.
func NewFileWriter(path string, optionalCfg ...FileWriterConfig) (*FileWriter, error) {
	var cfg FileWriterConfig
	if len(optionalCfg) == 1 {
		cfg = optionalCfg[0]
	} else if len(optionalCfg) > 1 {
		return nil, fmt.Errorf("file writer created with more than one config")
	}

	writer := &FileWriter{path: path}

	directory := filepath.Dir(path)

	tmpFile, err := os.CreateTemp(directory, filepath.Base(path))
	if err != nil {
		return nil, err
	}

	if cfg.FileMode != 0 {
		if err := tmpFile.Chmod(cfg.FileMode); err != nil {
			_ = writer.Close()
			return nil, err
		}
	}

	writer.tmpFile = tmpFile

	return writer, nil
}

// Write wraps the temporary file's Write.
func (fw *FileWriter) Write(p []byte) (n int, err error) {
	return fw.tmpFile.Write(p)
}

// Commit will close the temporary file and rename it to the target file name
// the first call to Commit() will close and delete the temporary file, so
// subsequently calls to Commit() are gauaranteed to return an error.
func (fw *FileWriter) Commit() error {
	err := ErrAlreadyDone

	fw.commitOrClose.Do(func() {
		if err = fw.tmpFile.Sync(); err != nil {
			err = fmt.Errorf("syncing temp file: %w", err)
			return
		}

		if err = fw.tmpFile.Close(); err != nil {
			err = fmt.Errorf("closing temp file: %w", err)
			return
		}

		if err = fw.rename(); err != nil {
			err = fmt.Errorf("renaming temp file: %w", err)
			return
		}

		if err = fw.syncDir(); err != nil {
			err = fmt.Errorf("syncing dir: %w", err)
			return
		}
	})

	return err
}

// rename renames the temporary file to the target file
func (fw *FileWriter) rename() error {
	return os.Rename(fw.tmpFile.Name(), fw.path)
}

// syncDir will sync the directory
func (fw *FileWriter) syncDir() error {
	f, err := os.Open(filepath.Dir(fw.path))
	if err != nil {
		return err
	}
	defer f.Close()

	return f.Sync()
}

// Close will close and remove the temp file artifact if it exists. If the file
// was already committed, an ErrAlreadyClosed error will be returned and no
// changes will be made to the filesystem.
func (fw *FileWriter) Close() error {
	err := ErrAlreadyDone

	fw.commitOrClose.Do(func() {
		if err = fw.tmpFile.Close(); err != nil {
			return
		}
		if err = os.Remove(fw.tmpFile.Name()); err != nil && !os.IsNotExist(err) {
			return
		}
	})

	return err
}