main
1package util
2
3import (
4 "errors"
5 "io"
6 "os"
7 "path/filepath"
8 "strconv"
9 "sync"
10 "time"
11
12 "github.com/go-git/go-billy/v5"
13)
14
15// RemoveAll removes path and any children it contains. It removes everything it
16// can but returns the first error it encounters. If the path does not exist,
17// RemoveAll returns nil (no error).
18func RemoveAll(fs billy.Basic, path string) error {
19 fs, path = getUnderlyingAndPath(fs, path)
20
21 if r, ok := fs.(removerAll); ok {
22 return r.RemoveAll(path)
23 }
24
25 return removeAll(fs, path)
26}
27
28type removerAll interface {
29 RemoveAll(string) error
30}
31
32func removeAll(fs billy.Basic, path string) error {
33 // This implementation is adapted from os.RemoveAll.
34
35 // Simple case: if Remove works, we're done.
36 err := fs.Remove(path)
37 if err == nil || errors.Is(err, os.ErrNotExist) {
38 return nil
39 }
40
41 // Otherwise, is this a directory we need to recurse into?
42 dir, serr := fs.Stat(path)
43 if serr != nil {
44 if errors.Is(serr, os.ErrNotExist) {
45 return nil
46 }
47
48 return serr
49 }
50
51 if !dir.IsDir() {
52 // Not a directory; return the error from Remove.
53 return err
54 }
55
56 dirfs, ok := fs.(billy.Dir)
57 if !ok {
58 return billy.ErrNotSupported
59 }
60
61 // Directory.
62 fis, err := dirfs.ReadDir(path)
63 if err != nil {
64 if errors.Is(err, os.ErrNotExist) {
65 // Race. It was deleted between the Lstat and Open.
66 // Return nil per RemoveAll's docs.
67 return nil
68 }
69
70 return err
71 }
72
73 // Remove contents & return first error.
74 err = nil
75 for _, fi := range fis {
76 cpath := fs.Join(path, fi.Name())
77 err1 := removeAll(fs, cpath)
78 if err == nil {
79 err = err1
80 }
81 }
82
83 // Remove directory.
84 err1 := fs.Remove(path)
85 if err1 == nil || errors.Is(err1, os.ErrNotExist) {
86 return nil
87 }
88
89 if err == nil {
90 err = err1
91 }
92
93 return err
94
95}
96
97// WriteFile writes data to a file named by filename in the given filesystem.
98// If the file does not exist, WriteFile creates it with permissions perm;
99// otherwise WriteFile truncates it before writing.
100func WriteFile(fs billy.Basic, filename string, data []byte, perm os.FileMode) (err error) {
101 f, err := fs.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
102 if err != nil {
103 return err
104 }
105 defer func() {
106 if f != nil {
107 err1 := f.Close()
108 if err == nil {
109 err = err1
110 }
111 }
112 }()
113
114 n, err := f.Write(data)
115 if err == nil && n < len(data) {
116 err = io.ErrShortWrite
117 }
118
119 return nil
120}
121
122// Random number state.
123// We generate random temporary file names so that there's a good
124// chance the file doesn't exist yet - keeps the number of tries in
125// TempFile to a minimum.
126var rand uint32
127var randmu sync.Mutex
128
129func reseed() uint32 {
130 return uint32(time.Now().UnixNano() + int64(os.Getpid()))
131}
132
133func nextSuffix() string {
134 randmu.Lock()
135 r := rand
136 if r == 0 {
137 r = reseed()
138 }
139 r = r*1664525 + 1013904223 // constants from Numerical Recipes
140 rand = r
141 randmu.Unlock()
142 return strconv.Itoa(int(1e9 + r%1e9))[1:]
143}
144
145// TempFile creates a new temporary file in the directory dir with a name
146// beginning with prefix, opens the file for reading and writing, and returns
147// the resulting *os.File. If dir is the empty string, TempFile uses the default
148// directory for temporary files (see os.TempDir). Multiple programs calling
149// TempFile simultaneously will not choose the same file. The caller can use
150// f.Name() to find the pathname of the file. It is the caller's responsibility
151// to remove the file when no longer needed.
152func TempFile(fs billy.Basic, dir, prefix string) (f billy.File, err error) {
153 // This implementation is based on stdlib ioutil.TempFile.
154 if dir == "" {
155 dir = getTempDir(fs)
156 }
157
158 nconflict := 0
159 for i := 0; i < 10000; i++ {
160 name := filepath.Join(dir, prefix+nextSuffix())
161 f, err = fs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
162 if errors.Is(err, os.ErrExist) {
163 if nconflict++; nconflict > 10 {
164 randmu.Lock()
165 rand = reseed()
166 randmu.Unlock()
167 }
168 continue
169 }
170 break
171 }
172 return
173}
174
175// TempDir creates a new temporary directory in the directory dir
176// with a name beginning with prefix and returns the path of the
177// new directory. If dir is the empty string, TempDir uses the
178// default directory for temporary files (see os.TempDir).
179// Multiple programs calling TempDir simultaneously
180// will not choose the same directory. It is the caller's responsibility
181// to remove the directory when no longer needed.
182func TempDir(fs billy.Dir, dir, prefix string) (name string, err error) {
183 // This implementation is based on stdlib ioutil.TempDir
184
185 if dir == "" {
186 dir = getTempDir(fs.(billy.Basic))
187 }
188
189 nconflict := 0
190 for i := 0; i < 10000; i++ {
191 try := filepath.Join(dir, prefix+nextSuffix())
192 err = fs.MkdirAll(try, 0700)
193 if errors.Is(err, os.ErrExist) {
194 if nconflict++; nconflict > 10 {
195 randmu.Lock()
196 rand = reseed()
197 randmu.Unlock()
198 }
199 continue
200 }
201 if errors.Is(err, os.ErrNotExist) {
202 if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) {
203 return "", err
204 }
205 }
206 if err == nil {
207 name = try
208 }
209 break
210 }
211 return
212}
213
214func getTempDir(fs billy.Basic) string {
215 ch, ok := fs.(billy.Chroot)
216 if !ok || ch.Root() == "" || ch.Root() == "/" || ch.Root() == string(filepath.Separator) {
217 return os.TempDir()
218 }
219
220 return ".tmp"
221}
222
223type underlying interface {
224 Underlying() billy.Basic
225}
226
227func getUnderlyingAndPath(fs billy.Basic, path string) (billy.Basic, string) {
228 u, ok := fs.(underlying)
229 if !ok {
230 return fs, path
231 }
232 if ch, ok := fs.(billy.Chroot); ok {
233 path = fs.Join(ch.Root(), path)
234 }
235
236 return u.Underlying(), path
237}
238
239// ReadFile reads the named file and returns the contents from the given filesystem.
240// A successful call returns err == nil, not err == EOF.
241// Because ReadFile reads the whole file, it does not treat an EOF from Read
242// as an error to be reported.
243func ReadFile(fs billy.Basic, name string) ([]byte, error) {
244 f, err := fs.Open(name)
245 if err != nil {
246 return nil, err
247 }
248
249 defer f.Close()
250
251 var size int
252 if info, err := fs.Stat(name); err == nil {
253 size64 := info.Size()
254 if int64(int(size64)) == size64 {
255 size = int(size64)
256 }
257 }
258
259 size++ // one byte for final read at EOF
260 // If a file claims a small size, read at least 512 bytes.
261 // In particular, files in Linux's /proc claim size 0 but
262 // then do not work right if read in small pieces,
263 // so an initial read of 1 byte would not work correctly.
264
265 if size < 512 {
266 size = 512
267 }
268
269 data := make([]byte, 0, size)
270 for {
271 if len(data) >= cap(data) {
272 d := append(data[:cap(data)], 0)
273 data = d[:len(data)]
274 }
275
276 n, err := f.Read(data[len(data):cap(data)])
277 data = data[:len(data)+n]
278
279 if err != nil {
280 if errors.Is(err, io.EOF) {
281 err = nil
282 }
283
284 return data, err
285 }
286 }
287}