cp.go 9.5 KB

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