cp.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  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. "errors"
  17. "fmt"
  18. "io"
  19. "os"
  20. "path/filepath"
  21. "strings"
  22. "github.com/docker/compose/v2/pkg/progress"
  23. "golang.org/x/sync/errgroup"
  24. "github.com/docker/cli/cli/command"
  25. "github.com/docker/compose/v2/pkg/api"
  26. "github.com/docker/docker/api/types/container"
  27. "github.com/moby/go-archive"
  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. return progress.RunWithTitle(ctx, func(ctx context.Context) error {
  37. return s.copy(ctx, projectName, options)
  38. }, s.stdinfo(), "Copying")
  39. }
  40. func (s *composeService) copy(ctx context.Context, projectName string, options api.CopyOptions) error {
  41. projectName = strings.ToLower(projectName)
  42. srcService, srcPath := splitCpArg(options.Source)
  43. destService, dstPath := splitCpArg(options.Destination)
  44. var direction copyDirection
  45. var serviceName string
  46. var copyFunc func(ctx context.Context, containerID string, srcPath string, dstPath string, opts api.CopyOptions) error
  47. if srcService != "" {
  48. direction |= fromService
  49. serviceName = srcService
  50. copyFunc = s.copyFromContainer
  51. }
  52. if destService != "" {
  53. direction |= toService
  54. serviceName = destService
  55. copyFunc = s.copyToContainer
  56. }
  57. if direction == acrossServices {
  58. return errors.New("copying between services is not supported")
  59. }
  60. if direction == 0 {
  61. return errors.New("unknown copy direction")
  62. }
  63. containers, err := s.listContainersTargetedForCopy(ctx, projectName, options, direction, serviceName)
  64. if err != nil {
  65. return err
  66. }
  67. w := progress.ContextWriter(ctx)
  68. g := errgroup.Group{}
  69. for _, cont := range containers {
  70. ctr := cont
  71. g.Go(func() error {
  72. name := getCanonicalContainerName(ctr)
  73. var msg string
  74. if direction == fromService {
  75. msg = fmt.Sprintf("copy %s:%s to %s", name, srcPath, dstPath)
  76. } else {
  77. msg = fmt.Sprintf("copy %s to %s:%s", srcPath, name, dstPath)
  78. }
  79. w.Event(progress.Event{
  80. ID: name,
  81. Text: msg,
  82. Status: progress.Working,
  83. StatusText: "Copying",
  84. })
  85. if err := copyFunc(ctx, ctr.ID, srcPath, dstPath, options); err != nil {
  86. return err
  87. }
  88. w.Event(progress.Event{
  89. ID: name,
  90. Text: msg,
  91. Status: progress.Done,
  92. StatusText: "Copied",
  93. })
  94. return nil
  95. })
  96. }
  97. return g.Wait()
  98. }
  99. func (s *composeService) listContainersTargetedForCopy(ctx context.Context, projectName string, options api.CopyOptions, direction copyDirection, serviceName string) (Containers, error) {
  100. var containers Containers
  101. var err error
  102. switch {
  103. case options.Index > 0:
  104. ctr, err := s.getSpecifiedContainer(ctx, projectName, oneOffExclude, true, serviceName, options.Index)
  105. if err != nil {
  106. return nil, err
  107. }
  108. return append(containers, ctr), nil
  109. default:
  110. withOneOff := oneOffExclude
  111. if options.All {
  112. withOneOff = oneOffInclude
  113. }
  114. containers, err = s.getContainers(ctx, projectName, withOneOff, true, serviceName)
  115. if err != nil {
  116. return nil, err
  117. }
  118. if len(containers) < 1 {
  119. return nil, fmt.Errorf("no container found for service %q", serviceName)
  120. }
  121. if direction == fromService {
  122. return containers[:1], err
  123. }
  124. return containers, err
  125. }
  126. }
  127. func (s *composeService) copyToContainer(ctx context.Context, containerID string, srcPath string, dstPath string, opts api.CopyOptions) error {
  128. var err error
  129. if srcPath != "-" {
  130. // Get an absolute source path.
  131. srcPath, err = resolveLocalPath(srcPath)
  132. if err != nil {
  133. return err
  134. }
  135. }
  136. // Prepare destination copy info by stat-ing the container path.
  137. dstInfo := archive.CopyInfo{Path: dstPath}
  138. dstStat, err := s.apiClient().ContainerStatPath(ctx, containerID, dstPath)
  139. // If the destination is a symbolic link, we should evaluate it.
  140. if err == nil && dstStat.Mode&os.ModeSymlink != 0 {
  141. linkTarget := dstStat.LinkTarget
  142. if !isAbs(linkTarget) {
  143. // Join with the parent directory.
  144. dstParent, _ := archive.SplitPathDirEntry(dstPath)
  145. linkTarget = filepath.Join(dstParent, linkTarget)
  146. }
  147. dstInfo.Path = linkTarget
  148. dstStat, err = s.apiClient().ContainerStatPath(ctx, containerID, linkTarget)
  149. }
  150. // Validate the destination path
  151. if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil {
  152. return fmt.Errorf(`destination "%s:%s" must be a directory or a regular file: %w`, containerID, dstPath, err)
  153. }
  154. // Ignore any error and assume that the parent directory of the destination
  155. // path exists, in which case the copy may still succeed. If there is any
  156. // type of conflict (e.g., non-directory overwriting an existing directory
  157. // or vice versa) the extraction will fail. If the destination simply did
  158. // not exist, but the parent directory does, the extraction will still
  159. // succeed.
  160. if err == nil {
  161. dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir()
  162. }
  163. var (
  164. content io.Reader
  165. resolvedDstPath string
  166. )
  167. if srcPath == "-" {
  168. content = s.stdin()
  169. resolvedDstPath = dstInfo.Path
  170. if !dstInfo.IsDir {
  171. return fmt.Errorf("destination \"%s:%s\" must be a directory", containerID, dstPath)
  172. }
  173. } else {
  174. // Prepare source copy info.
  175. srcInfo, err := archive.CopyInfoSourcePath(srcPath, opts.FollowLink)
  176. if err != nil {
  177. return err
  178. }
  179. srcArchive, err := archive.TarResource(srcInfo)
  180. if err != nil {
  181. return err
  182. }
  183. defer srcArchive.Close() //nolint:errcheck
  184. // With the stat info about the local source as well as the
  185. // destination, we have enough information to know whether we need to
  186. // alter the archive that we upload so that when the server extracts
  187. // it to the specified directory in the container we get the desired
  188. // copy behavior.
  189. // See comments in the implementation of `archive.PrepareArchiveCopy`
  190. // for exactly what goes into deciding how and whether the source
  191. // archive needs to be altered for the correct copy behavior when it is
  192. // extracted. This function also infers from the source and destination
  193. // info which directory to extract to, which may be the parent of the
  194. // destination that the user specified.
  195. // Don't create the archive if running in Dry Run mode
  196. if !s.dryRun {
  197. dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo)
  198. if err != nil {
  199. return err
  200. }
  201. defer preparedArchive.Close() //nolint:errcheck
  202. resolvedDstPath = dstDir
  203. content = preparedArchive
  204. }
  205. }
  206. options := container.CopyToContainerOptions{
  207. AllowOverwriteDirWithFile: false,
  208. CopyUIDGID: opts.CopyUIDGID,
  209. }
  210. return s.apiClient().CopyToContainer(ctx, containerID, resolvedDstPath, content, options)
  211. }
  212. func (s *composeService) copyFromContainer(ctx context.Context, containerID, srcPath, dstPath string, opts api.CopyOptions) error {
  213. var err error
  214. if dstPath != "-" {
  215. // Get an absolute destination path.
  216. dstPath, err = resolveLocalPath(dstPath)
  217. if err != nil {
  218. return err
  219. }
  220. }
  221. if err := command.ValidateOutputPath(dstPath); err != nil {
  222. return err
  223. }
  224. // if client requests to follow symbol link, then must decide target file to be copied
  225. var rebaseName string
  226. if opts.FollowLink {
  227. srcStat, err := s.apiClient().ContainerStatPath(ctx, containerID, srcPath)
  228. // If the destination is a symbolic link, we should follow it.
  229. if err == nil && srcStat.Mode&os.ModeSymlink != 0 {
  230. linkTarget := srcStat.LinkTarget
  231. if !isAbs(linkTarget) {
  232. // Join with the parent directory.
  233. srcParent, _ := archive.SplitPathDirEntry(srcPath)
  234. linkTarget = filepath.Join(srcParent, linkTarget)
  235. }
  236. linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget)
  237. srcPath = linkTarget
  238. }
  239. }
  240. content, stat, err := s.apiClient().CopyFromContainer(ctx, containerID, srcPath)
  241. if err != nil {
  242. return err
  243. }
  244. defer content.Close() //nolint:errcheck
  245. if dstPath == "-" {
  246. _, err = io.Copy(s.stdout(), content)
  247. return err
  248. }
  249. srcInfo := archive.CopyInfo{
  250. Path: srcPath,
  251. Exists: true,
  252. IsDir: stat.Mode.IsDir(),
  253. RebaseName: rebaseName,
  254. }
  255. preArchive := content
  256. if srcInfo.RebaseName != "" {
  257. _, srcBase := archive.SplitPathDirEntry(srcInfo.Path)
  258. preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName)
  259. }
  260. return archive.CopyTo(preArchive, srcInfo, dstPath)
  261. }
  262. // IsAbs is a platform-agnostic wrapper for filepath.IsAbs.
  263. //
  264. // On Windows, golang filepath.IsAbs does not consider a path \windows\system32
  265. // as absolute as it doesn't start with a drive-letter/colon combination. However,
  266. // in docker we need to verify things such as WORKDIR /windows/system32 in
  267. // a Dockerfile (which gets translated to \windows\system32 when being processed
  268. // by the daemon). This SHOULD be treated as absolute from a docker processing
  269. // perspective.
  270. func isAbs(path string) bool {
  271. return filepath.IsAbs(path) || strings.HasPrefix(path, string(os.PathSeparator))
  272. }
  273. func splitCpArg(arg string) (ctr, path string) {
  274. if isAbs(arg) {
  275. // Explicit local absolute path, e.g., `C:\foo` or `/foo`.
  276. return "", arg
  277. }
  278. parts := strings.SplitN(arg, ":", 2)
  279. if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
  280. // Either there's no `:` in the arg
  281. // OR it's an explicit local relative path like `./file:name.txt`.
  282. return "", arg
  283. }
  284. return parts[0], parts[1]
  285. }
  286. func resolveLocalPath(localPath string) (absPath string, err error) {
  287. if absPath, err = filepath.Abs(localPath); err != nil {
  288. return
  289. }
  290. return archive.PreserveTrailingDotOrSeparator(absPath, localPath), nil
  291. }