-
Notifications
You must be signed in to change notification settings - Fork 351
/
io.go
177 lines (158 loc) · 3.94 KB
/
io.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
package fileutil
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/karrick/godirwalk"
)
const (
DefaultDirectoryMask = 0o755
)
var (
ErrNotFile = errors.New("path is not a file")
ErrBadPath = errors.New("bad path traversal blocked")
ErrSymbolicLink = errors.New("symbolic links not supported")
ErrInvalidPath = errors.New("invalid path")
)
// IsDir Returns true if p is a directory, otherwise false
func IsDir(p string) (bool, error) {
stat, err := os.Stat(p)
if err != nil {
return false, err
}
return stat.IsDir(), nil
}
// FindInParents Returns the first occurrence of filename going up the dir tree
func FindInParents(dir, filename string) (string, error) {
var lookup string
fullPath, err := filepath.Abs(dir)
if err != nil {
return "", err
}
volumeName := filepath.VolumeName(fullPath)
for fullPath != filepath.Join(volumeName, string(filepath.Separator)) {
info, err := os.Stat(fullPath)
if err != nil {
return "", fmt.Errorf("%s: %w", fullPath, err)
}
if !info.IsDir() {
// find filename here
lookup = filepath.Join(filepath.Dir(fullPath), filename)
} else {
lookup = filepath.Join(fullPath, filename)
}
_, err = os.Stat(lookup)
if err == nil {
return lookup, nil
}
if !errors.Is(err, fs.ErrNotExist) {
return "", err
}
// error == fs.ErrNotExist
fullPath = filepath.Dir(fullPath)
}
return "", nil
}
func IsDirEmpty(dir string) (bool, error) {
s, err := godirwalk.NewScanner(dir)
if err != nil {
return false, err
}
defer func() {
_ = s.Close()
}()
// Attempt to read only the first directory entry. Note that Scan skips both "." and ".." entries.
hasAtLeastOneChild := s.Scan()
if err = s.Err(); err != nil {
return false, err
}
if hasAtLeastOneChild {
return false, nil
}
return true, nil
}
// PruneEmptyDirectories iterates through the directory tree, removing empty directories, and directories that only
// contain empty directories.
func PruneEmptyDirectories(dir string) ([]string, error) {
var pruned []string
err := godirwalk.Walk(dir, &godirwalk.Options{
Unsorted: true,
Callback: func(_ string, _ *godirwalk.Dirent) error {
// no-op while diving in; all the fun happens in PostChildrenCallback
return nil
},
PostChildrenCallback: func(d string, _ *godirwalk.Dirent) error {
empty, err := IsDirEmpty(d)
if err != nil {
return err
}
if d == dir || !empty { // do not remove top level directory or a directory with at least one child
return nil
}
err = os.Remove(d)
if err == nil {
pruned = append(pruned, d)
}
return err
},
})
return pruned, err
}
func RemoveFile(p string) error {
fileExists, err := FileExists(p)
if err != nil {
return err
}
if !fileExists {
return nil // does not exist
}
return os.Remove(p)
}
func FileExists(p string) (bool, error) {
info, err := os.Stat(p)
if os.IsNotExist(err) {
return false, nil
} else if err != nil {
return false, err
}
if !info.IsDir() {
return true, nil
}
return false, fmt.Errorf("%s: %w", p, ErrNotFile)
}
func VerifyAbsPath(absPath, basePath string) error {
// check we have a valid abs path
if !filepath.IsAbs(absPath) || filepath.Clean(absPath) != absPath {
return ErrBadPath
}
// point to storage namespace
if !strings.HasPrefix(absPath, basePath) {
return ErrInvalidPath
}
return nil
}
func VerifyRelPath(relPath, basePath string) error {
abs := filepath.Join(basePath, relPath)
return VerifyAbsPath(abs, basePath)
}
// VerifySafeFilename checks that the given file name is not a symbolic link and that
// the file name does not contain path traversal
func VerifySafeFilename(absPath string) error {
if err := VerifyAbsPath(absPath, absPath); err != nil {
return err
}
if !filepath.IsAbs(absPath) {
return fmt.Errorf("relative path not allowed: %w", ErrInvalidPath)
}
filename, err := filepath.EvalSymlinks(absPath)
if err != nil {
return err
}
if filename != absPath {
return ErrSymbolicLink
}
return nil
}