cp.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. package client
  2. import (
  3. "fmt"
  4. "io"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. Cli "github.com/docker/docker/cli"
  9. "github.com/docker/docker/pkg/archive"
  10. flag "github.com/docker/docker/pkg/mflag"
  11. "github.com/docker/docker/pkg/system"
  12. "github.com/docker/engine-api/types"
  13. )
  14. type copyDirection int
  15. const (
  16. fromContainer copyDirection = (1 << iota)
  17. toContainer
  18. acrossContainers = fromContainer | toContainer
  19. )
  20. type cpConfig struct {
  21. followLink bool
  22. }
  23. // CmdCp copies files/folders to or from a path in a container.
  24. //
  25. // When copying from a container, if DEST_PATH is '-' the data is written as a
  26. // tar archive file to STDOUT.
  27. //
  28. // When copying to a container, if SRC_PATH is '-' the data is read as a tar
  29. // archive file from STDIN, and the destination CONTAINER:DEST_PATH, must specify
  30. // a directory.
  31. //
  32. // Usage:
  33. // docker cp CONTAINER:SRC_PATH DEST_PATH|-
  34. // docker cp SRC_PATH|- CONTAINER:DEST_PATH
  35. func (cli *DockerCli) CmdCp(args ...string) error {
  36. cmd := Cli.Subcmd(
  37. "cp",
  38. []string{"CONTAINER:SRC_PATH DEST_PATH|-", "SRC_PATH|- CONTAINER:DEST_PATH"},
  39. strings.Join([]string{
  40. Cli.DockerCommands["cp"].Description,
  41. "\nUse '-' as the source to read a tar archive from stdin\n",
  42. "and extract it to a directory destination in a container.\n",
  43. "Use '-' as the destination to stream a tar archive of a\n",
  44. "container source to stdout.",
  45. }, ""),
  46. true,
  47. )
  48. followLink := cmd.Bool([]string{"L", "-follow-link"}, false, "Always follow symbol link in SRC_PATH")
  49. cmd.Require(flag.Exact, 2)
  50. cmd.ParseFlags(args, true)
  51. if cmd.Arg(0) == "" {
  52. return fmt.Errorf("source can not be empty")
  53. }
  54. if cmd.Arg(1) == "" {
  55. return fmt.Errorf("destination can not be empty")
  56. }
  57. srcContainer, srcPath := splitCpArg(cmd.Arg(0))
  58. dstContainer, dstPath := splitCpArg(cmd.Arg(1))
  59. var direction copyDirection
  60. if srcContainer != "" {
  61. direction |= fromContainer
  62. }
  63. if dstContainer != "" {
  64. direction |= toContainer
  65. }
  66. cpParam := &cpConfig{
  67. followLink: *followLink,
  68. }
  69. switch direction {
  70. case fromContainer:
  71. return cli.copyFromContainer(srcContainer, srcPath, dstPath, cpParam)
  72. case toContainer:
  73. return cli.copyToContainer(srcPath, dstContainer, dstPath, cpParam)
  74. case acrossContainers:
  75. // Copying between containers isn't supported.
  76. return fmt.Errorf("copying between containers is not supported")
  77. default:
  78. // User didn't specify any container.
  79. return fmt.Errorf("must specify at least one container source")
  80. }
  81. }
  82. // We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
  83. // in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by
  84. // requiring a LOCALPATH with a `:` to be made explicit with a relative or
  85. // absolute path:
  86. // `/path/to/file:name.txt` or `./file:name.txt`
  87. //
  88. // This is apparently how `scp` handles this as well:
  89. // http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/
  90. //
  91. // We can't simply check for a filepath separator because container names may
  92. // have a separator, e.g., "host0/cname1" if container is in a Docker cluster,
  93. // so we have to check for a `/` or `.` prefix. Also, in the case of a Windows
  94. // client, a `:` could be part of an absolute Windows path, in which case it
  95. // is immediately proceeded by a backslash.
  96. func splitCpArg(arg string) (container, path string) {
  97. if system.IsAbs(arg) {
  98. // Explicit local absolute path, e.g., `C:\foo` or `/foo`.
  99. return "", arg
  100. }
  101. parts := strings.SplitN(arg, ":", 2)
  102. if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
  103. // Either there's no `:` in the arg
  104. // OR it's an explicit local relative path like `./file:name.txt`.
  105. return "", arg
  106. }
  107. return parts[0], parts[1]
  108. }
  109. func (cli *DockerCli) statContainerPath(containerName, path string) (types.ContainerPathStat, error) {
  110. return cli.client.ContainerStatPath(containerName, path)
  111. }
  112. func resolveLocalPath(localPath string) (absPath string, err error) {
  113. if absPath, err = filepath.Abs(localPath); err != nil {
  114. return
  115. }
  116. return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
  117. }
  118. func (cli *DockerCli) copyFromContainer(srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) {
  119. if dstPath != "-" {
  120. // Get an absolute destination path.
  121. dstPath, err = resolveLocalPath(dstPath)
  122. if err != nil {
  123. return err
  124. }
  125. }
  126. // if client requests to follow symbol link, then must decide target file to be copied
  127. var rebaseName string
  128. if cpParam.followLink {
  129. srcStat, err := cli.statContainerPath(srcContainer, srcPath)
  130. // If the destination is a symbolic link, we should follow it.
  131. if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
  132. linkTarget := srcStat.LinkTarget
  133. if !system.IsAbs(linkTarget) {
  134. // Join with the parent directory.
  135. srcParent, _ := archive.SplitPathDirEntry(srcPath)
  136. linkTarget = filepath.Join(srcParent, linkTarget)
  137. }
  138. linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
  139. srcPath = linkTarget
  140. }
  141. }
  142. content, stat, err := cli.client.CopyFromContainer(srcContainer, srcPath)
  143. if err != nil {
  144. return err
  145. }
  146. defer content.Close()
  147. if dstPath == "-" {
  148. // Send the response to STDOUT.
  149. _, err = io.Copy(os.Stdout, content)
  150. return err
  151. }
  152. // Prepare source copy info.
  153. srcInfo := archive.CopyInfo{
  154. Path: srcPath,
  155. Exists: true,
  156. IsDir: stat.Mode.IsDir(),
  157. RebaseName: rebaseName,
  158. }
  159. preArchive := content
  160. if len(srcInfo.RebaseName) != 0 {
  161. _, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
  162. preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
  163. }
  164. // See comments in the implementation of `archive.CopyTo` for exactly what
  165. // goes into deciding how and whether the source archive needs to be
  166. // altered for the correct copy behavior.
  167. return archive.CopyTo(preArchive, srcInfo, dstPath)
  168. }
  169. func (cli *DockerCli) copyToContainer(srcPath, dstContainer, dstPath string, cpParam *cpConfig) (err error) {
  170. if srcPath != "-" {
  171. // Get an absolute source path.
  172. srcPath, err = resolveLocalPath(srcPath)
  173. if err != nil {
  174. return err
  175. }
  176. }
  177. // In order to get the copy behavior right, we need to know information
  178. // about both the source and destination. The API is a simple tar
  179. // archive/extract API but we can use the stat info header about the
  180. // destination to be more informed about exactly what the destination is.
  181. // Prepare destination copy info by stat-ing the container path.
  182. dstInfo := archive.CopyInfo{Path: dstPath}
  183. dstStat, err := cli.statContainerPath(dstContainer, dstPath)
  184. // If the destination is a symbolic link, we should evaluate it.
  185. if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
  186. linkTarget := dstStat.LinkTarget
  187. if !system.IsAbs(linkTarget) {
  188. // Join with the parent directory.
  189. dstParent, _ := archive.SplitPathDirEntry(dstPath)
  190. linkTarget = filepath.Join(dstParent, linkTarget)
  191. }
  192. dstInfo.Path = linkTarget
  193. dstStat, err = cli.statContainerPath(dstContainer, linkTarget)
  194. }
  195. // Ignore any error and assume that the parent directory of the destination
  196. // path exists, in which case the copy may still succeed. If there is any
  197. // type of conflict (e.g., non-directory overwriting an existing directory
  198. // or vice versa) the extraction will fail. If the destination simply did
  199. // not exist, but the parent directory does, the extraction will still
  200. // succeed.
  201. if err == nil {
  202. dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
  203. }
  204. var (
  205. content io.Reader
  206. resolvedDstPath string
  207. )
  208. if srcPath == "-" {
  209. // Use STDIN.
  210. content = os.Stdin
  211. resolvedDstPath = dstInfo.Path
  212. if !dstInfo.IsDir {
  213. return fmt.Errorf("destination %q must be a directory", fmt.Sprintf("%s:%s", dstContainer, dstPath))
  214. }
  215. } else {
  216. // Prepare source copy info.
  217. srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink)
  218. if err != nil {
  219. return err
  220. }
  221. srcArchive, err := archive.TarResource(srcInfo)
  222. if err != nil {
  223. return err
  224. }
  225. defer srcArchive.Close()
  226. // With the stat info about the local source as well as the
  227. // destination, we have enough information to know whether we need to
  228. // alter the archive that we upload so that when the server extracts
  229. // it to the specified directory in the container we get the desired
  230. // copy behavior.
  231. // See comments in the implementation of `archive.PrepareArchiveCopy`
  232. // for exactly what goes into deciding how and whether the source
  233. // archive needs to be altered for the correct copy behavior when it is
  234. // extracted. This function also infers from the source and destination
  235. // info which directory to extract to, which may be the parent of the
  236. // destination that the user specified.
  237. dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
  238. if err != nil {
  239. return err
  240. }
  241. defer preparedArchive.Close()
  242. resolvedDstPath = dstDir
  243. content = preparedArchive
  244. }
  245. options := types.CopyToContainerOptions{
  246. ContainerID: dstContainer,
  247. Path: resolvedDstPath,
  248. Content: content,
  249. AllowOverwriteDirWithFile: false,
  250. }
  251. return cli.client.CopyToContainer(options)
  252. }