Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(797)

Side by Side Diff: go/src/infra/tools/cipd/files.go

Issue 1129043003: cipd: Refactor client to make it more readable. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « go/src/infra/tools/cipd/fetcher_test.go ('k') | go/src/infra/tools/cipd/files_test.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package cipd
6
7 import (
8 "fmt"
9 "io"
10 "io/ioutil"
11 "os"
12 "path/filepath"
13 "strings"
14 )
15
16 // File defines a single file to be added or extracted from a package. All paths
17 // are slash separated (including symlink targets).
18 type File interface {
19 // Name returns slash separated file path relative to a package root, e. g. "dir/dir/file".
20 Name() string
21 // Size returns size of the file. 0 for symlinks.
22 Size() uint64
23 // Executable returns true if the file is executable. Only used for Linu x\Mac archives. false for symlinks.
24 Executable() bool
25 // Symlink returns true if the file is a symlink.
26 Symlink() bool
27 // SymlinkTarget return a path the symlink is pointing to.
28 SymlinkTarget() (string, error)
29 // Open opens the regular file for reading. Returns error for symlink fi les.
30 Open() (io.ReadCloser, error)
31 }
32
33 // Destination knows how to create files when extracting a package. It supports
34 // transactional semantic by providing 'Begin' and 'End' methods. No changes
35 // should be applied until End(true) is called. A call to End(false) should
36 // discard any pending changes. All paths are slash separated.
37 type Destination interface {
38 // Begin initiates a new write transaction. Called before first CreateFi le.
39 Begin() error
40 // CreateFile opens a writer to extract some package file to. 'name' mus t
41 // be a slash separated path relative to the destination root.
42 CreateFile(name string, executable bool) (io.WriteCloser, error)
43 // CreateSymlink creates a symlink (with absolute or relative target). ' name'
44 // must be a slash separated path relative to the destination root.
45 CreateSymlink(name string, target string) error
46 // End finalizes package extraction (commit or rollback, based on succes s).
47 End(success bool) error
48 }
49
50 ////////////////////////////////////////////////////////////////////////////////
51 // File system source.
52
53 type fileSystemFile struct {
54 absPath string
55 name string
56 size uint64
57 executable bool
58 symlinkTarget string
59 }
60
61 func (f *fileSystemFile) Name() string { return f.name }
62 func (f *fileSystemFile) Size() uint64 { return f.size }
63 func (f *fileSystemFile) Executable() bool { return f.executable }
64 func (f *fileSystemFile) Symlink() bool { return f.symlinkTarget != "" }
65
66 func (f *fileSystemFile) SymlinkTarget() (string, error) {
67 if f.symlinkTarget != "" {
68 return f.symlinkTarget, nil
69 }
70 return "", fmt.Errorf("Not a symlink: %s", f.Name())
71 }
72
73 func (f *fileSystemFile) Open() (io.ReadCloser, error) {
74 if f.Symlink() {
75 return nil, fmt.Errorf("Opening a symlink is not allowed: %s", f .Name())
76 }
77 return os.Open(f.absPath)
78 }
79
80 // ScanFilter is predicate used by ScanFileSystem to decide what files to skip.
81 type ScanFilter func(abs string) bool
82
83 // ScanFileSystem returns all files in some file system directory in
84 // an alphabetical order. It returns only files, skipping directory entries
85 // (i.e. empty directories are completely invisible). ScanFileSystem does not
86 // follow symbolic links, but recognizes them as such (see Symlink() method
87 // of File interface). It scans "dir" path, returning File objects that have
88 // paths relative to "root". It skips files and directories for which
89 // 'exclude(absolute path)' returns true.
90 func ScanFileSystem(dir string, root string, exclude ScanFilter) ([]File, error) {
91 root, err := filepath.Abs(filepath.Clean(root))
92 if err != nil {
93 return nil, err
94 }
95 dir, err = filepath.Abs(filepath.Clean(dir))
96 if err != nil {
97 return nil, err
98 }
99 if !isSubpath(dir, root) {
100 return nil, fmt.Errorf("Scanned directory must be under root dir ectory")
101 }
102
103 files := []File{}
104
105 err = filepath.Walk(dir, func(abs string, info os.FileInfo, err error) e rror {
106 if err != nil {
107 return err
108 }
109 if exclude != nil && abs != dir && exclude(abs) {
110 if info.Mode().IsDir() {
111 return filepath.SkipDir
112 }
113 return nil
114 }
115 if info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
116 f, err := WrapFile(abs, root, &info)
117 if err != nil {
118 return err
119 }
120 files = append(files, f)
121 }
122 return nil
123 })
124
125 if err != nil {
126 return nil, err
127 }
128 return files, nil
129 }
130
131 // WrapFile constructs File object for some file in the file system, specified
132 // by its native absolute path 'abs' (subpath of 'root', also specified as
133 // a native absolute path). Returned File object has path relative to 'root'.
134 // If fileInfo is given, it will be used to grab file mode and size, otherwise
135 // os.Lstat will be used to get it. Recognizes symlinks.
136 func WrapFile(abs string, root string, fileInfo *os.FileInfo) (File, error) {
137 if !filepath.IsAbs(abs) {
138 return nil, fmt.Errorf("Expecting absolute path, got this: %q", abs)
139 }
140 if !filepath.IsAbs(root) {
141 return nil, fmt.Errorf("Expecting absolute path, got this: %q", root)
142 }
143 if !isSubpath(abs, root) {
144 return nil, fmt.Errorf("Path %q is not under %q", abs, root)
145 }
146
147 var info os.FileInfo
148 if fileInfo == nil {
149 // Use Lstat to NOT follow symlinks (as os.Stat does).
150 var err error
151 info, err = os.Lstat(abs)
152 if err != nil {
153 return nil, err
154 }
155 } else {
156 info = *fileInfo
157 }
158
159 rel, err := filepath.Rel(root, abs)
160 if err != nil {
161 return nil, err
162 }
163
164 // Recognize symlinks as such, convert target to relative path, if neede d.
165 if info.Mode()&os.ModeSymlink != 0 {
166 target, err := os.Readlink(abs)
167 if err != nil {
168 return nil, err
169 }
170 if filepath.IsAbs(target) {
171 // Convert absolute path pointing somewhere in "root" in to a path
172 // relative to the symlink file itself. Store other abso lute paths as
173 // they are. For example, it allows to package virtual e nv directory
174 // that symlinks python binary from /usr/bin.
175 if isSubpath(target, root) {
176 target, err = filepath.Rel(filepath.Dir(abs), ta rget)
177 if err != nil {
178 return nil, err
179 }
180 }
181 } else {
182 // Only relative paths that do not go outside "root" are allowed.
183 // A package must not depend on its installation path.
184 targetAbs := filepath.Clean(filepath.Join(filepath.Dir(a bs), target))
185 if !isSubpath(targetAbs, root) {
186 return nil, fmt.Errorf(
187 "Invalid symlink %s: a relative symlink pointing to a file outside of the package root", rel)
188 }
189 }
190 return &fileSystemFile{
191 absPath: abs,
192 name: filepath.ToSlash(rel),
193 symlinkTarget: filepath.ToSlash(target),
194 }, nil
195 }
196
197 // Regular file.
198 if info.Mode().IsRegular() {
199 return &fileSystemFile{
200 absPath: abs,
201 name: filepath.ToSlash(rel),
202 size: uint64(info.Size()),
203 executable: (info.Mode().Perm() & 0111) != 0,
204 }, nil
205 }
206
207 return nil, fmt.Errorf("Not a regular file or symlink: %s", abs)
208 }
209
210 // isSubpath returns true if 'path' is 'root' or is inside a subdirectory of
211 // 'root'. Both 'path' and 'root' should be given as a native paths. If any of
212 // paths can't be converted to an absolute path returns false.
213 func isSubpath(path, root string) bool {
214 path, err := filepath.Abs(filepath.Clean(path))
215 if err != nil {
216 return false
217 }
218 root, err = filepath.Abs(filepath.Clean(root))
219 if err != nil {
220 return false
221 }
222 if root == path {
223 return true
224 }
225 if root[len(root)-1] != filepath.Separator {
226 root += string(filepath.Separator)
227 }
228 return strings.HasPrefix(path, root)
229 }
230
231 ////////////////////////////////////////////////////////////////////////////////
232 // FileSystemDestination implementation.
233
234 type fileSystemDestination struct {
235 // Destination directory.
236 dir string
237 // Root temporary directory.
238 tempDir string
239 // Where to extract all temp files, subdirectory of tempDir.
240 outDir string
241 // Currently open files.
242 openFiles map[string]*os.File
243 }
244
245 // NewFileSystemDestination returns a destination in the file system (directory)
246 // to extract a package to.
247 func NewFileSystemDestination(dir string) Destination {
248 return &fileSystemDestination{
249 dir: dir,
250 openFiles: map[string]*os.File{},
251 }
252 }
253
254 func (d *fileSystemDestination) Begin() (err error) {
255 if d.tempDir != "" {
256 return fmt.Errorf("Destination is already open")
257 }
258
259 // Ensure parent directory of destination directory exists.
260 d.dir, err = filepath.Abs(filepath.Clean(d.dir))
261 if err != nil {
262 return err
263 }
264 err = os.MkdirAll(filepath.Dir(d.dir), 0777)
265 if err != nil {
266 return err
267 }
268
269 // Called in case something below fails.
270 cleanup := func() {
271 if d.tempDir != "" {
272 os.RemoveAll(d.tempDir)
273 }
274 d.tempDir = ""
275 d.outDir = ""
276 }
277
278 // Create root temp dir, on the same level as destination directory.
279 d.tempDir, err = ioutil.TempDir(filepath.Dir(d.dir), filepath.Base(d.dir )+"_")
280 if err != nil {
281 cleanup()
282 return err
283 }
284
285 // Create a staging output directory where everything will be extracted.
286 d.outDir = filepath.Join(d.tempDir, "out")
287 err = os.MkdirAll(d.outDir, 0777)
288 if err != nil {
289 cleanup()
290 return err
291 }
292
293 return nil
294 }
295
296 func (d *fileSystemDestination) CreateFile(name string, executable bool) (io.Wri teCloser, error) {
297 _, ok := d.openFiles[name]
298 if ok {
299 return nil, fmt.Errorf("File %s is already open", name)
300 }
301
302 path, err := d.prepareFilePath(name)
303 if err != nil {
304 return nil, err
305 }
306
307 // Let the umask trim the file mode. Do not set 'writable' bit though.
308 var mode os.FileMode
309 if executable {
310 mode = 0555
311 } else {
312 mode = 0444
313 }
314
315 file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_EXCL, mode)
316 if err != nil {
317 return nil, err
318 }
319 d.openFiles[name] = file
320 return &fileSystemDestinationFile{
321 nested: file,
322 parent: d,
323 closeCallback: func() {
324 delete(d.openFiles, name)
325 },
326 }, nil
327 }
328
329 func (d *fileSystemDestination) CreateSymlink(name string, target string) error {
330 path, err := d.prepareFilePath(name)
331 if err != nil {
332 return err
333 }
334
335 // Forbid relative symlinks to files outside of the destination root.
336 target = filepath.FromSlash(target)
337 if !filepath.IsAbs(target) {
338 targetAbs := filepath.Clean(filepath.Join(filepath.Dir(path), ta rget))
339 if !isSubpath(targetAbs, d.outDir) {
340 return fmt.Errorf("Relative symlink is pointing outside of the destination dir: %s", name)
341 }
342 }
343
344 return os.Symlink(target, path)
345 }
346
347 func (d *fileSystemDestination) End(success bool) error {
348 if d.tempDir == "" {
349 return fmt.Errorf("Destination is not open")
350 }
351 if len(d.openFiles) != 0 {
352 return fmt.Errorf("Not all files were closed. Leaking.")
353 }
354
355 // Clean up temp dir and the state no matter what.
356 defer func() {
357 os.RemoveAll(d.tempDir)
358 d.tempDir = ""
359 d.outDir = ""
360 }()
361
362 if success {
363 // Move existing directory away, if it is there.
364 old := filepath.Join(d.tempDir, "old")
365 if os.Rename(d.dir, old) != nil {
366 old = ""
367 }
368
369 // Move new directory in place.
370 err := os.Rename(d.outDir, d.dir)
371 if err != nil {
372 // Try to return the original directory back...
373 if old != "" {
374 os.Rename(old, d.dir)
375 }
376 return err
377 }
378 }
379
380 return nil
381 }
382
383 // prepareFilePath performs steps common to CreateFile and CreateSymlink: it
384 // does some validation, expands "name" to an absolute path and creates parent
385 // directories for a future file. Returns absolute path where the file should
386 // be put.
387 func (d *fileSystemDestination) prepareFilePath(name string) (string, error) {
388 if d.tempDir == "" {
389 return "", fmt.Errorf("Destination is not open")
390 }
391 path := filepath.Clean(filepath.Join(d.outDir, filepath.FromSlash(name)) )
392 if !isSubpath(path, d.outDir) {
393 return "", fmt.Errorf("Invalid relative file name: %s", name)
394 }
395 err := os.MkdirAll(filepath.Dir(path), 0777)
396 if err != nil {
397 return "", err
398 }
399 return path, nil
400 }
401
402 type fileSystemDestinationFile struct {
403 nested io.WriteCloser
404 parent *fileSystemDestination
405 closeCallback func()
406 }
407
408 func (f *fileSystemDestinationFile) Write(p []byte) (n int, err error) {
409 return f.nested.Write(p)
410 }
411
412 func (f *fileSystemDestinationFile) Close() error {
413 f.closeCallback()
414 return f.nested.Close()
415 }
OLDNEW
« no previous file with comments | « go/src/infra/tools/cipd/fetcher_test.go ('k') | go/src/infra/tools/cipd/files_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698