David K. Bainbridge | 528b318 | 2017-01-23 08:51:59 -0800 | [diff] [blame] | 1 | // Copyright 2013 Canonical Ltd. |
| 2 | // Licensed under the LGPLv3, see LICENCE file for details. |
| 3 | |
| 4 | package utils |
| 5 | |
| 6 | import ( |
| 7 | "fmt" |
| 8 | "io" |
| 9 | "io/ioutil" |
| 10 | "os" |
| 11 | "path" |
| 12 | "path/filepath" |
| 13 | "regexp" |
| 14 | ) |
| 15 | |
| 16 | // UserHomeDir returns the home directory for the specified user, or the |
| 17 | // home directory for the current user if the specified user is empty. |
| 18 | func UserHomeDir(userName string) (hDir string, err error) { |
| 19 | if userName == "" { |
| 20 | // TODO (wallyworld) - fix tests on Windows |
| 21 | // Ordinarily, we'd always use user.Current() to get the current user |
| 22 | // and then get the HomeDir from that. But our tests rely on poking |
| 23 | // a value into $HOME in order to override the normal home dir for the |
| 24 | // current user. So we're forced to use Home() to make the tests pass. |
| 25 | // All of our tests currently construct paths with the default user in |
| 26 | // mind eg "~/foo". |
| 27 | return Home(), nil |
| 28 | } |
| 29 | hDir, err = homeDir(userName) |
| 30 | if err != nil { |
| 31 | return "", err |
| 32 | } |
| 33 | return hDir, nil |
| 34 | } |
| 35 | |
| 36 | // Only match paths starting with ~ (~user/test, ~/test). This will prevent |
| 37 | // accidental expansion on Windows when short form paths are present (C:\users\ADMINI~1\test) |
| 38 | var userHomePathRegexp = regexp.MustCompile("(^~(?P<user>[^/]*))(?P<path>.*)") |
| 39 | |
| 40 | // NormalizePath expands a path containing ~ to its absolute form, |
| 41 | // and removes any .. or . path elements. |
| 42 | func NormalizePath(dir string) (string, error) { |
| 43 | if userHomePathRegexp.MatchString(dir) { |
| 44 | user := userHomePathRegexp.ReplaceAllString(dir, "$user") |
| 45 | userHomeDir, err := UserHomeDir(user) |
| 46 | if err != nil { |
| 47 | return "", err |
| 48 | } |
| 49 | dir = userHomePathRegexp.ReplaceAllString(dir, fmt.Sprintf("%s$path", userHomeDir)) |
| 50 | } |
| 51 | return filepath.Clean(dir), nil |
| 52 | } |
| 53 | |
| 54 | // EnsureBaseDir ensures that path is always prefixed by baseDir, |
| 55 | // allowing for the fact that path might have a Window drive letter in |
| 56 | // it. |
| 57 | func EnsureBaseDir(baseDir, path string) string { |
| 58 | if baseDir == "" { |
| 59 | return path |
| 60 | } |
| 61 | volume := filepath.VolumeName(path) |
| 62 | return filepath.Join(baseDir, path[len(volume):]) |
| 63 | } |
| 64 | |
| 65 | // JoinServerPath joins any number of path elements into a single path, adding |
| 66 | // a path separator (based on the current juju server OS) if necessary. The |
| 67 | // result is Cleaned; in particular, all empty strings are ignored. |
| 68 | func JoinServerPath(elem ...string) string { |
| 69 | return path.Join(elem...) |
| 70 | } |
| 71 | |
| 72 | // UniqueDirectory returns "path/name" if that directory doesn't exist. If it |
| 73 | // does, the method starts appending .1, .2, etc until a unique name is found. |
| 74 | func UniqueDirectory(path, name string) (string, error) { |
| 75 | dir := filepath.Join(path, name) |
| 76 | _, err := os.Stat(dir) |
| 77 | if os.IsNotExist(err) { |
| 78 | return dir, nil |
| 79 | } |
| 80 | for i := 1; ; i++ { |
| 81 | dir := filepath.Join(path, fmt.Sprintf("%s.%d", name, i)) |
| 82 | _, err := os.Stat(dir) |
| 83 | if os.IsNotExist(err) { |
| 84 | return dir, nil |
| 85 | } else if err != nil { |
| 86 | return "", err |
| 87 | } |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | // CopyFile writes the contents of the given source file to dest. |
| 92 | func CopyFile(dest, source string) error { |
| 93 | df, err := os.Create(dest) |
| 94 | if err != nil { |
| 95 | return err |
| 96 | } |
| 97 | f, err := os.Open(source) |
| 98 | if err != nil { |
| 99 | return err |
| 100 | } |
| 101 | defer f.Close() |
| 102 | _, err = io.Copy(df, f) |
| 103 | return err |
| 104 | } |
| 105 | |
| 106 | // AtomicWriteFileAndChange atomically writes the filename with the |
| 107 | // given contents and calls the given function after the contents were |
| 108 | // written, but before the file is renamed. |
| 109 | func AtomicWriteFileAndChange(filename string, contents []byte, change func(*os.File) error) (err error) { |
| 110 | dir, file := filepath.Split(filename) |
| 111 | f, err := ioutil.TempFile(dir, file) |
| 112 | if err != nil { |
| 113 | return fmt.Errorf("cannot create temp file: %v", err) |
| 114 | } |
| 115 | defer f.Close() |
| 116 | defer func() { |
| 117 | if err != nil { |
| 118 | // Don't leave the temp file lying around on error. |
| 119 | // Close the file before removing. Trying to remove an open file on |
| 120 | // Windows will fail. |
| 121 | f.Close() |
| 122 | os.Remove(f.Name()) |
| 123 | } |
| 124 | }() |
| 125 | if _, err := f.Write(contents); err != nil { |
| 126 | return fmt.Errorf("cannot write %q contents: %v", filename, err) |
| 127 | } |
| 128 | if err := change(f); err != nil { |
| 129 | return err |
| 130 | } |
| 131 | f.Close() |
| 132 | if err := ReplaceFile(f.Name(), filename); err != nil { |
| 133 | return fmt.Errorf("cannot replace %q with %q: %v", f.Name(), filename, err) |
| 134 | } |
| 135 | return nil |
| 136 | } |
| 137 | |
| 138 | // AtomicWriteFile atomically writes the filename with the given |
| 139 | // contents and permissions, replacing any existing file at the same |
| 140 | // path. |
| 141 | func AtomicWriteFile(filename string, contents []byte, perms os.FileMode) (err error) { |
| 142 | return AtomicWriteFileAndChange(filename, contents, func(f *os.File) error { |
| 143 | // FileMod.Chmod() is not implemented on Windows, however, os.Chmod() is |
| 144 | if err := os.Chmod(f.Name(), perms); err != nil { |
| 145 | return fmt.Errorf("cannot set permissions: %v", err) |
| 146 | } |
| 147 | return nil |
| 148 | }) |
| 149 | } |