-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfiles.go
297 lines (267 loc) · 8.33 KB
/
files.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
package files
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
"github.com/xyproto/binary"
"github.com/xyproto/env/v2"
)
// cache for storing file existence results
var (
cacheMutex sync.RWMutex
existsCache = make(map[string]bool)
whichCache = make(map[string]string)
executableCache = make(map[string]bool)
)
// Exists checks if the given path exists
func Exists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
// IsFile checks if the given path exists and is a regular file
func IsFile(path string) bool {
fi, err := os.Stat(path)
return err == nil && fi.Mode().IsRegular()
}
// IsSymlink checks if the given path exists and is a symbolic link
func IsSymlink(path string) bool {
fi, err := os.Lstat(path)
return err == nil && fi.Mode()&os.ModeSymlink != 0
}
// IsFileOrSymlink checks if the given path exists and is a regular file or a symbolic link
func IsFileOrSymlink(path string) bool {
// use Lstat instead of Stat to avoid following the symlink
fi, err := os.Lstat(path)
return err == nil && (fi.Mode().IsRegular() || (fi.Mode()&os.ModeSymlink != 0))
}
// IsDir checks if the given path exists and is a directory
func IsDir(path string) bool {
fi, err := os.Stat(path)
return err == nil && fi.Mode().IsDir()
}
// Which tries to find the given executable name in the $PATH
// Returns an empty string if not found.
func Which(executable string) string {
if p, err := exec.LookPath(executable); err == nil { // success
return p
}
return ""
}
// WhichCached tries to find the given executable name in the $PATH, using a cache for faster access.
// Assumes that the $PATH environment variable has not changed since the last check.
func WhichCached(executable string) string {
cacheMutex.RLock()
cachedResult, cached := whichCache[executable]
cacheMutex.RUnlock()
if cached {
return cachedResult
}
// If not cached, perform the lookup
path := Which(executable)
// Cache the result
cacheMutex.Lock()
whichCache[executable] = path
cacheMutex.Unlock()
return path
}
// PathHas checks if the given executable is in $PATH
func PathHas(executable string) bool {
_, err := exec.LookPath(executable)
return err == nil
}
// PathHasCached checks if the given executable is in $PATH (looks in the cache first and then caches the result)
func PathHasCached(executable string) bool {
return WhichCached(executable) != ""
}
// BinDirectory will check if the given filename is in one of these directories:
// /bin, /sbin, /usr/bin, /usr/sbin, /usr/local/bin, /usr/local/sbin, ~/.bin, ~/bin, ~/.local/bin
func BinDirectory(filename string) bool {
p, err := filepath.Abs(filepath.Dir(filename))
if err != nil {
return false
}
switch p {
case "/bin", "/sbin", "/usr/bin", "/usr/sbin", "/usr/local/bin", "/usr/local/sbin":
return true
}
homeDir := env.HomeDir()
switch p {
case filepath.Join(homeDir, ".bin"), filepath.Join(homeDir, "bin"), filepath.Join("local", "bin"):
return true
}
return false
}
// DataReadyOnStdin checks if data is ready on stdin
func DataReadyOnStdin() bool {
fileInfo, err := os.Stdin.Stat()
return err == nil && !(fileInfo.Mode()&os.ModeNamedPipe == 0)
}
// IsBinary returns true if the given filename can be read and is a binary file
func IsBinary(filename string) bool {
isBinary, err := binary.File(filename)
return err == nil && isBinary
}
// FilterOutBinaryFiles filters out files that are either binary or can not be read
func FilterOutBinaryFiles(filenames []string) []string {
var nonBinaryFilenames []string
for _, filename := range filenames {
if isBinary, err := binary.File(filename); !isBinary && err == nil {
nonBinaryFilenames = append(nonBinaryFilenames, filename)
}
}
return nonBinaryFilenames
}
// TimestampedFilename prefixes the given filename with a timestamp
func TimestampedFilename(filename string) string {
now := time.Now()
year, month, day := now.Date()
hour, minute, second := now.Clock()
return fmt.Sprintf("%04d-%02d-%02dT%02d-%02d-%02d-%s", year, int(month), day, hour, minute, second, filename)
}
// ShortPath replaces the home directory with ~ in a given path.
// The given path is expected to contain the home directory path either 0 or 1 times,
// and if it contains the path to the home directory, it is expected to be at the start of the given string.
func ShortPath(path string) string {
homeDir := env.HomeDir()
if strings.HasPrefix(path, homeDir) {
return strings.Replace(path, homeDir, "~", 1)
}
return path
}
// FileHas checks if the given file exists and contains the given string
func FileHas(path, what string) bool {
data, err := os.ReadFile(path)
if err != nil {
return false
}
return bytes.Contains(data, []byte(what))
}
// ReadString returns the contents of the given filename as a string.
// Returns an empty string if there were errors.
func ReadString(filename string) string {
if data, err := os.ReadFile(filename); err == nil { // success
return string(data)
}
return ""
}
// CanRead checks if 1 byte can actually be read from the given filename
func CanRead(filename string) bool {
f, err := os.Open(filename)
if err != nil {
return false
}
defer f.Close()
var onebyte [1]byte
n, err := io.ReadFull(f, onebyte[:])
// could exactly 1 byte be read?
return err == nil && n == 1
}
// Relative takes an absolute or relative path and attempts to return it relative to the current directory.
// If there are errors, it simply returns the given path.
func Relative(path string) string {
currentDir, err := os.Getwd()
if err != nil {
return path
}
relativePath, err := filepath.Rel(currentDir, path)
if err != nil {
return path
}
return relativePath
}
// Touch behaves like "touch" on the command line, and creates a file or updates the timestamp
func Touch(filename string) error {
// Check if the file exists
if Exists(filename) {
// If the file exists, update its modification time
currentTime := time.Now()
return os.Chtimes(filename, currentTime, currentTime)
}
// If the file does not exist, create it with mode 0666
file, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return err
}
return file.Close()
}
// ExistsCached checks if the given path exists, using a cache for faster access.
// Assumes that the filesystem has not changed since the last check.
func ExistsCached(path string) bool {
cacheMutex.RLock()
cachedResult, cached := existsCache[path]
cacheMutex.RUnlock()
if cached {
return cachedResult
}
// If not cached, check if the file exists
exists := Exists(path)
// Cache the result
cacheMutex.Lock()
existsCache[path] = exists
cacheMutex.Unlock()
return exists
}
// ClearCache clears all cache
func ClearCache() {
cacheMutex.Lock()
defer cacheMutex.Unlock()
existsCache = make(map[string]bool)
executableCache = make(map[string]bool)
whichCache = make(map[string]string)
}
// RemoveFile deletes a file, but it only returns an error if the file both exists and also could not be removed
func RemoveFile(path string) error {
err := os.Remove(path)
if !os.IsNotExist(err) {
return err // the file both exists and could not be removed
}
return nil // the file has been removed, or did not exist in the first place
}
// DirectoryWithFiles checks if the given path is a directory and contains at least one file
func DirectoryWithFiles(path string) (bool, error) {
if fileInfo, err := os.Stat(path); err != nil {
return false, err
} else if !fileInfo.IsDir() {
return false, fmt.Errorf("path is not a directory")
}
if entries, err := os.ReadDir(path); err != nil {
return false, err
} else {
for _, entry := range entries {
if entry.Type().IsRegular() || entry.Type()&os.ModeSymlink != 0 {
return true, nil
}
}
}
return false, nil
}
// IsExecutable checks if the given path exists and is executable
func IsExecutable(path string) bool {
fi, err := os.Stat(path)
if err != nil {
return false
}
mode := fi.Mode()
return mode.IsRegular() && mode&0111 != 0
}
// IsExecutableCached checks if the given path exists and is an executable file, with cache support.
// Assumes that the filesystem permissions have not changed since the last check.
func IsExecutableCached(path string) bool {
cacheMutex.RLock()
cachedResult, cached := executableCache[path]
cacheMutex.RUnlock()
if cached {
return cachedResult
}
isExecutable := IsExecutable(path)
cacheMutex.Lock()
executableCache[path] = isExecutable
cacheMutex.Unlock()
return isExecutable
}