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