github.com/hauerwu/docker@v1.8.0-rc1/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" 10 "path/filepath" 11 "strings" 12 13 log "github.com/Sirupsen/logrus" 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 if !SpecifiesCurrentDir(cleanedPath) && SpecifiesCurrentDir(originalPath) { 33 if !HasTrailingPathSeparator(cleanedPath) { 34 // Add a separator if it doesn't already end with one (a cleaned 35 // path would only end in a separator if it is the root). 36 cleanedPath += string(filepath.Separator) 37 } 38 cleanedPath += "." 39 } 40 41 if !HasTrailingPathSeparator(cleanedPath) && HasTrailingPathSeparator(originalPath) { 42 cleanedPath += string(filepath.Separator) 43 } 44 45 return cleanedPath 46 } 47 48 // AssertsDirectory returns whether the given path is 49 // asserted to be a directory, i.e., the path ends with 50 // a trailing '/' or `/.`, assuming a path separator of `/`. 51 func AssertsDirectory(path string) bool { 52 return HasTrailingPathSeparator(path) || SpecifiesCurrentDir(path) 53 } 54 55 // HasTrailingPathSeparator returns whether the given 56 // path ends with the system's path separator character. 57 func HasTrailingPathSeparator(path string) bool { 58 return len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) 59 } 60 61 // SpecifiesCurrentDir returns whether the given path specifies 62 // a "current directory", i.e., the last path segment is `.`. 63 func SpecifiesCurrentDir(path string) bool { 64 return filepath.Base(path) == "." 65 } 66 67 // SplitPathDirEntry splits the given path between its 68 // parent directory and its basename in that directory. 69 func SplitPathDirEntry(localizedPath string) (dir, base string) { 70 normalizedPath := filepath.ToSlash(localizedPath) 71 vol := filepath.VolumeName(normalizedPath) 72 normalizedPath = normalizedPath[len(vol):] 73 74 if normalizedPath == "/" { 75 // Specifies the root path. 76 return filepath.FromSlash(vol + normalizedPath), "." 77 } 78 79 trimmedPath := vol + strings.TrimRight(normalizedPath, "/") 80 81 dir = filepath.FromSlash(path.Dir(trimmedPath)) 82 base = filepath.FromSlash(path.Base(trimmedPath)) 83 84 return dir, base 85 } 86 87 // TarResource archives the resource at the given sourcePath into a Tar 88 // archive. A non-nil error is returned if sourcePath does not exist or is 89 // asserted to be a directory but exists as another type of file. 90 // 91 // This function acts as a convenient wrapper around TarWithOptions, which 92 // requires a directory as the source path. TarResource accepts either a 93 // directory or a file path and correctly sets the Tar options. 94 func TarResource(sourcePath string) (content Archive, err error) { 95 if _, err = os.Lstat(sourcePath); err != nil { 96 // Catches the case where the source does not exist or is not a 97 // directory if asserted to be a directory, as this also causes an 98 // error. 99 return 100 } 101 102 if len(sourcePath) > 1 && HasTrailingPathSeparator(sourcePath) { 103 // In the case where the source path is a symbolic link AND it ends 104 // with a path separator, we will want to evaluate the symbolic link. 105 trimmedPath := sourcePath[:len(sourcePath)-1] 106 stat, err := os.Lstat(trimmedPath) 107 if err != nil { 108 return nil, err 109 } 110 111 if stat.Mode()&os.ModeSymlink != 0 { 112 if sourcePath, err = filepath.EvalSymlinks(trimmedPath); err != nil { 113 return nil, err 114 } 115 } 116 } 117 118 // Separate the source path between it's directory and 119 // the entry in that directory which we are archiving. 120 sourceDir, sourceBase := SplitPathDirEntry(sourcePath) 121 122 filter := []string{sourceBase} 123 124 log.Debugf("copying %q from %q", sourceBase, sourceDir) 125 126 return TarWithOptions(sourceDir, &TarOptions{ 127 Compression: Uncompressed, 128 IncludeFiles: filter, 129 IncludeSourceDir: true, 130 }) 131 } 132 133 // CopyInfo holds basic info about the source 134 // or destination path of a copy operation. 135 type CopyInfo struct { 136 Path string 137 Exists bool 138 IsDir bool 139 } 140 141 // CopyInfoStatPath stats the given path to create a CopyInfo 142 // struct representing that resource. If mustExist is true, then 143 // it is an error if there is no file or directory at the given path. 144 func CopyInfoStatPath(path string, mustExist bool) (CopyInfo, error) { 145 pathInfo := CopyInfo{Path: path} 146 147 fileInfo, err := os.Lstat(path) 148 149 if err == nil { 150 pathInfo.Exists, pathInfo.IsDir = true, fileInfo.IsDir() 151 } else if os.IsNotExist(err) && !mustExist { 152 err = nil 153 } 154 155 return pathInfo, err 156 } 157 158 // PrepareArchiveCopy prepares the given srcContent archive, which should 159 // contain the archived resource described by srcInfo, to the destination 160 // described by dstInfo. Returns the possibly modified content archive along 161 // with the path to the destination directory which it should be extracted to. 162 func PrepareArchiveCopy(srcContent ArchiveReader, srcInfo, dstInfo CopyInfo) (dstDir string, content Archive, err error) { 163 // Separate the destination path between its directory and base 164 // components in case the source archive contents need to be rebased. 165 dstDir, dstBase := SplitPathDirEntry(dstInfo.Path) 166 _, srcBase := SplitPathDirEntry(srcInfo.Path) 167 168 switch { 169 case dstInfo.Exists && dstInfo.IsDir: 170 // The destination exists as a directory. No alteration 171 // to srcContent is needed as its contents can be 172 // simply extracted to the destination directory. 173 return dstInfo.Path, ioutil.NopCloser(srcContent), nil 174 case dstInfo.Exists && srcInfo.IsDir: 175 // The destination exists as some type of file and the source 176 // content is a directory. This is an error condition since 177 // you cannot copy a directory to an existing file location. 178 return "", nil, ErrCannotCopyDir 179 case dstInfo.Exists: 180 // The destination exists as some type of file and the source content 181 // is also a file. The source content entry will have to be renamed to 182 // have a basename which matches the destination path's basename. 183 return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil 184 case srcInfo.IsDir: 185 // The destination does not exist and the source content is an archive 186 // of a directory. The archive should be extracted to the parent of 187 // the destination path instead, and when it is, the directory that is 188 // created as a result should take the name of the destination path. 189 // The source content entries will have to be renamed to have a 190 // basename which matches the destination path's basename. 191 return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil 192 case AssertsDirectory(dstInfo.Path): 193 // The destination does not exist and is asserted to be created as a 194 // directory, but the source content is not a directory. This is an 195 // error condition since you cannot create a directory from a file 196 // source. 197 return "", nil, ErrDirNotExists 198 default: 199 // The last remaining case is when the destination does not exist, is 200 // not asserted to be a directory, and the source content is not an 201 // archive of a directory. It this case, the destination file will need 202 // to be created when the archive is extracted and the source content 203 // entry will have to be renamed to have a basename which matches the 204 // destination path's basename. 205 return dstDir, rebaseArchiveEntries(srcContent, srcBase, dstBase), nil 206 } 207 208 } 209 210 // rebaseArchiveEntries rewrites the given srcContent archive replacing 211 // an occurance of oldBase with newBase at the beginning of entry names. 212 func rebaseArchiveEntries(srcContent ArchiveReader, oldBase, newBase string) Archive { 213 rebased, w := io.Pipe() 214 215 go func() { 216 srcTar := tar.NewReader(srcContent) 217 rebasedTar := tar.NewWriter(w) 218 219 for { 220 hdr, err := srcTar.Next() 221 if err == io.EOF { 222 // Signals end of archive. 223 rebasedTar.Close() 224 w.Close() 225 return 226 } 227 if err != nil { 228 w.CloseWithError(err) 229 return 230 } 231 232 hdr.Name = strings.Replace(hdr.Name, oldBase, newBase, 1) 233 234 if err = rebasedTar.WriteHeader(hdr); err != nil { 235 w.CloseWithError(err) 236 return 237 } 238 239 if _, err = io.Copy(rebasedTar, srcTar); err != nil { 240 w.CloseWithError(err) 241 return 242 } 243 } 244 }() 245 246 return rebased 247 } 248 249 // CopyResource performs an archive copy from the given source path to the 250 // given destination path. The source path MUST exist and the destination 251 // path's parent directory must exist. 252 func CopyResource(srcPath, dstPath string) error { 253 var ( 254 srcInfo CopyInfo 255 err error 256 ) 257 258 // Clean the source and destination paths. 259 srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath) 260 dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath) 261 262 if srcInfo, err = CopyInfoStatPath(srcPath, true); err != nil { 263 return err 264 } 265 266 content, err := TarResource(srcPath) 267 if err != nil { 268 return err 269 } 270 defer content.Close() 271 272 return CopyTo(content, srcInfo, dstPath) 273 } 274 275 // CopyTo handles extracting the given content whose 276 // entries should be sourced from srcInfo to dstPath. 277 func CopyTo(content ArchiveReader, srcInfo CopyInfo, dstPath string) error { 278 dstInfo, err := CopyInfoStatPath(dstPath, false) 279 if err != nil { 280 return err 281 } 282 283 if !dstInfo.Exists { 284 // Ensure destination parent dir exists. 285 dstParent, _ := SplitPathDirEntry(dstPath) 286 287 dstStat, err := os.Lstat(dstParent) 288 if err != nil { 289 return err 290 } 291 if !dstStat.IsDir() { 292 return ErrNotDirectory 293 } 294 } 295 296 dstDir, copyArchive, err := PrepareArchiveCopy(content, srcInfo, dstInfo) 297 if err != nil { 298 return err 299 } 300 defer copyArchive.Close() 301 302 options := &TarOptions{ 303 NoLchown: true, 304 NoOverwriteDirNonDir: true, 305 } 306 307 return Untar(copyArchive, dstDir, options) 308 }