-
Notifications
You must be signed in to change notification settings - Fork 0
/
paths.go
237 lines (194 loc) · 6.04 KB
/
paths.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
// Copyright 2020 Adam Chalkley
//
// https://github.com/atc0005/check-path
//
// Licensed under the MIT License. See LICENSE file in the project root for
// full license information.
package paths
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/atc0005/check-path/internal/textutils"
)
// Application-specific errors for common path checks.
var (
ErrPathExists = errors.New("path exists")
ErrPathDoesNotExist = errors.New("path does not exist")
ErrPathEmptyString = errors.New("specified path is empty string")
ErrPathCheckFailed = errors.New("failed to check path")
ErrPathCheckCanceled = errors.New("path check canceled")
ErrPathOldFilesFound = errors.New("old files found in path")
ErrPathIgnored = errors.New("path ignored per request")
ErrSizeOfFilesTooLarge = errors.New("evaluated files in specified path too large")
ErrSizeOfFilesTooSmall = errors.New("evaluated files in specified path too small")
ErrPathMissingUsername = errors.New("requested username not set on file/directory")
ErrPathMissingGroupName = errors.New("requested group name not set on file/directory")
)
// ProcessResult is a superset of a MetaRecord and any associated error
// encountered while processing a path.
type ProcessResult struct {
MetaRecord
Error error
}
// AssertNotExists accepts a list of paths to process and returns the specific
// error if unable to check the path, a set MetaRecord value and a package
// specific associated error for a path that is found or an empty MetaRecord and
// nil if all paths do not exist.
func AssertNotExists(list []string) (MetaRecord, error) {
for _, path := range list {
pathInfo, err := Info(path)
switch {
// desired state
case errors.Is(err, ErrPathDoesNotExist):
continue
// some other error occurred
case err != nil:
return MetaRecord{}, fmt.Errorf(
"unable to assert non-existence of %s: %w",
path,
err,
)
// path found
case err == nil:
var pathType string
pathType = "file"
if pathInfo.IsDir() {
pathType = "directory"
}
return pathInfo, fmt.Errorf(
"%s %q: %w",
pathType, path, ErrPathExists,
)
}
}
// no errors, paths not found
return MetaRecord{}, nil
}
// Info is a helper function used to quickly gather details on a specified
// path. A MetaRecord value and nil is returned for successful path evaluation,
// otherwise an empty MetaRecord value and appropriate error is returned.
func Info(path string) (MetaRecord, error) {
// Make sure path isn't empty
if strings.TrimSpace(path) == "" {
return MetaRecord{}, ErrPathEmptyString
}
pathInfo, statErr := os.Stat(path)
if statErr != nil {
if !os.IsNotExist(statErr) {
// ERROR: another error occurred aside from file not found
return MetaRecord{}, fmt.Errorf(
"error checking path %s: %w",
path,
statErr,
)
}
// path not found
return MetaRecord{}, fmt.Errorf("%w: %s", ErrPathDoesNotExist, path)
}
// path found
return MetaRecord{
FileInfo: pathInfo,
FQPath: path,
ParentDir: filepath.Dir(path),
}, nil
}
// Exists is a helper function used to quickly determine whether a specified
// path exists.
func Exists(path string) (bool, error) {
// Make sure path isn't empty
if strings.TrimSpace(path) == "" {
return false, fmt.Errorf("specified path is empty string")
}
_, statErr := os.Stat(path)
if statErr != nil {
if !os.IsNotExist(statErr) {
// ERROR: another error occurred aside from file not found
return false, fmt.Errorf(
"error checking path %s: %w",
path,
statErr,
)
}
// file not found
return false, nil
}
// file found
return true, nil
}
// Process evalutes the specified path, either at a flat level or if
// specified, recursively. ProcessResult values are sent back by way of a
// results channel.
func Process(ctx context.Context, path string, ignoreList []string, recurse bool, results chan<- ProcessResult) {
// NOTE: This is safe to close *ONLY* because we recreate the channel on
// each iteration of the specified paths (e.g., one path at a time) before
// calling this function.
defer close(results)
// Qualify the path for later use, otherwise bail.
fqPath, absErr := filepath.Abs(path)
if absErr != nil {
results <- ProcessResult{
Error: absErr,
}
return
}
walkErr := filepath.Walk(fqPath, func(path string, info os.FileInfo, err error) error {
// If we return a non-nil error, this will stop the filepath.Walk()
// function from continuing to walk the path.
switch {
// error: context canceled
case ctx.Err() != nil:
results <- ProcessResult{
Error: fmt.Errorf("%s: %w", ctx.Err().Error(), ErrPathCheckCanceled),
}
return ctx.Err()
// error: path does not exist
case os.IsNotExist(err):
return fmt.Errorf("%w: %s", ErrPathDoesNotExist, path)
// error: other
case err != nil:
return err
// OK: we're excluding this path from further checks
case textutils.InList(path, ignoreList):
// report this as an error result, but of a type that the channel
// consumer will recognize as a special case
results <- ProcessResult{
Error: fmt.Errorf("%w: %s", ErrPathIgnored, path),
}
if info.IsDir() {
// if we have been asked to ignore a path that turns out to be
// a directory, we need to make sure that we don't descend
// into the directory and evaluate content within
return filepath.SkipDir
}
// nothing further to do for a path (file) we have been asked to
// ignore
return nil
// is a directory & not fully-qualified, specified path; skip if
// recurse is not enabled
case info.IsDir() && path != fqPath:
if !recurse {
return filepath.SkipDir
}
}
mr := MetaRecord{
FileInfo: info,
FQPath: path,
ParentDir: filepath.Dir(path),
}
results <- ProcessResult{
MetaRecord: mr,
}
// indicate no error to filepath.Walk() so that it will continue to
// the next item in the path (if applicable)
return nil
})
if walkErr != nil {
results <- ProcessResult{
Error: fmt.Errorf("error examining path %q: %w", path, walkErr),
}
}
}