cp.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  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, project string, opts api.CopyOptions) error {
  36. srcService, srcPath := splitCpArg(opts.Source)
  37. destService, dstPath := splitCpArg(opts.Destination)
  38. var direction copyDirection
  39. var serviceName string
  40. if srcService != "" {
  41. direction |= fromService
  42. serviceName = srcService
  43. // copying from multiple containers of a services doesn't make sense.
  44. if opts.All {
  45. return errors.New("cannot use the --all flag when copying from a service")
  46. }
  47. }
  48. if destService != "" {
  49. direction |= toService
  50. serviceName = destService
  51. }
  52. containers, err := s.getContainers(ctx, project, oneOffExclude, true, serviceName)
  53. if err != nil {
  54. return err
  55. }
  56. if len(containers) < 1 {
  57. return fmt.Errorf("no container found for service %q", serviceName)
  58. }
  59. if !opts.All {
  60. containers = containers.filter(indexed(opts.Index))
  61. }
  62. g := errgroup.Group{}
  63. for _, container := range containers {
  64. containerID := container.ID
  65. g.Go(func() error {
  66. switch direction {
  67. case fromService:
  68. return s.copyFromContainer(ctx, containerID, srcPath, dstPath, opts)
  69. case toService:
  70. return s.copyToContainer(ctx, containerID, srcPath, dstPath, opts)
  71. case acrossServices:
  72. return errors.New("copying between services is not supported")
  73. default:
  74. return errors.New("unknown copy direction")
  75. }
  76. })
  77. }
  78. return g.Wait()
  79. }
  80. func (s *composeService) copyToContainer(ctx context.Context, containerID string, srcPath string, dstPath string, opts api.CopyOptions) error {
  81. var err error
  82. if srcPath != "-" {
  83. // Get an absolute source path.
  84. srcPath, err = resolveLocalPath(srcPath)
  85. if err != nil {
  86. return err
  87. }
  88. }
  89. // Prepare destination copy info by stat-ing the container path.
  90. dstInfo := archive.CopyInfo{Path: dstPath}
  91. dstStat, err := s.apiClient().ContainerStatPath(ctx, containerID, dstPath)
  92. // If the destination is a symbolic link, we should evaluate it.
  93. if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
  94. linkTarget := dstStat.LinkTarget
  95. if !system.IsAbs(linkTarget) {
  96. // Join with the parent directory.
  97. dstParent, _ := archive.SplitPathDirEntry(dstPath)
  98. linkTarget = filepath.Join(dstParent, linkTarget)
  99. }
  100. dstInfo.Path = linkTarget
  101. dstStat, err = s.apiClient().ContainerStatPath(ctx, containerID, linkTarget)
  102. }
  103. // Validate the destination path
  104. if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil {
  105. return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, containerID, dstPath)
  106. }
  107. // Ignore any error and assume that the parent directory of the destination
  108. // path exists, in which case the copy may still succeed. If there is any
  109. // type of conflict (e.g., non-directory overwriting an existing directory
  110. // or vice versa) the extraction will fail. If the destination simply did
  111. // not exist, but the parent directory does, the extraction will still
  112. // succeed.
  113. if err == nil {
  114. dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
  115. }
  116. var (
  117. content io.Reader
  118. resolvedDstPath string
  119. )
  120. if srcPath == "-" {
  121. content = s.stdin()
  122. resolvedDstPath = dstInfo.Path
  123. if !dstInfo.IsDir {
  124. return errors.Errorf("destination \"%s:%s\" must be a directory", containerID, dstPath)
  125. }
  126. } else {
  127. // Prepare source copy info.
  128. srcInfo, err := archive.CopyInfoSourcePath(srcPath, opts.FollowLink)
  129. if err != nil {
  130. return err
  131. }
  132. srcArchive, err := archive.TarResource(srcInfo)
  133. if err != nil {
  134. return err
  135. }
  136. defer srcArchive.Close() //nolint:errcheck
  137. // With the stat info about the local source as well as the
  138. // destination, we have enough information to know whether we need to
  139. // alter the archive that we upload so that when the server extracts
  140. // it to the specified directory in the container we get the desired
  141. // copy behavior.
  142. // See comments in the implementation of `archive.PrepareArchiveCopy`
  143. // for exactly what goes into deciding how and whether the source
  144. // archive needs to be altered for the correct copy behavior when it is
  145. // extracted. This function also infers from the source and destination
  146. // info which directory to extract to, which may be the parent of the
  147. // destination that the user specified.
  148. dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
  149. if err != nil {
  150. return err
  151. }
  152. defer preparedArchive.Close() //nolint:errcheck
  153. resolvedDstPath = dstDir
  154. content = preparedArchive
  155. }
  156. options := moby.CopyToContainerOptions{
  157. AllowOverwriteDirWithFile: false,
  158. CopyUIDGID: opts.CopyUIDGID,
  159. }
  160. return s.apiClient().CopyToContainer(ctx, containerID, resolvedDstPath, content, options)
  161. }
  162. func (s *composeService) copyFromContainer(ctx context.Context, containerID, srcPath, dstPath string, opts api.CopyOptions) error {
  163. var err error
  164. if dstPath != "-" {
  165. // Get an absolute destination path.
  166. dstPath, err = resolveLocalPath(dstPath)
  167. if err != nil {
  168. return err
  169. }
  170. }
  171. if err := command.ValidateOutputPath(dstPath); err != nil {
  172. return err
  173. }
  174. // if client requests to follow symbol link, then must decide target file to be copied
  175. var rebaseName string
  176. if opts.FollowLink {
  177. srcStat, err := s.apiClient().ContainerStatPath(ctx, containerID, srcPath)
  178. // If the destination is a symbolic link, we should follow it.
  179. if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
  180. linkTarget := srcStat.LinkTarget
  181. if !system.IsAbs(linkTarget) {
  182. // Join with the parent directory.
  183. srcParent, _ := archive.SplitPathDirEntry(srcPath)
  184. linkTarget = filepath.Join(srcParent, linkTarget)
  185. }
  186. linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
  187. srcPath = linkTarget
  188. }
  189. }
  190. content, stat, err := s.apiClient().CopyFromContainer(ctx, containerID, srcPath)
  191. if err != nil {
  192. return err
  193. }
  194. defer content.Close() //nolint:errcheck
  195. if dstPath == "-" {
  196. _, err = io.Copy(s.stdout(), content)
  197. return err
  198. }
  199. srcInfo := archive.CopyInfo{
  200. Path: srcPath,
  201. Exists: true,
  202. IsDir: stat.Mode.IsDir(),
  203. RebaseName: rebaseName,
  204. }
  205. preArchive := content
  206. if len(srcInfo.RebaseName) != 0 {
  207. _, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
  208. preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
  209. }
  210. return archive.CopyTo(preArchive, srcInfo, dstPath)
  211. }
  212. func splitCpArg(arg string) (container, path string) {
  213. if system.IsAbs(arg) {
  214. // Explicit local absolute path, e.g., `C:\foo` or `/foo`.
  215. return "", arg
  216. }
  217. parts := strings.SplitN(arg, ":", 2)
  218. if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
  219. // Either there's no `:` in the arg
  220. // OR it's an explicit local relative path like `./file:name.txt`.
  221. return "", arg
  222. }
  223. return parts[0], parts[1]
  224. }
  225. func resolveLocalPath(localPath string) (absPath string, err error) {
  226. if absPath, err = filepath.Abs(localPath); err != nil {
  227. return
  228. }
  229. return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil
  230. }