]>
Commit | Line | Data |
---|---|---|
9b12e4fe JC |
1 | package fileutils |
2 | ||
3 | import ( | |
4 | "errors" | |
5 | "fmt" | |
6 | "io" | |
7 | "os" | |
8 | "path/filepath" | |
9 | "regexp" | |
10 | "strings" | |
11 | "text/scanner" | |
12 | ||
13 | "github.com/fsouza/go-dockerclient/external/github.com/Sirupsen/logrus" | |
14 | ) | |
15 | ||
16 | // exclusion return true if the specified pattern is an exclusion | |
17 | func exclusion(pattern string) bool { | |
18 | return pattern[0] == '!' | |
19 | } | |
20 | ||
21 | // empty return true if the specified pattern is empty | |
22 | func empty(pattern string) bool { | |
23 | return pattern == "" | |
24 | } | |
25 | ||
26 | // CleanPatterns takes a slice of patterns returns a new | |
27 | // slice of patterns cleaned with filepath.Clean, stripped | |
28 | // of any empty patterns and lets the caller know whether the | |
29 | // slice contains any exception patterns (prefixed with !). | |
30 | func CleanPatterns(patterns []string) ([]string, [][]string, bool, error) { | |
31 | // Loop over exclusion patterns and: | |
32 | // 1. Clean them up. | |
33 | // 2. Indicate whether we are dealing with any exception rules. | |
34 | // 3. Error if we see a single exclusion marker on it's own (!). | |
35 | cleanedPatterns := []string{} | |
36 | patternDirs := [][]string{} | |
37 | exceptions := false | |
38 | for _, pattern := range patterns { | |
39 | // Eliminate leading and trailing whitespace. | |
40 | pattern = strings.TrimSpace(pattern) | |
41 | if empty(pattern) { | |
42 | continue | |
43 | } | |
44 | if exclusion(pattern) { | |
45 | if len(pattern) == 1 { | |
46 | return nil, nil, false, errors.New("Illegal exclusion pattern: !") | |
47 | } | |
48 | exceptions = true | |
49 | } | |
50 | pattern = filepath.Clean(pattern) | |
51 | cleanedPatterns = append(cleanedPatterns, pattern) | |
52 | if exclusion(pattern) { | |
53 | pattern = pattern[1:] | |
54 | } | |
55 | patternDirs = append(patternDirs, strings.Split(pattern, "/")) | |
56 | } | |
57 | ||
58 | return cleanedPatterns, patternDirs, exceptions, nil | |
59 | } | |
60 | ||
61 | // Matches returns true if file matches any of the patterns | |
62 | // and isn't excluded by any of the subsequent patterns. | |
63 | func Matches(file string, patterns []string) (bool, error) { | |
64 | file = filepath.Clean(file) | |
65 | ||
66 | if file == "." { | |
67 | // Don't let them exclude everything, kind of silly. | |
68 | return false, nil | |
69 | } | |
70 | ||
71 | patterns, patDirs, _, err := CleanPatterns(patterns) | |
72 | if err != nil { | |
73 | return false, err | |
74 | } | |
75 | ||
76 | return OptimizedMatches(file, patterns, patDirs) | |
77 | } | |
78 | ||
79 | // OptimizedMatches is basically the same as fileutils.Matches() but optimized for archive.go. | |
80 | // It will assume that the inputs have been preprocessed and therefore the function | |
81 | // doesn't need to do as much error checking and clean-up. This was done to avoid | |
82 | // repeating these steps on each file being checked during the archive process. | |
83 | // The more generic fileutils.Matches() can't make these assumptions. | |
84 | func OptimizedMatches(file string, patterns []string, patDirs [][]string) (bool, error) { | |
85 | matched := false | |
86 | parentPath := filepath.Dir(file) | |
87 | parentPathDirs := strings.Split(parentPath, "/") | |
88 | ||
89 | for i, pattern := range patterns { | |
90 | negative := false | |
91 | ||
92 | if exclusion(pattern) { | |
93 | negative = true | |
94 | pattern = pattern[1:] | |
95 | } | |
96 | ||
97 | match, err := regexpMatch(pattern, file) | |
98 | if err != nil { | |
99 | return false, fmt.Errorf("Error in pattern (%s): %s", pattern, err) | |
100 | } | |
101 | ||
102 | if !match && parentPath != "." { | |
103 | // Check to see if the pattern matches one of our parent dirs. | |
104 | if len(patDirs[i]) <= len(parentPathDirs) { | |
105 | match, _ = regexpMatch(strings.Join(patDirs[i], "/"), | |
106 | strings.Join(parentPathDirs[:len(patDirs[i])], "/")) | |
107 | } | |
108 | } | |
109 | ||
110 | if match { | |
111 | matched = !negative | |
112 | } | |
113 | } | |
114 | ||
115 | if matched { | |
116 | logrus.Debugf("Skipping excluded path: %s", file) | |
117 | } | |
118 | ||
119 | return matched, nil | |
120 | } | |
121 | ||
122 | // regexpMatch tries to match the logic of filepath.Match but | |
123 | // does so using regexp logic. We do this so that we can expand the | |
124 | // wildcard set to include other things, like "**" to mean any number | |
125 | // of directories. This means that we should be backwards compatible | |
126 | // with filepath.Match(). We'll end up supporting more stuff, due to | |
127 | // the fact that we're using regexp, but that's ok - it does no harm. | |
128 | func regexpMatch(pattern, path string) (bool, error) { | |
129 | regStr := "^" | |
130 | ||
131 | // Do some syntax checking on the pattern. | |
132 | // filepath's Match() has some really weird rules that are inconsistent | |
133 | // so instead of trying to dup their logic, just call Match() for its | |
134 | // error state and if there is an error in the pattern return it. | |
135 | // If this becomes an issue we can remove this since its really only | |
136 | // needed in the error (syntax) case - which isn't really critical. | |
137 | if _, err := filepath.Match(pattern, path); err != nil { | |
138 | return false, err | |
139 | } | |
140 | ||
141 | // Go through the pattern and convert it to a regexp. | |
142 | // We use a scanner so we can support utf-8 chars. | |
143 | var scan scanner.Scanner | |
144 | scan.Init(strings.NewReader(pattern)) | |
145 | ||
146 | sl := string(os.PathSeparator) | |
147 | escSL := sl | |
148 | if sl == `\` { | |
149 | escSL += `\` | |
150 | } | |
151 | ||
152 | for scan.Peek() != scanner.EOF { | |
153 | ch := scan.Next() | |
154 | ||
155 | if ch == '*' { | |
156 | if scan.Peek() == '*' { | |
157 | // is some flavor of "**" | |
158 | scan.Next() | |
159 | ||
160 | if scan.Peek() == scanner.EOF { | |
161 | // is "**EOF" - to align with .gitignore just accept all | |
162 | regStr += ".*" | |
163 | } else { | |
164 | // is "**" | |
165 | regStr += "((.*" + escSL + ")|([^" + escSL + "]*))" | |
166 | } | |
167 | ||
168 | // Treat **/ as ** so eat the "/" | |
169 | if string(scan.Peek()) == sl { | |
170 | scan.Next() | |
171 | } | |
172 | } else { | |
173 | // is "*" so map it to anything but "/" | |
174 | regStr += "[^" + escSL + "]*" | |
175 | } | |
176 | } else if ch == '?' { | |
177 | // "?" is any char except "/" | |
178 | regStr += "[^" + escSL + "]" | |
179 | } else if strings.Index(".$", string(ch)) != -1 { | |
180 | // Escape some regexp special chars that have no meaning | |
181 | // in golang's filepath.Match | |
182 | regStr += `\` + string(ch) | |
183 | } else if ch == '\\' { | |
184 | // escape next char. Note that a trailing \ in the pattern | |
185 | // will be left alone (but need to escape it) | |
186 | if sl == `\` { | |
187 | // On windows map "\" to "\\", meaning an escaped backslash, | |
188 | // and then just continue because filepath.Match on | |
189 | // Windows doesn't allow escaping at all | |
190 | regStr += escSL | |
191 | continue | |
192 | } | |
193 | if scan.Peek() != scanner.EOF { | |
194 | regStr += `\` + string(scan.Next()) | |
195 | } else { | |
196 | regStr += `\` | |
197 | } | |
198 | } else { | |
199 | regStr += string(ch) | |
200 | } | |
201 | } | |
202 | ||
203 | regStr += "$" | |
204 | ||
205 | res, err := regexp.MatchString(regStr, path) | |
206 | ||
207 | // Map regexp's error to filepath's so no one knows we're not using filepath | |
208 | if err != nil { | |
209 | err = filepath.ErrBadPattern | |
210 | } | |
211 | ||
212 | return res, err | |
213 | } | |
214 | ||
215 | // CopyFile copies from src to dst until either EOF is reached | |
216 | // on src or an error occurs. It verifies src exists and remove | |
217 | // the dst if it exists. | |
218 | func CopyFile(src, dst string) (int64, error) { | |
219 | cleanSrc := filepath.Clean(src) | |
220 | cleanDst := filepath.Clean(dst) | |
221 | if cleanSrc == cleanDst { | |
222 | return 0, nil | |
223 | } | |
224 | sf, err := os.Open(cleanSrc) | |
225 | if err != nil { | |
226 | return 0, err | |
227 | } | |
228 | defer sf.Close() | |
229 | if err := os.Remove(cleanDst); err != nil && !os.IsNotExist(err) { | |
230 | return 0, err | |
231 | } | |
232 | df, err := os.Create(cleanDst) | |
233 | if err != nil { | |
234 | return 0, err | |
235 | } | |
236 | defer df.Close() | |
237 | return io.Copy(df, sf) | |
238 | } | |
239 | ||
240 | // ReadSymlinkedDirectory returns the target directory of a symlink. | |
241 | // The target of the symbolic link may not be a file. | |
242 | func ReadSymlinkedDirectory(path string) (string, error) { | |
243 | var realPath string | |
244 | var err error | |
245 | if realPath, err = filepath.Abs(path); err != nil { | |
246 | return "", fmt.Errorf("unable to get absolute path for %s: %s", path, err) | |
247 | } | |
248 | if realPath, err = filepath.EvalSymlinks(realPath); err != nil { | |
249 | return "", fmt.Errorf("failed to canonicalise path for %s: %s", path, err) | |
250 | } | |
251 | realPathInfo, err := os.Stat(realPath) | |
252 | if err != nil { | |
253 | return "", fmt.Errorf("failed to stat target '%s' of '%s': %s", realPath, path, err) | |
254 | } | |
255 | if !realPathInfo.Mode().IsDir() { | |
256 | return "", fmt.Errorf("canonical path points to a file '%s'", realPath) | |
257 | } | |
258 | return realPath, nil | |
259 | } | |
260 | ||
261 | // CreateIfNotExists creates a file or a directory only if it does not already exist. | |
262 | func CreateIfNotExists(path string, isDir bool) error { | |
263 | if _, err := os.Stat(path); err != nil { | |
264 | if os.IsNotExist(err) { | |
265 | if isDir { | |
266 | return os.MkdirAll(path, 0755) | |
267 | } | |
268 | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { | |
269 | return err | |
270 | } | |
271 | f, err := os.OpenFile(path, os.O_CREATE, 0755) | |
272 | if err != nil { | |
273 | return err | |
274 | } | |
275 | f.Close() | |
276 | } | |
277 | } | |
278 | return nil | |
279 | } |