/
fileutil.go
331 lines (308 loc) · 8.86 KB
/
fileutil.go
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
package file
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"os"
"os/user"
"path"
"path/filepath"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/config/params"
log "github.com/sirupsen/logrus"
)
// ExpandPath given a string which may be a relative path.
// 1. replace tilde with users home dir
// 2. expands embedded environment variables
// 3. cleans the path, e.g. /a/b/../c -> /a/c
// Note, it has limitations, e.g. ~someuser/tmp will not be expanded
func ExpandPath(p string) (string, error) {
if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") {
if home := HomeDir(); home != "" {
p = home + p[1:]
}
}
return filepath.Abs(path.Clean(os.ExpandEnv(p)))
}
// HandleBackupDir takes an input directory path and either alters its permissions to be usable if it already exists, creates it if not
func HandleBackupDir(dirPath string, permissionOverride bool) error {
expanded, err := ExpandPath(dirPath)
if err != nil {
return err
}
exists, err := HasDir(expanded)
if err != nil {
return err
}
if exists {
info, err := os.Stat(expanded)
if err != nil {
return err
}
if info.Mode().Perm() != params.BeaconIoConfig().ReadWriteExecutePermissions {
if permissionOverride {
if err := os.Chmod(expanded, params.BeaconIoConfig().ReadWriteExecutePermissions); err != nil {
return err
}
} else {
return errors.New("dir already exists without proper 0700 permissions")
}
}
}
return os.MkdirAll(expanded, params.BeaconIoConfig().ReadWriteExecutePermissions)
}
// MkdirAll takes in a path, expands it if necessary, and looks through the
// permissions of every directory along the path, ensuring we are not attempting
// to overwrite any existing permissions. Finally, creates the directory accordingly
// with standardized, Prysm project permissions. This is the static-analysis enforced
// method for creating a directory programmatically in Prysm.
func MkdirAll(dirPath string) error {
expanded, err := ExpandPath(dirPath)
if err != nil {
return err
}
exists, err := HasDir(expanded)
if err != nil {
return err
}
if exists {
info, err := os.Stat(expanded)
if err != nil {
return err
}
if info.Mode().Perm() != params.BeaconIoConfig().ReadWriteExecutePermissions {
return errors.New("dir already exists without proper 0700 permissions")
}
}
return os.MkdirAll(expanded, params.BeaconIoConfig().ReadWriteExecutePermissions)
}
// WriteFile is the static-analysis enforced method for writing binary data to a file
// in Prysm, enforcing a single entrypoint with standardized permissions.
func WriteFile(file string, data []byte) error {
expanded, err := ExpandPath(file)
if err != nil {
return err
}
if Exists(expanded) {
info, err := os.Stat(expanded)
if err != nil {
return err
}
if info.Mode() != params.BeaconIoConfig().ReadWritePermissions {
return errors.New("file already exists without proper 0600 permissions")
}
}
return os.WriteFile(expanded, data, params.BeaconIoConfig().ReadWritePermissions)
}
// HomeDir for a user.
func HomeDir() string {
if home := os.Getenv("HOME"); home != "" {
return home
}
if usr, err := user.Current(); err == nil {
return usr.HomeDir
}
return ""
}
// HasDir checks if a directory indeed exists at the specified path.
func HasDir(dirPath string) (bool, error) {
fullPath, err := ExpandPath(dirPath)
if err != nil {
return false, err
}
info, err := os.Stat(fullPath)
if os.IsNotExist(err) {
return false, nil
}
if info == nil {
return false, err
}
return info.IsDir(), err
}
// HasReadWritePermissions checks if file at a path has proper
// 0600 permissions set.
func HasReadWritePermissions(itemPath string) (bool, error) {
info, err := os.Stat(itemPath)
if err != nil {
return false, err
}
return info.Mode() == params.BeaconIoConfig().ReadWritePermissions, nil
}
// Exists returns true if a file is not a directory and exists
// at the specified path.
func Exists(filename string) bool {
filePath, err := ExpandPath(filename)
if err != nil {
return false
}
info, err := os.Stat(filePath)
if err != nil {
if !os.IsNotExist(err) {
log.WithError(err).Info("Checking for file existence returned an error")
}
return false
}
return info != nil && !info.IsDir()
}
// RecursiveFileFind returns true, and the path, if a file is not a directory and exists
// at dir or any of its subdirectories. Finds the first instant based on the Walk order and returns.
// Define non-fatal error to stop the recursive directory walk
var stopWalk = errors.New("stop walking")
// RecursiveFileFind searches for file in a directory and its subdirectories.
func RecursiveFileFind(filename, dir string) (bool, string, error) {
var found bool
var fpath string
dir = filepath.Clean(dir)
found = false
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// checks if its a file and has the exact name as the filename
// need to break the walk function by using a non-fatal error
if !info.IsDir() && filename == info.Name() {
found = true
fpath = path
return stopWalk
}
// no errors or file found
return nil
})
if err != nil && err != stopWalk {
return false, "", err
}
return found, fpath, nil
}
// ReadFileAsBytes expands a file name's absolute path and reads it as bytes from disk.
func ReadFileAsBytes(filename string) ([]byte, error) {
filePath, err := ExpandPath(filename)
if err != nil {
return nil, errors.Wrap(err, "could not determine absolute path of password file")
}
return os.ReadFile(filePath) // #nosec G304
}
// CopyFile copy a file from source to destination path.
func CopyFile(src, dst string) error {
if !Exists(src) {
return errors.New("source file does not exist at provided path")
}
f, err := os.Open(src) // #nosec G304
if err != nil {
return err
}
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, params.BeaconIoConfig().ReadWritePermissions) // #nosec G304
if err != nil {
return err
}
_, err = io.Copy(dstFile, f)
return err
}
// CopyDir copies contents of one directory into another, recursively.
func CopyDir(src, dst string) error {
dstExists, err := HasDir(dst)
if err != nil {
return err
}
if dstExists {
return errors.New("destination directory already exists")
}
fds, err := os.ReadDir(src)
if err != nil {
return err
}
if err := MkdirAll(dst); err != nil {
return errors.Wrapf(err, "error creating directory: %s", dst)
}
for _, fd := range fds {
srcPath := path.Join(src, fd.Name())
dstPath := path.Join(dst, fd.Name())
if fd.IsDir() {
if err = CopyDir(srcPath, dstPath); err != nil {
return errors.Wrapf(err, "error copying directory %s -> %s", srcPath, dstPath)
}
} else {
if err = CopyFile(srcPath, dstPath); err != nil {
return errors.Wrapf(err, "error copying file %s -> %s", srcPath, dstPath)
}
}
}
return nil
}
// DirsEqual checks whether two directories have the same content.
func DirsEqual(src, dst string) bool {
hash1, err := HashDir(src)
if err != nil {
return false
}
hash2, err := HashDir(dst)
if err != nil {
return false
}
return hash1 == hash2
}
// HashDir calculates and returns hash of directory: each file's hash is calculated and saved along
// with the file name into the list, after which list is hashed to produce the final signature.
// Implementation is based on https://github.com/golang/mod/blob/release-branch.go1.15/sumdb/dirhash/hash.go
func HashDir(dir string) (string, error) {
files, err := DirFiles(dir)
if err != nil {
return "", err
}
h := sha256.New()
files = append([]string(nil), files...)
sort.Strings(files)
for _, file := range files {
hf, err := HashFile(filepath.Join(dir, file))
if err != nil {
return "", err
}
if _, err := fmt.Fprintf(h, "%x %s\n", hf, file); err != nil {
return "", err
}
}
return "hashdir:" + base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
}
// HashFile calculates and returns the hash of a file.
func HashFile(filePath string) ([]byte, error) {
f, err := os.Open(filepath.Clean(filePath))
if err != nil {
return nil, err
}
hf := sha256.New()
if _, err := io.Copy(hf, f); err != nil {
return nil, err
}
err = f.Close()
if err != nil {
return nil, err
}
return hf.Sum(nil), nil
}
// DirFiles returns list of files found within a given directory and its sub-directories.
// Directory prefix will not be included as a part of returned file string i.e. for a file located
// in "dir/foo/bar" only "foo/bar" part will be returned.
func DirFiles(dir string) ([]string, error) {
var files []string
dir = filepath.Clean(dir)
err := filepath.Walk(dir, func(file string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
relFile := file
if dir != "." {
relFile = file[len(dir)+1:]
}
files = append(files, filepath.ToSlash(relFile))
return nil
})
if err != nil {
return nil, err
}
return files, nil
}