2016-09-08 13:11:39 -04:00
package container
import (
2018-05-03 21:02:44 -04:00
"context"
2020-09-04 16:23:13 -04:00
"fmt"
2016-09-08 13:11:39 -04:00
"io"
"os"
"path/filepath"
"strings"
2017-04-17 18:07:56 -04:00
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
2023-03-29 19:05:54 -04:00
"github.com/docker/cli/cli/streams"
2016-09-08 13:11:39 -04:00
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/system"
2020-09-04 16:23:13 -04:00
units "github.com/docker/go-units"
"github.com/morikuni/aec"
2017-03-09 13:23:45 -05:00
"github.com/pkg/errors"
2016-09-08 13:11:39 -04:00
"github.com/spf13/cobra"
)
type copyOptions struct {
source string
destination string
followLink bool
2016-11-14 08:37:08 -05:00
copyUIDGID bool
2020-09-04 16:23:13 -04:00
quiet bool
2016-09-08 13:11:39 -04:00
}
type copyDirection int
const (
2017-10-30 17:52:08 -04:00
fromContainer copyDirection = 1 << iota
2016-09-08 13:11:39 -04:00
toContainer
acrossContainers = fromContainer | toContainer
)
type cpConfig struct {
followLink bool
2017-10-30 17:52:08 -04:00
copyUIDGID bool
2020-09-04 16:23:13 -04:00
quiet bool
2017-10-30 17:52:08 -04:00
sourcePath string
destPath string
container string
2016-09-08 13:11:39 -04:00
}
2020-09-04 16:23:13 -04:00
// copyProgressPrinter wraps io.ReadCloser to print progress information when
// copying files to/from a container.
type copyProgressPrinter struct {
io . ReadCloser
2023-03-29 20:00:03 -04:00
total * float64
writer io . Writer
isTerm bool
header string
2020-09-04 16:23:13 -04:00
}
2023-03-29 20:00:03 -04:00
const (
copyToContainerHeader = "Copying to container - "
copyFromContainerHeader = "Copying from container - "
)
2020-09-04 16:23:13 -04:00
func ( pt * copyProgressPrinter ) Read ( p [ ] byte ) ( int , error ) {
n , err := pt . ReadCloser . Read ( p )
2023-03-29 20:00:03 -04:00
isFirst := * pt . total == 0
2023-03-29 19:05:54 -04:00
if n > 0 {
* pt . total += float64 ( n )
2023-03-29 20:00:03 -04:00
}
if err != nil && err != io . EOF {
return n , err
}
2023-03-29 19:05:54 -04:00
2023-03-29 20:00:03 -04:00
if ! pt . isTerm {
return n , err
}
if isFirst {
fmt . Fprint ( pt . writer , aec . Restore )
fmt . Fprint ( pt . writer , aec . EraseLine ( aec . EraseModes . All ) )
fmt . Fprint ( pt . writer , pt . header )
2020-09-04 16:23:13 -04:00
}
2023-03-29 20:00:03 -04:00
fmt . Fprint ( pt . writer , aec . Column ( uint ( len ( pt . header ) + 1 ) ) )
fmt . Fprint ( pt . writer , aec . EraseLine ( aec . EraseModes . Tail ) )
fmt . Fprint ( pt . writer , units . HumanSize ( * pt . total ) )
2020-09-04 16:23:13 -04:00
return n , err
}
2016-09-08 13:11:39 -04:00
// NewCopyCommand creates a new `docker cp` command
2017-10-11 12:18:27 -04:00
func NewCopyCommand ( dockerCli command . Cli ) * cobra . Command {
2016-09-08 13:11:39 -04:00
var opts copyOptions
cmd := & cobra . Command {
Use : ` cp [ OPTIONS ] CONTAINER : SRC_PATH DEST_PATH | -
docker cp [ OPTIONS ] SRC_PATH | - CONTAINER : DEST_PATH ` ,
Short : "Copy files/folders between a container and the local filesystem" ,
Long : strings . Join ( [ ] string {
"Copy files/folders between a container and the local filesystem\n" ,
"\nUse '-' as the source to read a tar archive from stdin\n" ,
"and extract it to a directory destination in a container.\n" ,
"Use '-' as the destination to stream a tar archive of a\n" ,
"container source to stdout." ,
} , "" ) ,
Args : cli . ExactArgs ( 2 ) ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
if args [ 0 ] == "" {
2016-12-24 19:05:37 -05:00
return errors . New ( "source can not be empty" )
2016-09-08 13:11:39 -04:00
}
if args [ 1 ] == "" {
2016-12-24 19:05:37 -05:00
return errors . New ( "destination can not be empty" )
2016-09-08 13:11:39 -04:00
}
opts . source = args [ 0 ]
opts . destination = args [ 1 ]
2020-09-04 16:23:13 -04:00
if ! cmd . Flag ( "quiet" ) . Changed {
// User did not specify "quiet" flag; suppress output if no terminal is attached
opts . quiet = ! dockerCli . Out ( ) . IsTerminal ( )
}
2016-09-08 13:11:39 -04:00
return runCopy ( dockerCli , opts )
} ,
cli: use custom annotation for aliases
Cobra allows for aliases to be defined for a command, but only allows these
to be defined at the same level (for example, `docker image ls` as alias for
`docker image list`). Our CLI has some commands that are available both as a
top-level shorthand as well as `docker <object> <verb>` subcommands. For example,
`docker ps` is a shorthand for `docker container ps` / `docker container ls`.
This patch introduces a custom "aliases" annotation that can be used to print
all available aliases for a command. While this requires these aliases to be
defined manually, in practice the list of aliases rarely changes, so maintenance
should be minimal.
As a convention, we could consider the first command in this list to be the
canonical command, so that we can use this information to add redirects in
our documentation in future.
Before this patch:
docker images --help
Usage: docker images [OPTIONS] [REPOSITORY[:TAG]]
List images
Options:
-a, --all Show all images (default hides intermediate images)
...
With this patch:
docker images --help
Usage: docker images [OPTIONS] [REPOSITORY[:TAG]]
List images
Aliases:
docker image ls, docker image list, docker images
Options:
-a, --all Show all images (default hides intermediate images)
...
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2022-06-28 04:52:25 -04:00
Annotations : map [ string ] string {
"aliases" : "docker container cp, docker cp" ,
} ,
2016-09-08 13:11:39 -04:00
}
flags := cmd . Flags ( )
flags . BoolVarP ( & opts . followLink , "follow-link" , "L" , false , "Always follow symbol link in SRC_PATH" )
2016-11-14 08:37:08 -05:00
flags . BoolVarP ( & opts . copyUIDGID , "archive" , "a" , false , "Archive mode (copy all uid/gid information)" )
2020-09-04 16:23:13 -04:00
flags . BoolVarP ( & opts . quiet , "quiet" , "q" , false , "Suppress progress output during copy. Progress output is automatically suppressed if no terminal is attached" )
2016-09-08 13:11:39 -04:00
return cmd
}
2017-10-11 12:18:27 -04:00
func runCopy ( dockerCli command . Cli , opts copyOptions ) error {
2016-09-08 13:11:39 -04:00
srcContainer , srcPath := splitCpArg ( opts . source )
2017-10-30 17:52:08 -04:00
destContainer , destPath := splitCpArg ( opts . destination )
copyConfig := cpConfig {
followLink : opts . followLink ,
copyUIDGID : opts . copyUIDGID ,
2020-09-04 16:23:13 -04:00
quiet : opts . quiet ,
2017-10-30 17:52:08 -04:00
sourcePath : srcPath ,
destPath : destPath ,
}
2016-09-08 13:11:39 -04:00
var direction copyDirection
if srcContainer != "" {
direction |= fromContainer
2017-10-30 17:52:08 -04:00
copyConfig . container = srcContainer
2016-09-08 13:11:39 -04:00
}
2017-10-30 17:52:08 -04:00
if destContainer != "" {
2016-09-08 13:11:39 -04:00
direction |= toContainer
2017-10-30 17:52:08 -04:00
copyConfig . container = destContainer
2016-09-08 13:11:39 -04:00
}
ctx := context . Background ( )
switch direction {
case fromContainer :
2017-10-30 17:52:08 -04:00
return copyFromContainer ( ctx , dockerCli , copyConfig )
2016-09-08 13:11:39 -04:00
case toContainer :
2017-10-30 17:52:08 -04:00
return copyToContainer ( ctx , dockerCli , copyConfig )
2016-09-08 13:11:39 -04:00
case acrossContainers :
2016-12-24 19:05:37 -05:00
return errors . New ( "copying between containers is not supported" )
2016-09-08 13:11:39 -04:00
default :
2016-12-24 19:05:37 -05:00
return errors . New ( "must specify at least one container source" )
2016-09-08 13:11:39 -04:00
}
}
func resolveLocalPath ( localPath string ) ( absPath string , err error ) {
if absPath , err = filepath . Abs ( localPath ) ; err != nil {
return
}
2017-09-29 08:18:19 -04:00
return archive . PreserveTrailingDotOrSeparator ( absPath , localPath , filepath . Separator ) , nil
2016-09-08 13:11:39 -04:00
}
2017-10-30 17:52:08 -04:00
func copyFromContainer ( ctx context . Context , dockerCli command . Cli , copyConfig cpConfig ) ( err error ) {
dstPath := copyConfig . destPath
srcPath := copyConfig . sourcePath
2016-09-08 13:11:39 -04:00
if dstPath != "-" {
// Get an absolute destination path.
dstPath , err = resolveLocalPath ( dstPath )
if err != nil {
return err
}
}
2019-02-07 03:17:35 -05:00
if err := command . ValidateOutputPath ( dstPath ) ; err != nil {
return err
}
2017-10-30 17:52:08 -04:00
client := dockerCli . Client ( )
2016-09-08 13:11:39 -04:00
// if client requests to follow symbol link, then must decide target file to be copied
var rebaseName string
2017-10-30 17:52:08 -04:00
if copyConfig . followLink {
srcStat , err := client . ContainerStatPath ( ctx , copyConfig . container , srcPath )
2016-09-08 13:11:39 -04:00
// If the destination is a symbolic link, we should follow it.
if err == nil && srcStat . Mode & os . ModeSymlink != 0 {
linkTarget := srcStat . LinkTarget
if ! system . IsAbs ( linkTarget ) {
// Join with the parent directory.
srcParent , _ := archive . SplitPathDirEntry ( srcPath )
linkTarget = filepath . Join ( srcParent , linkTarget )
}
linkTarget , rebaseName = archive . GetRebaseName ( srcPath , linkTarget )
srcPath = linkTarget
}
}
2017-10-30 17:52:08 -04:00
content , stat , err := client . CopyFromContainer ( ctx , copyConfig . container , srcPath )
2016-09-08 13:11:39 -04:00
if err != nil {
return err
}
defer content . Close ( )
if dstPath == "-" {
2017-10-30 17:52:08 -04:00
_ , err = io . Copy ( dockerCli . Out ( ) , content )
2016-09-08 13:11:39 -04:00
return err
}
srcInfo := archive . CopyInfo {
Path : srcPath ,
Exists : true ,
IsDir : stat . Mode . IsDir ( ) ,
RebaseName : rebaseName ,
}
2023-03-29 19:05:54 -04:00
stderrIsTerm := streams . NewOut ( dockerCli . Err ( ) ) . IsTerminal ( )
2020-09-04 16:23:13 -04:00
var copiedSize float64
if ! copyConfig . quiet {
content = & copyProgressPrinter {
2023-03-29 20:00:03 -04:00
ReadCloser : content ,
writer : dockerCli . Err ( ) ,
total : & copiedSize ,
isTerm : stderrIsTerm ,
header : copyFromContainerHeader ,
2020-09-04 16:23:13 -04:00
}
}
2016-09-08 13:11:39 -04:00
preArchive := content
if len ( srcInfo . RebaseName ) != 0 {
_ , srcBase := archive . SplitPathDirEntry ( srcInfo . Path )
preArchive = archive . RebaseArchiveEntries ( content , srcBase , srcInfo . RebaseName )
}
2020-09-04 16:23:13 -04:00
if copyConfig . quiet {
return archive . CopyTo ( preArchive , srcInfo , dstPath )
}
2023-03-29 19:05:54 -04:00
if stderrIsTerm {
2023-03-29 19:47:50 -04:00
fmt . Fprint ( dockerCli . Err ( ) , aec . Hide )
2023-03-29 19:05:54 -04:00
fmt . Fprint ( dockerCli . Err ( ) , aec . Save )
fmt . Fprintln ( dockerCli . Err ( ) , "Preparing to copy..." )
}
2020-09-04 16:23:13 -04:00
res := archive . CopyTo ( preArchive , srcInfo , dstPath )
2023-03-29 19:05:54 -04:00
if stderrIsTerm {
fmt . Fprint ( dockerCli . Err ( ) , aec . Restore )
fmt . Fprint ( dockerCli . Err ( ) , aec . EraseLine ( aec . EraseModes . All ) )
2023-03-29 19:47:50 -04:00
fmt . Fprint ( dockerCli . Err ( ) , aec . Show )
2023-03-29 19:05:54 -04:00
}
2020-09-04 16:23:13 -04:00
fmt . Fprintln ( dockerCli . Err ( ) , "Successfully copied" , units . HumanSize ( copiedSize ) , "to" , dstPath )
return res
2016-09-08 13:11:39 -04:00
}
2017-10-30 17:52:08 -04:00
// In order to get the copy behavior right, we need to know information
// about both the source and destination. The API is a simple tar
// archive/extract API but we can use the stat info header about the
// destination to be more informed about exactly what the destination is.
func copyToContainer ( ctx context . Context , dockerCli command . Cli , copyConfig cpConfig ) ( err error ) {
srcPath := copyConfig . sourcePath
dstPath := copyConfig . destPath
2016-09-08 13:11:39 -04:00
if srcPath != "-" {
// Get an absolute source path.
srcPath , err = resolveLocalPath ( srcPath )
if err != nil {
return err
}
}
2017-10-30 17:52:08 -04:00
client := dockerCli . Client ( )
2016-09-08 13:11:39 -04:00
// Prepare destination copy info by stat-ing the container path.
dstInfo := archive . CopyInfo { Path : dstPath }
2017-10-30 17:52:08 -04:00
dstStat , err := client . ContainerStatPath ( ctx , copyConfig . container , dstPath )
2016-09-08 13:11:39 -04:00
// If the destination is a symbolic link, we should evaluate it.
if err == nil && dstStat . Mode & os . ModeSymlink != 0 {
linkTarget := dstStat . LinkTarget
if ! system . IsAbs ( linkTarget ) {
// Join with the parent directory.
dstParent , _ := archive . SplitPathDirEntry ( dstPath )
linkTarget = filepath . Join ( dstParent , linkTarget )
}
dstInfo . Path = linkTarget
2017-10-30 17:52:08 -04:00
dstStat , err = client . ContainerStatPath ( ctx , copyConfig . container , linkTarget )
2016-09-08 13:11:39 -04:00
}
2019-02-07 03:17:35 -05:00
// Validate the destination path
if err := command . ValidateOutputPathFileMode ( dstStat . Mode ) ; err != nil {
return errors . Wrapf ( err , ` destination "%s:%s" must be a directory or a regular file ` , copyConfig . container , dstPath )
}
2016-09-08 13:11:39 -04:00
// Ignore any error and assume that the parent directory of the destination
// path exists, in which case the copy may still succeed. If there is any
// type of conflict (e.g., non-directory overwriting an existing directory
// or vice versa) the extraction will fail. If the destination simply did
// not exist, but the parent directory does, the extraction will still
// succeed.
if err == nil {
dstInfo . Exists , dstInfo . IsDir = true , dstStat . Mode . IsDir ( )
}
var (
2020-09-04 16:23:13 -04:00
content io . ReadCloser
2016-09-08 13:11:39 -04:00
resolvedDstPath string
2020-09-04 16:23:13 -04:00
copiedSize float64
2016-09-08 13:11:39 -04:00
)
2023-03-29 19:05:54 -04:00
stderrIsTerm := streams . NewOut ( dockerCli . Err ( ) ) . IsTerminal ( )
2016-09-08 13:11:39 -04:00
if srcPath == "-" {
content = os . Stdin
resolvedDstPath = dstInfo . Path
if ! dstInfo . IsDir {
2017-10-30 17:52:08 -04:00
return errors . Errorf ( "destination \"%s:%s\" must be a directory" , copyConfig . container , dstPath )
2016-09-08 13:11:39 -04:00
}
} else {
// Prepare source copy info.
2017-10-30 17:52:08 -04:00
srcInfo , err := archive . CopyInfoSourcePath ( srcPath , copyConfig . followLink )
2016-09-08 13:11:39 -04:00
if err != nil {
return err
}
srcArchive , err := archive . TarResource ( srcInfo )
if err != nil {
return err
}
defer srcArchive . Close ( )
// With the stat info about the local source as well as the
// destination, we have enough information to know whether we need to
// alter the archive that we upload so that when the server extracts
// it to the specified directory in the container we get the desired
// copy behavior.
// See comments in the implementation of `archive.PrepareArchiveCopy`
// for exactly what goes into deciding how and whether the source
// archive needs to be altered for the correct copy behavior when it is
// extracted. This function also infers from the source and destination
// info which directory to extract to, which may be the parent of the
// destination that the user specified.
dstDir , preparedArchive , err := archive . PrepareArchiveCopy ( srcArchive , srcInfo , dstInfo )
if err != nil {
return err
}
defer preparedArchive . Close ( )
resolvedDstPath = dstDir
content = preparedArchive
2020-09-04 16:23:13 -04:00
if ! copyConfig . quiet {
content = & copyProgressPrinter {
2023-03-29 20:00:03 -04:00
ReadCloser : content ,
writer : dockerCli . Err ( ) ,
total : & copiedSize ,
isTerm : stderrIsTerm ,
header : copyToContainerHeader ,
2020-09-04 16:23:13 -04:00
}
}
2016-09-08 13:11:39 -04:00
}
options := types . CopyToContainerOptions {
AllowOverwriteDirWithFile : false ,
2017-10-30 17:52:08 -04:00
CopyUIDGID : copyConfig . copyUIDGID ,
2016-09-08 13:11:39 -04:00
}
2020-09-04 16:23:13 -04:00
if copyConfig . quiet {
return client . CopyToContainer ( ctx , copyConfig . container , resolvedDstPath , content , options )
}
2023-03-29 19:05:54 -04:00
if stderrIsTerm {
2023-03-29 19:47:50 -04:00
fmt . Fprint ( dockerCli . Err ( ) , aec . Hide )
2023-03-29 19:05:54 -04:00
fmt . Fprint ( dockerCli . Err ( ) , aec . Save )
fmt . Fprintln ( dockerCli . Err ( ) , "Preparing to copy..." )
}
2020-09-04 16:23:13 -04:00
res := client . CopyToContainer ( ctx , copyConfig . container , resolvedDstPath , content , options )
2023-03-29 19:05:54 -04:00
if stderrIsTerm {
fmt . Fprint ( dockerCli . Err ( ) , aec . Restore )
fmt . Fprint ( dockerCli . Err ( ) , aec . EraseLine ( aec . EraseModes . All ) )
2023-03-29 19:47:50 -04:00
fmt . Fprint ( dockerCli . Err ( ) , aec . Show )
2023-03-29 19:05:54 -04:00
}
2020-09-04 16:23:13 -04:00
fmt . Fprintln ( dockerCli . Err ( ) , "Successfully copied" , units . HumanSize ( copiedSize ) , "to" , copyConfig . container + ":" + dstInfo . Path )
return res
2016-09-08 13:11:39 -04:00
}
// We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be
// in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by
// requiring a LOCALPATH with a `:` to be made explicit with a relative or
// absolute path:
2022-07-13 06:29:49 -04:00
//
// `/path/to/file:name.txt` or `./file:name.txt`
2016-09-08 13:11:39 -04:00
//
// This is apparently how `scp` handles this as well:
2022-07-13 06:29:49 -04:00
//
// http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/
2016-09-08 13:11:39 -04:00
//
// We can't simply check for a filepath separator because container names may
// have a separator, e.g., "host0/cname1" if container is in a Docker cluster,
// so we have to check for a `/` or `.` prefix. Also, in the case of a Windows
// client, a `:` could be part of an absolute Windows path, in which case it
// is immediately proceeded by a backslash.
func splitCpArg ( arg string ) ( container , path string ) {
if system . IsAbs ( arg ) {
// Explicit local absolute path, e.g., `C:\foo` or `/foo`.
return "" , arg
}
2022-12-27 10:26:08 -05:00
container , path , ok := strings . Cut ( arg , ":" )
if ! ok || strings . HasPrefix ( container , "." ) {
2016-09-08 13:11:39 -04:00
// Either there's no `:` in the arg
// OR it's an explicit local relative path like `./file:name.txt`.
return "" , arg
}
2022-12-27 10:26:08 -05:00
return container , path
2016-09-08 13:11:39 -04:00
}