github.com/joshdk/godel@v0.0.0-20170529232908-862138a45aee/layout/operations.go (about) 1 // Copyright 2016 Palantir Technologies, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package layout 16 17 import ( 18 "crypto/sha256" 19 "encoding/hex" 20 "io" 21 "io/ioutil" 22 "os" 23 "path" 24 25 "github.com/pkg/errors" 26 ) 27 28 // Move the file or directory at src to dst. Conceptually, this is equivalent to executing "mv src dst". dst must not 29 // already exist and the path up to it must already exist. Uses os.Rename to execute the move, which means that there 30 // may be platform-specific restrictions such as not being able to move the directory between different volumes. 31 func Move(src, dst string) error { 32 if err := verifyDstPathSafe(dst); err != nil { 33 return errors.Wrapf(err, "Cannot move directory to path %s", dst) 34 } 35 if err := os.Rename(src, dst); err != nil { 36 return errors.Wrapf(err, "Failed to rename %s to %s", src, dst) 37 } 38 return nil 39 } 40 41 // CopyDir recursively copies the src directory to the path specified by dst. dst must not already exist and the path up 42 // to it must already exist. 43 func CopyDir(src, dst string) error { 44 if err := verifyDstPathSafe(dst); err != nil { 45 return errors.Wrapf(err, "Cannot copy directory to path %s", dst) 46 } 47 48 srcInfo, err := os.Stat(src) 49 if err != nil { 50 return errors.Wrapf(err, "Failed to stat source directory %s", src) 51 } 52 53 if err := os.Mkdir(dst, srcInfo.Mode()); err != nil { 54 return errors.Wrapf(err, "Failed to create destination directory %s", dst) 55 } 56 57 files, err := ioutil.ReadDir(src) 58 if err != nil { 59 return errors.Wrapf(err, "Failed to read directory %s", src) 60 } 61 62 for _, f := range files { 63 srcPath := path.Join(src, f.Name()) 64 dstPath := path.Join(dst, f.Name()) 65 66 if f.IsDir() { 67 err = CopyDir(srcPath, dstPath) 68 } else { 69 err = CopyFile(srcPath, dstPath) 70 } 71 72 if err != nil { 73 return errors.Wrapf(err, "Failed to copy %s to %s", srcPath, dstPath) 74 } 75 } 76 77 return nil 78 } 79 80 // CopyFile copies the file at src to dst. src must specify a regular file (non-directory, no special mode) that exists, 81 // and dst must specify a path that does not yet exist, but whose parent directory does exist. The copied file will have 82 // the same permissions as the original. 83 func CopyFile(src, dst string) (rErr error) { 84 srcInfo, err := os.Stat(src) 85 if err != nil { 86 return errors.Wrapf(err, "Failed to stat source file %s", src) 87 } 88 if !srcInfo.Mode().IsRegular() { 89 return errors.Wrapf(err, "Source file %s is not a regular file, had mode: %v", src, srcInfo.Mode()) 90 } 91 srcPerms := srcInfo.Mode().Perm() 92 93 srcFile, err := os.Open(src) 94 if err != nil { 95 return errors.Wrapf(err, "Failed to open %s", src) 96 } 97 defer func() { 98 if err := srcFile.Close(); err != nil { 99 rErr = errors.Wrapf(err, "failed to close %s in defer", src) 100 } 101 }() 102 103 if err := verifyDstPathSafe(dst); err != nil { 104 return errors.Wrapf(err, "cannot copy to destination path %s", dst) 105 } 106 107 dstFile, err := os.Create(dst) 108 if err != nil { 109 return errors.Wrapf(err, "failed to open %s", dst) 110 } 111 defer func() { 112 if err := dstFile.Close(); err != nil { 113 rErr = errors.Wrapf(err, "failed to close %s in defer") 114 } 115 }() 116 117 if _, err := io.Copy(dstFile, srcFile); err != nil { 118 return errors.Wrapf(err, "failed to copy %s to %s", src, dst) 119 } 120 121 if err := dstFile.Chmod(srcPerms); err != nil { 122 return errors.Wrapf(err, "failed to chmod %s to have permissions %v", dst, srcPerms) 123 } 124 125 return nil 126 } 127 128 // SyncDir syncs the contents of the provided directories such that the content of dstDir matches that of srcDir (except 129 // for files that match a name in the provided skip slice). 130 func SyncDir(srcDir, dstDir string, skip []string) (bool, error) { 131 modified := false 132 133 srcFiles, err := ioutil.ReadDir(srcDir) 134 if err != nil { 135 return modified, errors.Wrapf(err, "failed to read directory %s", srcDir) 136 } 137 srcFilesMap := toMap(srcFiles) 138 139 dstFiles, err := ioutil.ReadDir(dstDir) 140 if err != nil { 141 return modified, errors.Wrapf(err, "failed to read directory %s", dstDir) 142 } 143 dstFilesMap := toMap(dstFiles) 144 145 skipSet := toSet(skip) 146 for dstFileName, dstFileInfo := range dstFilesMap { 147 if _, ok := skipSet[dstFileName]; ok { 148 // skip if file name is in skip list 149 continue 150 } 151 152 remove := false 153 srcFilePath := path.Join(srcDir, dstFileName) 154 dstFilePath := path.Join(dstDir, dstFileName) 155 156 if currSrcFileInfo, ok := srcFilesMap[dstFileName]; !ok { 157 // if dst exists but src does not, remove dst 158 remove = true 159 } else if dstFileInfo.IsDir() != currSrcFileInfo.IsDir() { 160 // if dst file and src file are different types, remove dst 161 remove = true 162 } else if !dstFileInfo.IsDir() { 163 srcChecksum, err := checksum(srcFilePath) 164 if err != nil { 165 return modified, errors.Wrapf(err, "failed to compute checksum for %s", srcFilePath) 166 } 167 dstChecksum, err := checksum(dstFilePath) 168 if err != nil { 169 return modified, errors.Wrapf(err, "failed to compute checksum for %s", dstFilePath) 170 } 171 172 // if dst and src are both files and their checksums differ, remove dst 173 if srcChecksum != dstChecksum { 174 remove = true 175 } 176 } else { 177 // if dst and src both exist and are both directories, sync them recursively 178 recursiveModified, err := SyncDir(srcFilePath, dstFilePath, skip) 179 if err != nil { 180 return modified, errors.Wrapf(err, "failed to sync %s with %s", dstFilePath, srcFilePath) 181 } 182 modified = modified || recursiveModified 183 } 184 185 // remove path if it was marked for removal 186 if remove { 187 if err := os.RemoveAll(dstFilePath); err != nil { 188 return modified, errors.Wrapf(err, "failed to remove %s", dstFilePath) 189 } 190 modified = true 191 } 192 } 193 194 for srcFileName, srcFileInfo := range srcFilesMap { 195 if _, ok := skipSet[srcFileName]; ok { 196 // skip if file name is in skip list 197 continue 198 } 199 200 srcFilePath := path.Join(srcDir, srcFileName) 201 dstFilePath := path.Join(dstDir, srcFileName) 202 203 if _, err := os.Stat(dstFilePath); os.IsNotExist(err) { 204 // if path does not exist at destination, copy source version 205 var err error 206 if srcFileInfo.IsDir() { 207 err = CopyDir(srcFilePath, dstFilePath) 208 } else { 209 err = CopyFile(srcFilePath, dstFilePath) 210 } 211 if err != nil { 212 return modified, errors.Wrapf(err, "failed to copy %s to %s", srcFilePath, dstFilePath) 213 } 214 modified = true 215 } else if err != nil { 216 return modified, errors.Wrapf(err, "failed to stat %s", dstFilePath) 217 } 218 } 219 220 return modified, nil 221 } 222 223 func toSet(input []string) map[string]struct{} { 224 s := make(map[string]struct{}, len(input)) 225 for _, curr := range input { 226 s[curr] = struct{}{} 227 } 228 return s 229 } 230 231 func toMap(input []os.FileInfo) map[string]os.FileInfo { 232 m := make(map[string]os.FileInfo, len(input)) 233 for _, curr := range input { 234 m[curr.Name()] = curr 235 } 236 return m 237 } 238 239 func checksum(p string) (string, error) { 240 f, err := os.Open(p) 241 if err != nil { 242 return "", errors.Wrapf(err, "failed to open %s", p) 243 } 244 h := sha256.New() 245 if _, err := io.Copy(h, f); err != nil { 246 return "", errors.Wrapf(err, "failed to copy file to hash buffer") 247 } 248 return hex.EncodeToString(h.Sum(nil)), nil 249 } 250 251 // SyncDirAdditive copies all of the files and directories in src that are not in dst. Directories that are present in 252 // both are handled recursively. Basically a recursive merge with source preservation. 253 func SyncDirAdditive(src, dst string) error { 254 srcInfos, err := ioutil.ReadDir(src) 255 if err != nil { 256 return errors.Wrapf(err, "failed to open %s", src) 257 } 258 259 for _, srcInfo := range srcInfos { 260 srcPath := path.Join(src, srcInfo.Name()) 261 dstPath := path.Join(dst, srcInfo.Name()) 262 263 if dstInfo, err := os.Stat(dstPath); os.IsNotExist(err) { 264 // safe to copy 265 if srcInfo.IsDir() { 266 err = CopyDir(srcPath, dstPath) 267 } else { 268 err = CopyFile(srcPath, dstPath) 269 } 270 if err != nil { 271 return errors.Wrapf(err, "failed to copy %s to %s", srcPath, dstPath) 272 } 273 } else if err != nil { 274 return errors.Wrapf(err, "failed to stat %s", dstPath) 275 } else if srcInfo.IsDir() && dstInfo.IsDir() { 276 // if source and destination are both directories, sync recursively 277 if err = SyncDirAdditive(srcPath, dstPath); err != nil { 278 return errors.Wrapf(err, "failed to sync %s to %s", srcPath, dstPath) 279 } 280 } 281 } 282 return nil 283 } 284 285 func VerifyDirExists(dir string) error { 286 return verifyPath(dir, path.Base(dir), true, false) 287 } 288 289 func verifyPath(p, expectedName string, isDir bool, optional bool) error { 290 if path.Base(p) != expectedName { 291 return errors.Errorf("%s is not a path to %s", p, expectedName) 292 } 293 294 if fi, err := os.Stat(p); err != nil { 295 if os.IsNotExist(err) { 296 if !optional { 297 return errors.Wrapf(err, "%s does not exist", p) 298 } 299 // path does not exist, but it is optional so is okay 300 return nil 301 } 302 return errors.Wrapf(err, "failed to stat %s", p) 303 } else if currIsDir := fi.IsDir(); currIsDir != isDir { 304 return errors.Errorf("IsDir for %s returned wrong value: expected %v, was %v", p, isDir, currIsDir) 305 } 306 307 return nil 308 } 309 310 // verifyDstPathSafe verifies that the provided destination path does not exist, but that the path to its parent does. 311 func verifyDstPathSafe(dst string) error { 312 if _, err := os.Stat(dst); !os.IsNotExist(err) { 313 return errors.Wrapf(err, "Destination path %s already exists", dst) 314 } 315 if _, err := os.Stat(path.Dir(dst)); os.IsNotExist(err) { 316 return errors.Wrapf(err, "Parent directory of destination path %s does not exist", dst) 317 } 318 return nil 319 }