Index: go/src/infra/tools/cipd/local/fs.go |
diff --git a/go/src/infra/tools/cipd/local/fs.go b/go/src/infra/tools/cipd/local/fs.go |
new file mode 100644 |
index 0000000000000000000000000000000000000000..e25f9d8e6bdd412f219fb93b1598f9d62fb57f36 |
--- /dev/null |
+++ b/go/src/infra/tools/cipd/local/fs.go |
@@ -0,0 +1,262 @@ |
+// Copyright 2015 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package local |
+ |
+import ( |
+ "fmt" |
+ "os" |
+ "path/filepath" |
+ "strings" |
+ "sync" |
+ "time" |
+ |
+ "github.com/luci/luci-go/common/logging" |
+) |
+ |
+// FileSystem is a high-level interface for operations that touch single file |
+// system subpath. All functions operate in terms of native file paths. It |
+// exists mostly to hide differences between file system semantic on Windows and |
+// Linux\Mac. |
+type FileSystem interface { |
+ // Root returns absolute path to a directory FileSystem operates in. All FS |
+ // actions are restricted to this directory. |
+ Root() string |
+ |
+ // ToAbsPath converts a relative path to an absolute one and verifies it is |
+ // under the root path of the FileSystem object. |
+ ToAbsPath(path string) (string, error) |
+ |
+ // EnsureDirectory creates a directory at given native path if it doesn't |
+ // exist yet. It takes an absolute path or a path relative to the current |
+ // working directory and always returns absolute path. |
+ EnsureDirectory(path string) (string, error) |
+ |
+ // EnsureSymlink creates a symlink pointing to a target. It will create full |
+ // directory path to the symlink if necessary. |
+ EnsureSymlink(path string, target string) error |
+ |
+ // EnsureFileGone removes a file, logging the errors (if any). Missing file is |
+ // not an error. |
+ EnsureFileGone(path string) error |
+ |
+ // EnsureDirectoryGone recursively removes a directory. |
+ EnsureDirectoryGone(path string) error |
+ |
+ // Renames oldpath to newpath. If newpath already exists (be it a file or a |
+ // directory), removes it first. |
+ Replace(oldpath, newpath string) error |
+} |
+ |
+// NewFileSystem returns default FileSystem implementation that operates with |
+// files under a given path. All methods accept absolute paths or paths relative |
+// to current working directory. FileSystem will ensure they are under 'root' |
+// directory. |
+func NewFileSystem(root string, logger logging.Logger) FileSystem { |
+ if logger == nil { |
+ logger = logging.Null() |
+ } |
+ abs, err := filepath.Abs(root) |
+ if err != nil { |
+ return &fsImplErr{err} |
+ } |
+ return &fsImpl{abs, logger} |
+} |
+ |
+// fsImplErr implements FileSystem by returning given error from all methods. |
+type fsImplErr struct { |
+ err error |
+} |
+ |
+// Root returns absolute path to a directory FileSystem operates in. All FS |
+// actions are restricted to this directory. |
+func (f *fsImplErr) Root() string { return "" } |
+func (f *fsImplErr) ToAbsPath(path string) (string, error) { return "", f.err } |
+func (f *fsImplErr) EnsureDirectory(path string) (string, error) { return "", f.err } |
+func (f *fsImplErr) EnsureSymlink(path string, target string) error { return f.err } |
+func (f *fsImplErr) EnsureFileGone(path string) error { return f.err } |
+func (f *fsImplErr) EnsureDirectoryGone(path string) error { return f.err } |
+func (f *fsImplErr) Replace(oldpath, newpath string) error { return f.err } |
+ |
+/// Implementation. |
+ |
+type fsImpl struct { |
+ root string |
+ logger logging.Logger |
+} |
+ |
+func (f *fsImpl) Root() string { |
+ return f.root |
+} |
+ |
+func (f *fsImpl) ToAbsPath(p string) (string, error) { |
+ p, err := filepath.Abs(p) |
+ if err != nil { |
+ return "", err |
+ } |
+ rel, err := filepath.Rel(f.root, p) |
+ if err != nil { |
+ return "", err |
+ } |
+ rel = filepath.ToSlash(rel) |
+ if rel == ".." || strings.HasPrefix(rel, "../") { |
+ return "", fmt.Errorf("fs: path %s is outside of %s", p, f.root) |
+ } |
+ return p, nil |
+} |
+ |
+func (f *fsImpl) EnsureDirectory(path string) (string, error) { |
+ path, err := f.ToAbsPath(path) |
+ if err != nil { |
+ return "", err |
+ } |
+ // MkdirAll returns nil if path already exists. |
+ if err = os.MkdirAll(path, 0777); err != nil { |
+ return "", err |
+ } |
+ return path, nil |
+} |
+ |
+func (f *fsImpl) EnsureSymlink(path string, target string) error { |
+ path, err := f.ToAbsPath(path) |
+ if err != nil { |
+ return err |
+ } |
+ if existing, _ := os.Readlink(path); existing == target { |
+ return nil |
+ } |
+ if _, err := f.EnsureDirectory(filepath.Dir(path)); err != nil { |
+ return err |
+ } |
+ |
+ // Create a new symlink file, can't modify existing one in place. |
+ temp := fmt.Sprintf("%s_%s", path, pseudoRand()) |
+ if err := os.Symlink(target, temp); err != nil { |
+ return err |
+ } |
+ |
+ // Atomically replace the current symlink with a new one. |
+ if err := os.Rename(temp, path); err != nil { |
+ err2 := os.Remove(temp) |
+ if err2 != nil { |
+ f.logger.Warningf("fs: failed to remove %s - %s", temp, err2) |
+ } |
+ return err |
+ } |
+ |
+ return nil |
+} |
+ |
+func (f *fsImpl) EnsureFileGone(path string) error { |
+ path, err := f.ToAbsPath(path) |
+ if err != nil { |
+ return err |
+ } |
+ err = os.Remove(path) |
+ if err != nil && !os.IsNotExist(err) { |
+ f.logger.Warningf("fs: failed to remove %s - %s", path, err) |
+ return err |
+ } |
+ return nil |
+} |
+ |
+func (f *fsImpl) EnsureDirectoryGone(path string) error { |
+ path, err := f.ToAbsPath(path) |
+ if err != nil { |
+ return err |
+ } |
+ // Make directory "disappear" instantly by renaming it first. |
+ temp := fmt.Sprintf("%s_%v", path, pseudoRand()) |
+ if err = os.Rename(path, temp); err != nil { |
+ if os.IsNotExist(err) { |
+ return nil |
+ } |
+ f.logger.Warningf("fs: failed to rename directory %s - %s", path, err) |
+ return err |
+ } |
+ if err = os.RemoveAll(temp); err != nil { |
+ f.logger.Warningf("fs: failed to remove directory %s - %s", temp, err) |
+ return err |
+ } |
+ return nil |
+} |
+ |
+func (f *fsImpl) Replace(oldpath, newpath string) error { |
+ oldpath, err := f.ToAbsPath(oldpath) |
+ if err != nil { |
+ return err |
+ } |
+ newpath, err = f.ToAbsPath(newpath) |
+ if err != nil { |
+ return err |
+ } |
+ if oldpath == newpath { |
+ return nil |
+ } |
+ |
+ // Make sure oldpath exists before doing heavy stuff. |
+ if _, err = os.Stat(oldpath); err != nil { |
+ return err |
+ } |
+ |
+ // Make parent directory of newpath. |
+ if _, err = f.EnsureDirectory(filepath.Dir(newpath)); err != nil { |
+ return err |
+ } |
+ |
+ // Try a regular move first. Replaces files and empty directories. |
+ if err = os.Rename(oldpath, newpath); err == nil { |
+ return nil |
+ } |
+ |
+ // Move existing path away, if it is there. |
+ temp := fmt.Sprintf("%s_%s", newpath, pseudoRand()) |
+ if err = os.Rename(newpath, temp); err != nil { |
+ if !os.IsNotExist(err) { |
+ f.logger.Warningf("fs: failed to rename(%v, %v) - %s", newpath, temp, err) |
+ return err |
+ } |
+ temp = "" |
+ } |
+ |
+ // 'newpath' now should be available. |
+ if err := os.Rename(oldpath, newpath); err != nil { |
+ f.logger.Warningf("fs: failed to rename(%v, %v) - %s", oldpath, newpath, err) |
+ // Try to return the path back... May be too late already. |
+ if temp != "" { |
+ if err := os.Rename(temp, newpath); err != nil { |
+ f.logger.Errorf("fs: failed to rename(%v, %v) after unsuccessful move - %s", temp, newpath, err) |
+ } |
+ } |
+ return err |
+ } |
+ |
+ // Cleanup the garbage left. Not a error if fails. |
+ if temp != "" { |
+ if err := f.EnsureDirectoryGone(temp); err != nil { |
+ f.logger.Warningf("fs: failed to cleanup garbage after file replace - %s", err) |
+ } |
+ } |
+ return nil |
+} |
+ |
+/// Internal stuff. |
+ |
+var ( |
+ lastUsedTime int64 |
+ lastUsedTimeLock sync.Mutex |
+) |
+ |
+// pseudoRand returns "random enough" string that can be used in file system |
+// paths of temp files. |
+func pseudoRand() string { |
+ ts := time.Now().UnixNano() |
+ lastUsedTimeLock.Lock() |
+ if ts <= lastUsedTime { |
+ ts = lastUsedTime + 1 |
+ } |
+ lastUsedTime = ts |
+ lastUsedTimeLock.Unlock() |
+ return fmt.Sprintf("%v_%v", os.Getpid(), ts) |
+} |