cp.go 10.0 KB

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