| 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)
|
| +}
|
|
|