cp.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. /*
  2. Copyright 2020 Docker Compose CLI authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package compose
  14. import (
  15. "context"
  16. "fmt"
  17. "io"
  18. "os"
  19. "path/filepath"
  20. "strings"
  21. "golang.org/x/sync/errgroup"
  22. "github.com/docker/cli/cli/command"
  23. "github.com/docker/compose/v2/pkg/api"
  24. moby "github.com/docker/docker/api/types"
  25. "github.com/docker/docker/pkg/archive"
  26. "github.com/docker/docker/pkg/system"
  27. "github.com/pkg/errors"
  28. )
  29. type copyDirection int
  30. const (
  31. fromService copyDirection = 1 << iota
  32. toService
  33. acrossServices = fromService | toService
  34. )
  35. func (s *composeService) Copy(ctx context.Context, projectName string, options api.CopyOptions) error {
  36. projectName = strings.ToLower(projectName)
  37. srcService, srcPath := splitCpArg(options.Source)
  38. destService, dstPath := splitCpArg(options.Destination)
  39. var direction copyDirection
  40. var serviceName string
  41. var copyFunc func(ctx context.Context, containerID string, srcPath string, dstPath string, opts api.CopyOptions) error
  42. if srcService != "" {
  43. direction |= fromService
  44. serviceName = srcService
  45. copyFunc = s.copyFromContainer
  46. // copying from multiple containers of a services doesn't make sense.
  47. if options.All {
  48. return errors.New("cannot use the --all flag when copying from a service")
  49. }
  50. }
  51. if destService != "" {
  52. direction |= toService
  53. serviceName = destService
  54. copyFunc = s.copyToContainer
  55. }
  56. if direction == acrossServices {
  57. return errors.New("copying between services is not supported")
  58. }
  59. if direction == 0 {
  60. return errors.New("unknown copy direction")
  61. }
  62. containers, err := s.listContainersTargetedForCopy(ctx, projectName, options.Index, direction, serviceName)
  63. if err != nil {
  64. return err
  65. }
  66. g := errgroup.Group{}
  67. for _, cont := range containers {
  68. container := cont
  69. g.Go(func() error {
  70. if err := copyFunc(ctx, container.ID, srcPath, dstPath, options); err != nil {
  71. return err
  72. }
  73. fromOrInside := "inside"
  74. if direction == fromService {
  75. fromOrInside = "from"
  76. }
  77. fmt.Fprintf(s.stderr(), "Copy %s to path %s %s %s service container\n", srcPath, dstPath, fromOrInside, getCanonicalContainerName(container))
  78. return nil
  79. })
  80. }
  81. return g.Wait()
  82. }
  83. func (s *composeService) listContainersTargetedForCopy(ctx context.Context, projectName string, index int, direction copyDirection, serviceName string) (Containers, error) {
  84. var containers Containers
  85. var err error
  86. switch {
  87. case index > 0:
  88. container, err := s.getSpecifiedContainer(ctx, projectName, oneOffExclude, true, serviceName, index)
  89. if err != nil {
  90. return nil, err
  91. }
  92. return append(containers, container), nil
  93. default:
  94. containers, err = s.getContainers(ctx, projectName, oneOffExclude, true, serviceName)
  95. if err != nil {
  96. return nil, err
  97. }
  98. if len(containers) < 1 {
  99. return nil, fmt.Errorf("no container found for service %q", serviceName)
  100. }
  101. if direction == fromService {
  102. return containers[:1], err
  103. }
  104. return containers, err
  105. }
  106. }
  107. func (s *composeService) copyToContainer(ctx context.Context, containerID string, srcPath string, dstPath string, opts api.CopyOptions) error {
  108. var err error
  109. if srcPath != "-" {
  110. // Get an absolute source path.
  111. srcPath, err = resolveLocalPath(srcPath)
  112. if err != nil {
  113. return err
  114. }
  115. }
  116. // Prepare destination copy info by stat-ing the container path.
  117. dstInfo := archive.CopyInfo{Path: dstPath}
  118. dstStat, err := s.apiClient().ContainerStatPath(ctx, containerID, dstPath)
  119. // If the destination is a symbolic link, we should evaluate it.
  120. if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
  121. linkTarget := dstStat.LinkTarget
  122. if !system.IsAbs(linkTarget) {
  123. // Join with the parent directory.
  124. dstParent, _ := archive.SplitPathDirEntry(dstPath)
  125. linkTarget = filepath.Join(dstParent, linkTarget)
  126. }
  127. dstInfo.Path = linkTarget
  128. dstStat, err = s.apiClient().ContainerStatPath(ctx, containerID, linkTarget)
  129. }
  130. // Validate the destination path
  131. if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil {
  132. return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, containerID, dstPath)
  133. }
  134. // Ignore any error and assume that the parent directory of the destination
  135. // path exists, in which case the copy may still succeed. If there is any
  136. // type of conflict (e.g., non-directory overwriting an existing directory
  137. // or vice versa) the extraction will fail. If the destination simply did
  138. // not exist, but the parent directory does, the extraction will still
  139. // succeed.
  140. if err == nil {
  141. dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
  142. }
  143. var (
  144. content io.Reader
  145. resolvedDstPath string
  146. )
  147. if srcPath == "-" {
  148. content = s.stdin()
  149. resolvedDstPath = dstInfo.Path
  150. if !dstInfo.IsDir {
  151. return errors.Errorf("destination \"%s:%s\" must be a directory", containerID, dstPath)
  152. }
  153. } else {
  154. // Prepare source copy info.
  155. srcInfo, err := archive.CopyInfoSourcePath(srcPath, opts.FollowLink)
  156. if err != nil {
  157. return err
  158. }
  159. srcArchive, err := archive.TarResource(srcInfo)
  160. if err != nil {
  161. return err
  162. }
  163. defer srcArchive.Close() //nolint:errcheck
  164. // With the stat info about the local source as well as the
  165. // destination, we have enough information to know whether we need to
  166. // alter the archive that we upload so that when the server extracts
  167. // it to the specified directory in the container we get the desired
  168. // copy behavior.
  169. // See comments in the implementation of `archive.PrepareArchiveCopy`
  170. // for exactly what goes into deciding how and whether the source
  171. // archive needs to be altered for the correct copy behavior when it is
  172. // extracted. This function also infers from the source and destination
  173. // info which directory to extract to, which may be the parent of the
  174. // destination that the user specified.
  175. // Don't create the archive if running in Dry Run mode
  176. if !s.dryRun {
  177. dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
  178. if err != nil {
  179. return err
  180. }
  181. defer preparedArchive.Close() //nolint:errcheck
  182. resolvedDstPath = dstDir
  183. content = preparedArchive
  184. }
  185. }
  186. options := moby.CopyToContainerOptions{
  187. AllowOverwriteDirWithFile: false,
  188. CopyUIDGID: opts.CopyUIDGID,
  189. }
  190. return s.apiClient().CopyToContainer(ctx, containerID, resolvedDstPath, content, options)
  191. }
  192. func (s *composeService) copyFromContainer(ctx context.Context, containerID, srcPath, dstPath string, opts api.CopyOptions) error {
  193. var err error
  194. if dstPath != "-" {
  195. // Get an absolute destination path.
  196. dstPath, err = resolveLocalPath(dstPath)
  197. if err != nil {
  198. return err
  199. }
  200. }
  201. if err := command.ValidateOutputPath(dstPath); err != nil {
  202. return err
  203. }
  204. // if client requests to follow symbol link, then must decide target file to be copied
  205. var rebaseName string
  206. if opts.FollowLink {
  207. srcStat, err := s.apiClient().ContainerStatPath(ctx, containerID, srcPath)
  208. // If the destination is a symbolic link, we should follow it.
  209. if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
  210. linkTarget := srcStat.LinkTarget
  211. if !system.IsAbs(linkTarget) {
  212. // Join with the parent directory.
  213. srcParent, _ := archive.SplitPathDirEntry(srcPath)
  214. linkTarget = filepath.Join(srcParent, linkTarget)
  215. }
  216. linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
  217. srcPath = linkTarget
  218. }
  219. }
  220. content, stat, err := s.apiClient().CopyFromContainer(ctx, containerID, srcPath)
  221. if err != nil {
  222. return err
  223. }
  224. defer content.Close() //nolint:errcheck
  225. if dstPath == "-" {
  226. _, err = io.Copy(s.stdout(), content)
  227. return err
  228. }
  229. srcInfo := archive.CopyInfo{
  230. Path: srcPath,
  231. Exists: true,
  232. IsDir: stat.Mode.IsDir(),
  233. RebaseName: rebaseName,
  234. }
  235. preArchive := content
  236. if srcInfo.RebaseName != "" {
  237. _, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
  238. preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
  239. }
  240. return archive.CopyTo(preArchive, srcInfo, dstPath)
  241. }
  242. func splitCpArg(arg string) (container, path string) {
  243. if system.IsAbs(arg) {
  244. // Explicit local absolute path, e.g., `C:\foo` or `/foo`.
  245. return "", arg
  246. }
  247. parts := strings.SplitN(arg, ":", 2)
  248. if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
  249. // Either there's no `:` in the arg
  250. // OR it's an explicit local relative path like `./file:name.txt`.
  251. return "", arg
  252. }
  253. return parts[0], parts[1]
  254. }
  255. func resolveLocalPath(localPath string) (absPath string, err error) {
  256. if absPath, err = filepath.Abs(localPath); err != nil {
  257. return
  258. }
  259. return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
  260. }