cp.go 10 KB

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