github.com/miqui/docker@v1.9.1/pkg/archive/copy.go (about) 1 package archive 2 3 import ( 4 "archive/tar" 5 "errors" 6 "io" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "strings" 11 12 "github.com/Sirupsen/logrus" 13 "github.com/docker/docker/pkg/system" 14 ) 15 16 // Errors used or returned by this file. 17 var ( 18 ErrNotDirectory = errors.New("not a directory") 19 ErrDirNotExists = errors.New("no such directory") 20 ErrCannotCopyDir = errors.New("cannot copy directory") 21 ErrInvalidCopySource = errors.New("invalid copy source content") 22 ) 23 24 // PreserveTrailingDotOrSeparator returns the given cleaned path (after 25 // processing using any utility functions from the path or filepath stdlib 26 // packages) and appends a trailing `/.` or `/` if its corresponding original 27 // path (from before being processed by utility functions from the path or 28 // filepath stdlib packages) ends with a trailing `/.` or `/`. If the cleaned 29 // path already ends in a `.` path segment, then another is not added. If the 30 // clean path already ends in a path separator, then another is not added. 31 func PreserveTrailingDotOrSeparator(cleanedPath, originalPath string) string { 32 // Ensure paths are in platform semantics 33 cleanedPath = normalizePath(cleanedPath) 34 originalPath = normalizePath(originalPath) 35 36 if !specifiesCurrentDir(cleanedPath) && specifiesCurrentDir(originalPath) { 37 if !hasTrailingPathSeparator(cleanedPath) { 38 // Add a separator if it doesn't already end with one (a cleaned 39 // path would only end in a separator if it is the root). 40 cleanedPath += string(filepath.Separator) 41 } 42 cleanedPath += "." 43 } 44 45 if !hasTrailingPathSeparator(cleanedPath) && hasTrailingPathSeparator(originalPath) { 46 cleanedPath += string(filepath.Separator) 47 } 48 49 return cleanedPath 50 } 51 52 // assertsDirectory returns whether the given path is 53 // asserted to be a directory, i.e., the path ends with 54 // a trailing '/' or `/.`, assuming a path separator of `/`. 55 func assertsDirectory(path string) bool { 56 return hasTrailingPathSeparator(path) || specifiesCurrentDir(path) 57 } 58 59 // hasTrailingPathSeparator returns whether the given 60 // path ends with the system's path separator character. 61 func hasTrailingPathSeparator(path string) bool { 62 return len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) 63 } 64 65 // specifiesCurrentDir returns whether the given path specifies 66 // a "current directory", i.e., the last path segment is `.`. 67 func specifiesCurrentDir(path string) bool { 68 return filepath.Base(path) == "." 69 } 70 71 // SplitPathDirEntry splits the given path between its directory name and its 72 // basename by first cleaning the path but preserves a trailing "." if the 73 // original path specified the current directory. 74 func SplitPathDirEntry(path string) (dir, base string) { 75 cleanedPath := filepath.Clean(normalizePath(path)) 76 77 if specifiesCurrentDir(path) { 78 cleanedPath += string(filepath.Separator) + "." 79 } 80 81 return filepath.Dir(cleanedPath), filepath.Base(cleanedPath) 82 } 83 84 // TarResource archives the resource described by the given CopyInfo to a Tar 85 // archive. A non-nil error is returned if sourcePath does not exist or is 86 // asserted to be a directory but exists as another type of file. 87 // 88 // This function acts as a convenient wrapper around TarWithOptions, which 89 // requires a directory as the source path. TarResource accepts either a 90 // directory or a file path and correctly sets the Tar options. 91 func TarResource(sourceInfo CopyInfo) (content Archive, err error) { 92 return TarResourceRebase(sourceInfo.Path, sourceInfo.RebaseName) 93 } 94 95 // TarResourceRebase is like TarResource but renames the first path element of 96 // items in the resulting tar archive to match the given rebaseName if not "". 97 func TarResourceRebase(sourcePath, rebaseName string) (content Archive, err error) { 98 sourcePath = normalizePath(sourcePath) 99 if _, err = os.Lstat(sourcePath); err != nil { 100 // Catches the case where the source does not exist or is not a 101 // directory if asserted to be a directory, as this also causes an 102 // error. 103 return 104 } 105 106 // Separate the source path between it's directory and 107 // the entry in that directory which we are archiving. 108 sourceDir, sourceBase := SplitPathDirEntry(sourcePath) 109 110 filter := []string{sourceBase} 111 112 logrus.Debugf("copying %q from %q", sourceBase, sourceDir) 113 114 return TarWithOptions(sourceDir, &TarOptions{ 115 Compression: Uncompressed, 116 IncludeFiles: filter, 117 IncludeSourceDir: true, 118 RebaseNames: map[string]string{ 119 sourceBase: rebaseName, 120 }, 121 }) 122 } 123 124 // CopyInfo holds basic info about the source 125 // or destination path of a copy operation. 126 type CopyInfo struct { 127 Path string 128 Exists bool 129 IsDir bool 130 RebaseName string 131 } 132 133 // CopyInfoSourcePath stats the given path to create a CopyInfo 134 // struct representing that resource for the source of an archive copy 135 // operation. The given path should be an absolute local path. A source path 136 // has all symlinks evaluated that appear before the last path separator ("/" 137 // on Unix). As it is to be a copy source, the path must exist. 138 func CopyInfoSourcePath(path string) (CopyInfo, error) { 139 // Split the given path into its Directory and Base components. We will 140 // evaluate symlinks in the directory component then append the base. 141 path = normalizePath(path) 142 dirPath, basePath := filepath.Split(path) 143 144 resolvedDirPath, err := filepath.EvalSymlinks(dirPath) 145 if err != nil { 146 return CopyInfo{}, err 147 } 148 149 // resolvedDirPath will have been cleaned (no trailing path separators) so 150 // we can manually join it with the base path element. 151 resolvedPath := resolvedDirPath + string(filepath.Separator) + basePath 152 153 var rebaseName string 154 if hasTrailingPathSeparator(path) && filepath.Base(path) != filepath.Base(resolvedPath) { 155 // In the case where the path had a trailing separator and a symlink 156 // evaluation has changed the last path component, we will need to 157 // rebase the name in the archive that is being copied to match the 158 // originally requested name. 159 rebaseName = filepath.Base(path) 160 } 161 162 stat, err := os.Lstat(resolvedPath) 163 if err != nil { 164 return CopyInfo{}, err 165 } 166 167 return CopyInfo{ 168 Path: resolvedPath, 169 Exists: true, 170 IsDir: stat.IsDir(), 171 RebaseName: rebaseName, 172 }, nil 173 } 174 175 // CopyInfoDestinationPath stats the given path to create a CopyInfo 176 // struct representing that resource for the destination of an archive copy 177 // operation. The given path should be an absolute local path. 178 func CopyInfoDestinationPath(path string) (info CopyInfo, err error) { 179 maxSymlinkIter := 10 // filepath.EvalSymlinks uses 255, but 10 already seems like a lot. 180 path = normalizePath(path) 181 originalPath := path 182 183 stat, err := os.Lstat(path) 184 185 if err == nil && stat.Mode()&os.ModeSymlink == 0 { 186 // The path exists and is not a symlink. 187 return CopyInfo{ 188 Path: path, 189 Exists: true, 190 IsDir: stat.IsDir(), 191 }, nil 192 } 193 194 // While the path is a symlink. 195 for n := 0; err == nil && stat.Mode()&os.ModeSymlink != 0; n++ { 196 if n > maxSymlinkIter { 197 // Don't follow symlinks more than this arbitrary number of times. 198 return CopyInfo{}, errors.New("too many symlinks in " + originalPath) 199 } 200 201 // The path is a symbolic link. We need to evaluate it so that the 202 // destination of the copy operation is the link target and not the 203 // link itself. This is notably different than CopyInfoSourcePath which 204 // only evaluates symlinks before the last appearing path separator. 205 // Also note that it is okay if the last path element is a broken 206 // symlink as the copy operation should create the target. 207 var linkTarget string 208 209 linkTarget, err = os.Readlink(path) 210 if err != nil { 211 return CopyInfo{}, err 212 } 213 214 if !system.IsAbs(linkTarget) { 215 // Join with the parent directory. 216 dstParent, _ := SplitPathDirEntry(path) 217 linkTarget = filepath.Join(dstParent, linkTarget) 218 } 219 220 path = linkTarget 221 stat, err = os.Lstat(path) 222 } 223 224 if err != nil { 225 // It's okay if the destination path doesn't exist. We can still 226 // continue the copy operation if the parent directory exists. 227 if !os.IsNotExist(err) { 228 return CopyInfo{}, err 229 } 230 231 // Ensure destination parent dir exists. 232 dstParent, _ := SplitPathDirEntry(path) 233 234 parentDirStat, err := os.Lstat(dstParent) 235 if err != nil { 236 return CopyInfo{}, err 237 } 238 if !parentDirStat.IsDir() { 239 return CopyInfo{}, ErrNotDirectory 240 } 241 242 return CopyInfo{Path: path}, nil 243 } 244 245 // The path exists after resolving symlinks. 246 return CopyInfo{ 247 Path: path, 248 Exists: true, 249 IsDir: stat.IsDir(), 250 }, nil 251 } 252 253 // PrepareArchiveCopy prepares the given srcContent archive, which should 254 // contain the archived resource described by srcInfo, to the destination 255 // described by dstInfo. Returns the possibly modified content archive along 256 // with the path to the destination directory which it should be extracted to. 257 func PrepareArchiveCopy(srcContent Reader, srcInfo, dstInfo CopyInfo) (dstDir string, content Archive, err error) { 258 // Ensure in platform semantics 259 srcInfo.Path = normalizePath(srcInfo.Path) 260 dstInfo.Path = normalizePath(dstInfo.Path) 261 262 // Separate the destination path between its directory and base 263 // components in case the source archive contents need to be rebased. 264 dstDir, dstBase := SplitPathDirEntry(dstInfo.Path) 265 _, srcBase := SplitPathDirEntry(srcInfo.Path) 266 267 switch { 268 case dstInfo.Exists && dstInfo.IsDir: 269 // The destination exists as a directory. No alteration 270 // to srcContent is needed as its contents can be 271 // simply extracted to the destination directory. 272 return dstInfo.Path, ioutil.NopCloser(srcContent), nil 273 case dstInfo.Exists && srcInfo.IsDir: 274 // The destination exists as some type of file and the source 275 // content is a directory. This is an error condition since 276 // you cannot copy a directory to an existing file location. 277 return "", nil, ErrCannotCopyDir 278 case dstInfo.Exists: 279 // The destination exists as some type of file and the source content 280 // is also a file. The source content entry will have to be renamed to 281 // have a basename which matches the destination path's basename. 282 return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil 283 case srcInfo.IsDir: 284 // The destination does not exist and the source content is an archive 285 // of a directory. The archive should be extracted to the parent of 286 // the destination path instead, and when it is, the directory that is 287 // created as a result should take the name of the destination path. 288 // The source content entries will have to be renamed to have a 289 // basename which matches the destination path's basename. 290 return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil 291 case assertsDirectory(dstInfo.Path): 292 // The destination does not exist and is asserted to be created as a 293 // directory, but the source content is not a directory. This is an 294 // error condition since you cannot create a directory from a file 295 // source. 296 return "", nil, ErrDirNotExists 297 default: 298 // The last remaining case is when the destination does not exist, is 299 // not asserted to be a directory, and the source content is not an 300 // archive of a directory. It this case, the destination file will need 301 // to be created when the archive is extracted and the source content 302 // entry will have to be renamed to have a basename which matches the 303 // destination path's basename. 304 return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil 305 } 306 307 } 308 309 // rebaseArchiveEntries rewrites the given srcContent archive replacing 310 // an occurrence of oldBase with newBase at the beginning of entry names. 311 func rebaseArchiveEntries(srcContent Reader, oldBase, newBase string) Archive { 312 if oldBase == string(os.PathSeparator) { 313 // If oldBase specifies the root directory, use an empty string as 314 // oldBase instead so that newBase doesn't replace the path separator 315 // that all paths will start with. 316 oldBase = "" 317 } 318 319 rebased, w := io.Pipe() 320 321 go func() { 322 srcTar := tar.NewReader(srcContent) 323 rebasedTar := tar.NewWriter(w) 324 325 for { 326 hdr, err := srcTar.Next() 327 if err == io.EOF { 328 // Signals end of archive. 329 rebasedTar.Close() 330 w.Close() 331 return 332 } 333 if err != nil { 334 w.CloseWithError(err) 335 return 336 } 337 338 hdr.Name = strings.Replace(hdr.Name, oldBase, newBase, 1) 339 340 if err = rebasedTar.WriteHeader(hdr); err != nil { 341 w.CloseWithError(err) 342 return 343 } 344 345 if _, err = io.Copy(rebasedTar, srcTar); err != nil { 346 w.CloseWithError(err) 347 return 348 } 349 } 350 }() 351 352 return rebased 353 } 354 355 // CopyResource performs an archive copy from the given source path to the 356 // given destination path. The source path MUST exist and the destination 357 // path's parent directory must exist. 358 func CopyResource(srcPath, dstPath string) error { 359 var ( 360 srcInfo CopyInfo 361 err error 362 ) 363 364 // Ensure in platform semantics 365 srcPath = normalizePath(srcPath) 366 dstPath = normalizePath(dstPath) 367 368 // Clean the source and destination paths. 369 srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath) 370 dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath) 371 372 if srcInfo, err = CopyInfoSourcePath(srcPath); err != nil { 373 return err 374 } 375 376 content, err := TarResource(srcInfo) 377 if err != nil { 378 return err 379 } 380 defer content.Close() 381 382 return CopyTo(content, srcInfo, dstPath) 383 } 384 385 // CopyTo handles extracting the given content whose 386 // entries should be sourced from srcInfo to dstPath. 387 func CopyTo(content Reader, srcInfo CopyInfo, dstPath string) error { 388 // The destination path need not exist, but CopyInfoDestinationPath will 389 // ensure that at least the parent directory exists. 390 dstInfo, err := CopyInfoDestinationPath(normalizePath(dstPath)) 391 if err != nil { 392 return err 393 } 394 395 dstDir, copyArchive, err := PrepareArchiveCopy(content, srcInfo, dstInfo) 396 if err != nil { 397 return err 398 } 399 defer copyArchive.Close() 400 401 options := &TarOptions{ 402 NoLchown: true, 403 NoOverwriteDirNonDir: true, 404 } 405 406 return Untar(copyArchive, dstDir, options) 407 }