cp.go 8.1 KB

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