cp.go 9.4 KB

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