]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/fsouza/go-dockerclient/external/github.com/docker/docker/pkg/archive/copy.go
e95091264387afd236f8fa2703dcbc50803abe75
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / fsouza / go-dockerclient / external / github.com / docker / docker / pkg / archive / copy.go
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/fsouza/go-dockerclient/external/github.com/Sirupsen/logrus"
13 "github.com/fsouza/go-dockerclient/external/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, followLink bool) (CopyInfo, error) {
139 // normalize the file path and then evaluate the symbol link
140 // we will use the target file instead of the symbol link if
141 // followLink is set
142 path = normalizePath(path)
143
144 resolvedPath, rebaseName, err := ResolveHostSourcePath(path, followLink)
145 if err != nil {
146 return CopyInfo{}, err
147 }
148
149 stat, err := os.Lstat(resolvedPath)
150 if err != nil {
151 return CopyInfo{}, err
152 }
153
154 return CopyInfo{
155 Path: resolvedPath,
156 Exists: true,
157 IsDir: stat.IsDir(),
158 RebaseName: rebaseName,
159 }, nil
160 }
161
162 // CopyInfoDestinationPath stats the given path to create a CopyInfo
163 // struct representing that resource for the destination of an archive copy
164 // operation. The given path should be an absolute local path.
165 func CopyInfoDestinationPath(path string) (info CopyInfo, err error) {
166 maxSymlinkIter := 10 // filepath.EvalSymlinks uses 255, but 10 already seems like a lot.
167 path = normalizePath(path)
168 originalPath := path
169
170 stat, err := os.Lstat(path)
171
172 if err == nil && stat.Mode()&os.ModeSymlink == 0 {
173 // The path exists and is not a symlink.
174 return CopyInfo{
175 Path: path,
176 Exists: true,
177 IsDir: stat.IsDir(),
178 }, nil
179 }
180
181 // While the path is a symlink.
182 for n := 0; err == nil && stat.Mode()&os.ModeSymlink != 0; n++ {
183 if n > maxSymlinkIter {
184 // Don't follow symlinks more than this arbitrary number of times.
185 return CopyInfo{}, errors.New("too many symlinks in " + originalPath)
186 }
187
188 // The path is a symbolic link. We need to evaluate it so that the
189 // destination of the copy operation is the link target and not the
190 // link itself. This is notably different than CopyInfoSourcePath which
191 // only evaluates symlinks before the last appearing path separator.
192 // Also note that it is okay if the last path element is a broken
193 // symlink as the copy operation should create the target.
194 var linkTarget string
195
196 linkTarget, err = os.Readlink(path)
197 if err != nil {
198 return CopyInfo{}, err
199 }
200
201 if !system.IsAbs(linkTarget) {
202 // Join with the parent directory.
203 dstParent, _ := SplitPathDirEntry(path)
204 linkTarget = filepath.Join(dstParent, linkTarget)
205 }
206
207 path = linkTarget
208 stat, err = os.Lstat(path)
209 }
210
211 if err != nil {
212 // It's okay if the destination path doesn't exist. We can still
213 // continue the copy operation if the parent directory exists.
214 if !os.IsNotExist(err) {
215 return CopyInfo{}, err
216 }
217
218 // Ensure destination parent dir exists.
219 dstParent, _ := SplitPathDirEntry(path)
220
221 parentDirStat, err := os.Lstat(dstParent)
222 if err != nil {
223 return CopyInfo{}, err
224 }
225 if !parentDirStat.IsDir() {
226 return CopyInfo{}, ErrNotDirectory
227 }
228
229 return CopyInfo{Path: path}, nil
230 }
231
232 // The path exists after resolving symlinks.
233 return CopyInfo{
234 Path: path,
235 Exists: true,
236 IsDir: stat.IsDir(),
237 }, nil
238 }
239
240 // PrepareArchiveCopy prepares the given srcContent archive, which should
241 // contain the archived resource described by srcInfo, to the destination
242 // described by dstInfo. Returns the possibly modified content archive along
243 // with the path to the destination directory which it should be extracted to.
244 func PrepareArchiveCopy(srcContent Reader, srcInfo, dstInfo CopyInfo) (dstDir string, content Archive, err error) {
245 // Ensure in platform semantics
246 srcInfo.Path = normalizePath(srcInfo.Path)
247 dstInfo.Path = normalizePath(dstInfo.Path)
248
249 // Separate the destination path between its directory and base
250 // components in case the source archive contents need to be rebased.
251 dstDir, dstBase := SplitPathDirEntry(dstInfo.Path)
252 _, srcBase := SplitPathDirEntry(srcInfo.Path)
253
254 switch {
255 case dstInfo.Exists && dstInfo.IsDir:
256 // The destination exists as a directory. No alteration
257 // to srcContent is needed as its contents can be
258 // simply extracted to the destination directory.
259 return dstInfo.Path, ioutil.NopCloser(srcContent), nil
260 case dstInfo.Exists && srcInfo.IsDir:
261 // The destination exists as some type of file and the source
262 // content is a directory. This is an error condition since
263 // you cannot copy a directory to an existing file location.
264 return "", nil, ErrCannotCopyDir
265 case dstInfo.Exists:
266 // The destination exists as some type of file and the source content
267 // is also a file. The source content entry will have to be renamed to
268 // have a basename which matches the destination path's basename.
269 if len(srcInfo.RebaseName) != 0 {
270 srcBase = srcInfo.RebaseName
271 }
272 return dstDir, RebaseArchiveEntries(srcContent, srcBase, dstBase), nil
273 case srcInfo.IsDir:
274 // The destination does not exist and the source content is an archive
275 // of a directory. The archive should be extracted to the parent of
276 // the destination path instead, and when it is, the directory that is
277 // created as a result should take the name of the destination path.
278 // The source content entries will have to be renamed to have a
279 // basename which matches the destination path's basename.
280 if len(srcInfo.RebaseName) != 0 {
281 srcBase = srcInfo.RebaseName
282 }
283 return dstDir, RebaseArchiveEntries(srcContent, srcBase, dstBase), nil
284 case assertsDirectory(dstInfo.Path):
285 // The destination does not exist and is asserted to be created as a
286 // directory, but the source content is not a directory. This is an
287 // error condition since you cannot create a directory from a file
288 // source.
289 return "", nil, ErrDirNotExists
290 default:
291 // The last remaining case is when the destination does not exist, is
292 // not asserted to be a directory, and the source content is not an
293 // archive of a directory. It this case, the destination file will need
294 // to be created when the archive is extracted and the source content
295 // entry will have to be renamed to have a basename which matches the
296 // destination path's basename.
297 if len(srcInfo.RebaseName) != 0 {
298 srcBase = srcInfo.RebaseName
299 }
300 return dstDir, RebaseArchiveEntries(srcContent, srcBase, dstBase), nil
301 }
302
303 }
304
305 // RebaseArchiveEntries rewrites the given srcContent archive replacing
306 // an occurrence of oldBase with newBase at the beginning of entry names.
307 func RebaseArchiveEntries(srcContent Reader, oldBase, newBase string) Archive {
308 if oldBase == string(os.PathSeparator) {
309 // If oldBase specifies the root directory, use an empty string as
310 // oldBase instead so that newBase doesn't replace the path separator
311 // that all paths will start with.
312 oldBase = ""
313 }
314
315 rebased, w := io.Pipe()
316
317 go func() {
318 srcTar := tar.NewReader(srcContent)
319 rebasedTar := tar.NewWriter(w)
320
321 for {
322 hdr, err := srcTar.Next()
323 if err == io.EOF {
324 // Signals end of archive.
325 rebasedTar.Close()
326 w.Close()
327 return
328 }
329 if err != nil {
330 w.CloseWithError(err)
331 return
332 }
333
334 hdr.Name = strings.Replace(hdr.Name, oldBase, newBase, 1)
335
336 if err = rebasedTar.WriteHeader(hdr); err != nil {
337 w.CloseWithError(err)
338 return
339 }
340
341 if _, err = io.Copy(rebasedTar, srcTar); err != nil {
342 w.CloseWithError(err)
343 return
344 }
345 }
346 }()
347
348 return rebased
349 }
350
351 // CopyResource performs an archive copy from the given source path to the
352 // given destination path. The source path MUST exist and the destination
353 // path's parent directory must exist.
354 func CopyResource(srcPath, dstPath string, followLink bool) error {
355 var (
356 srcInfo CopyInfo
357 err error
358 )
359
360 // Ensure in platform semantics
361 srcPath = normalizePath(srcPath)
362 dstPath = normalizePath(dstPath)
363
364 // Clean the source and destination paths.
365 srcPath = PreserveTrailingDotOrSeparator(filepath.Clean(srcPath), srcPath)
366 dstPath = PreserveTrailingDotOrSeparator(filepath.Clean(dstPath), dstPath)
367
368 if srcInfo, err = CopyInfoSourcePath(srcPath, followLink); err != nil {
369 return err
370 }
371
372 content, err := TarResource(srcInfo)
373 if err != nil {
374 return err
375 }
376 defer content.Close()
377
378 return CopyTo(content, srcInfo, dstPath)
379 }
380
381 // CopyTo handles extracting the given content whose
382 // entries should be sourced from srcInfo to dstPath.
383 func CopyTo(content Reader, srcInfo CopyInfo, dstPath string) error {
384 // The destination path need not exist, but CopyInfoDestinationPath will
385 // ensure that at least the parent directory exists.
386 dstInfo, err := CopyInfoDestinationPath(normalizePath(dstPath))
387 if err != nil {
388 return err
389 }
390
391 dstDir, copyArchive, err := PrepareArchiveCopy(content, srcInfo, dstInfo)
392 if err != nil {
393 return err
394 }
395 defer copyArchive.Close()
396
397 options := &TarOptions{
398 NoLchown: true,
399 NoOverwriteDirNonDir: true,
400 }
401
402 return Untar(copyArchive, dstDir, options)
403 }
404
405 // ResolveHostSourcePath decides real path need to be copied with parameters such as
406 // whether to follow symbol link or not, if followLink is true, resolvedPath will return
407 // link target of any symbol link file, else it will only resolve symlink of directory
408 // but return symbol link file itself without resolving.
409 func ResolveHostSourcePath(path string, followLink bool) (resolvedPath, rebaseName string, err error) {
410 if followLink {
411 resolvedPath, err = filepath.EvalSymlinks(path)
412 if err != nil {
413 return
414 }
415
416 resolvedPath, rebaseName = GetRebaseName(path, resolvedPath)
417 } else {
418 dirPath, basePath := filepath.Split(path)
419
420 // if not follow symbol link, then resolve symbol link of parent dir
421 var resolvedDirPath string
422 resolvedDirPath, err = filepath.EvalSymlinks(dirPath)
423 if err != nil {
424 return
425 }
426 // resolvedDirPath will have been cleaned (no trailing path separators) so
427 // we can manually join it with the base path element.
428 resolvedPath = resolvedDirPath + string(filepath.Separator) + basePath
429 if hasTrailingPathSeparator(path) && filepath.Base(path) != filepath.Base(resolvedPath) {
430 rebaseName = filepath.Base(path)
431 }
432 }
433 return resolvedPath, rebaseName, nil
434 }
435
436 // GetRebaseName normalizes and compares path and resolvedPath,
437 // return completed resolved path and rebased file name
438 func GetRebaseName(path, resolvedPath string) (string, string) {
439 // linkTarget will have been cleaned (no trailing path separators and dot) so
440 // we can manually join it with them
441 var rebaseName string
442 if specifiesCurrentDir(path) && !specifiesCurrentDir(resolvedPath) {
443 resolvedPath += string(filepath.Separator) + "."
444 }
445
446 if hasTrailingPathSeparator(path) && !hasTrailingPathSeparator(resolvedPath) {
447 resolvedPath += string(filepath.Separator)
448 }
449
450 if filepath.Base(path) != filepath.Base(resolvedPath) {
451 // In the case where the path had a trailing separator and a symlink
452 // evaluation has changed the last path component, we will need to
453 // rebase the name in the archive that is being copied to match the
454 // originally requested name.
455 rebaseName = filepath.Base(path)
456 }
457 return resolvedPath, rebaseName
458 }