github.com/cspotcode/docker-cli@v20.10.0-rc1.0.20201201121459-3faad7acc5b8+incompatible/cli/command/container/cp.go (about) 1 package container 2 3 import ( 4 "context" 5 "io" 6 "os" 7 "path/filepath" 8 "strings" 9 10 "github.com/docker/cli/cli" 11 "github.com/docker/cli/cli/command" 12 "github.com/docker/docker/api/types" 13 "github.com/docker/docker/pkg/archive" 14 "github.com/docker/docker/pkg/system" 15 "github.com/pkg/errors" 16 "github.com/spf13/cobra" 17 ) 18 19 type copyOptions struct { 20 source string 21 destination string 22 followLink bool 23 copyUIDGID bool 24 } 25 26 type copyDirection int 27 28 const ( 29 fromContainer copyDirection = 1 << iota 30 toContainer 31 acrossContainers = fromContainer | toContainer 32 ) 33 34 type cpConfig struct { 35 followLink bool 36 copyUIDGID bool 37 sourcePath string 38 destPath string 39 container string 40 } 41 42 // NewCopyCommand creates a new `docker cp` command 43 func NewCopyCommand(dockerCli command.Cli) *cobra.Command { 44 var opts copyOptions 45 46 cmd := &cobra.Command{ 47 Use: `cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|- 48 docker cp [OPTIONS] SRC_PATH|- CONTAINER:DEST_PATH`, 49 Short: "Copy files/folders between a container and the local filesystem", 50 Long: strings.Join([]string{ 51 "Copy files/folders between a container and the local filesystem\n", 52 "\nUse '-' as the source to read a tar archive from stdin\n", 53 "and extract it to a directory destination in a container.\n", 54 "Use '-' as the destination to stream a tar archive of a\n", 55 "container source to stdout.", 56 }, ""), 57 Args: cli.ExactArgs(2), 58 RunE: func(cmd *cobra.Command, args []string) error { 59 if args[0] == "" { 60 return errors.New("source can not be empty") 61 } 62 if args[1] == "" { 63 return errors.New("destination can not be empty") 64 } 65 opts.source = args[0] 66 opts.destination = args[1] 67 return runCopy(dockerCli, opts) 68 }, 69 } 70 71 flags := cmd.Flags() 72 flags.BoolVarP(&opts.followLink, "follow-link", "L", false, "Always follow symbol link in SRC_PATH") 73 flags.BoolVarP(&opts.copyUIDGID, "archive", "a", false, "Archive mode (copy all uid/gid information)") 74 return cmd 75 } 76 77 func runCopy(dockerCli command.Cli, opts copyOptions) error { 78 srcContainer, srcPath := splitCpArg(opts.source) 79 destContainer, destPath := splitCpArg(opts.destination) 80 81 copyConfig := cpConfig{ 82 followLink: opts.followLink, 83 copyUIDGID: opts.copyUIDGID, 84 sourcePath: srcPath, 85 destPath: destPath, 86 } 87 88 var direction copyDirection 89 if srcContainer != "" { 90 direction |= fromContainer 91 copyConfig.container = srcContainer 92 } 93 if destContainer != "" { 94 direction |= toContainer 95 copyConfig.container = destContainer 96 } 97 98 ctx := context.Background() 99 100 switch direction { 101 case fromContainer: 102 return copyFromContainer(ctx, dockerCli, copyConfig) 103 case toContainer: 104 return copyToContainer(ctx, dockerCli, copyConfig) 105 case acrossContainers: 106 return errors.New("copying between containers is not supported") 107 default: 108 return errors.New("must specify at least one container source") 109 } 110 } 111 112 func resolveLocalPath(localPath string) (absPath string, err error) { 113 if absPath, err = filepath.Abs(localPath); err != nil { 114 return 115 } 116 return archive.PreserveTrailingDotOrSeparator(absPath, localPath, filepath.Separator), nil 117 } 118 119 func copyFromContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) { 120 dstPath := copyConfig.destPath 121 srcPath := copyConfig.sourcePath 122 123 if dstPath != "-" { 124 // Get an absolute destination path. 125 dstPath, err = resolveLocalPath(dstPath) 126 if err != nil { 127 return err 128 } 129 } 130 131 if err := command.ValidateOutputPath(dstPath); err != nil { 132 return err 133 } 134 135 client := dockerCli.Client() 136 // if client requests to follow symbol link, then must decide target file to be copied 137 var rebaseName string 138 if copyConfig.followLink { 139 srcStat, err := client.ContainerStatPath(ctx, copyConfig.container, srcPath) 140 141 // If the destination is a symbolic link, we should follow it. 142 if err == nil && srcStat.Mode&os.ModeSymlink != 0 { 143 linkTarget := srcStat.LinkTarget 144 if !system.IsAbs(linkTarget) { 145 // Join with the parent directory. 146 srcParent, _ := archive.SplitPathDirEntry(srcPath) 147 linkTarget = filepath.Join(srcParent, linkTarget) 148 } 149 150 linkTarget, rebaseName = archive.GetRebaseName(srcPath, linkTarget) 151 srcPath = linkTarget 152 } 153 154 } 155 156 content, stat, err := client.CopyFromContainer(ctx, copyConfig.container, srcPath) 157 if err != nil { 158 return err 159 } 160 defer content.Close() 161 162 if dstPath == "-" { 163 _, err = io.Copy(dockerCli.Out(), content) 164 return err 165 } 166 167 srcInfo := archive.CopyInfo{ 168 Path: srcPath, 169 Exists: true, 170 IsDir: stat.Mode.IsDir(), 171 RebaseName: rebaseName, 172 } 173 174 preArchive := content 175 if len(srcInfo.RebaseName) != 0 { 176 _, srcBase := archive.SplitPathDirEntry(srcInfo.Path) 177 preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName) 178 } 179 return archive.CopyTo(preArchive, srcInfo, dstPath) 180 } 181 182 // In order to get the copy behavior right, we need to know information 183 // about both the source and destination. The API is a simple tar 184 // archive/extract API but we can use the stat info header about the 185 // destination to be more informed about exactly what the destination is. 186 func copyToContainer(ctx context.Context, dockerCli command.Cli, copyConfig cpConfig) (err error) { 187 srcPath := copyConfig.sourcePath 188 dstPath := copyConfig.destPath 189 190 if srcPath != "-" { 191 // Get an absolute source path. 192 srcPath, err = resolveLocalPath(srcPath) 193 if err != nil { 194 return err 195 } 196 } 197 198 client := dockerCli.Client() 199 // Prepare destination copy info by stat-ing the container path. 200 dstInfo := archive.CopyInfo{Path: dstPath} 201 dstStat, err := client.ContainerStatPath(ctx, copyConfig.container, dstPath) 202 203 // If the destination is a symbolic link, we should evaluate it. 204 if err == nil && dstStat.Mode&os.ModeSymlink != 0 { 205 linkTarget := dstStat.LinkTarget 206 if !system.IsAbs(linkTarget) { 207 // Join with the parent directory. 208 dstParent, _ := archive.SplitPathDirEntry(dstPath) 209 linkTarget = filepath.Join(dstParent, linkTarget) 210 } 211 212 dstInfo.Path = linkTarget 213 dstStat, err = client.ContainerStatPath(ctx, copyConfig.container, linkTarget) 214 } 215 216 // Validate the destination path 217 if err := command.ValidateOutputPathFileMode(dstStat.Mode); err != nil { 218 return errors.Wrapf(err, `destination "%s:%s" must be a directory or a regular file`, copyConfig.container, dstPath) 219 } 220 221 // Ignore any error and assume that the parent directory of the destination 222 // path exists, in which case the copy may still succeed. If there is any 223 // type of conflict (e.g., non-directory overwriting an existing directory 224 // or vice versa) the extraction will fail. If the destination simply did 225 // not exist, but the parent directory does, the extraction will still 226 // succeed. 227 if err == nil { 228 dstInfo.Exists, dstInfo.IsDir = true, dstStat.Mode.IsDir() 229 } 230 231 var ( 232 content io.Reader 233 resolvedDstPath string 234 ) 235 236 if srcPath == "-" { 237 content = os.Stdin 238 resolvedDstPath = dstInfo.Path 239 if !dstInfo.IsDir { 240 return errors.Errorf("destination \"%s:%s\" must be a directory", copyConfig.container, dstPath) 241 } 242 } else { 243 // Prepare source copy info. 244 srcInfo, err := archive.CopyInfoSourcePath(srcPath, copyConfig.followLink) 245 if err != nil { 246 return err 247 } 248 249 srcArchive, err := archive.TarResource(srcInfo) 250 if err != nil { 251 return err 252 } 253 defer srcArchive.Close() 254 255 // With the stat info about the local source as well as the 256 // destination, we have enough information to know whether we need to 257 // alter the archive that we upload so that when the server extracts 258 // it to the specified directory in the container we get the desired 259 // copy behavior. 260 261 // See comments in the implementation of `archive.PrepareArchiveCopy` 262 // for exactly what goes into deciding how and whether the source 263 // archive needs to be altered for the correct copy behavior when it is 264 // extracted. This function also infers from the source and destination 265 // info which directory to extract to, which may be the parent of the 266 // destination that the user specified. 267 dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) 268 if err != nil { 269 return err 270 } 271 defer preparedArchive.Close() 272 273 resolvedDstPath = dstDir 274 content = preparedArchive 275 } 276 277 options := types.CopyToContainerOptions{ 278 AllowOverwriteDirWithFile: false, 279 CopyUIDGID: copyConfig.copyUIDGID, 280 } 281 return client.CopyToContainer(ctx, copyConfig.container, resolvedDstPath, content, options) 282 } 283 284 // We use `:` as a delimiter between CONTAINER and PATH, but `:` could also be 285 // in a valid LOCALPATH, like `file:name.txt`. We can resolve this ambiguity by 286 // requiring a LOCALPATH with a `:` to be made explicit with a relative or 287 // absolute path: 288 // `/path/to/file:name.txt` or `./file:name.txt` 289 // 290 // This is apparently how `scp` handles this as well: 291 // http://www.cyberciti.biz/faq/rsync-scp-file-name-with-colon-punctuation-in-it/ 292 // 293 // We can't simply check for a filepath separator because container names may 294 // have a separator, e.g., "host0/cname1" if container is in a Docker cluster, 295 // so we have to check for a `/` or `.` prefix. Also, in the case of a Windows 296 // client, a `:` could be part of an absolute Windows path, in which case it 297 // is immediately proceeded by a backslash. 298 func splitCpArg(arg string) (container, path string) { 299 if system.IsAbs(arg) { 300 // Explicit local absolute path, e.g., `C:\foo` or `/foo`. 301 return "", arg 302 } 303 304 parts := strings.SplitN(arg, ":", 2) 305 306 if len(parts) == 1 || strings.HasPrefix(parts[0], ".") { 307 // Either there's no `:` in the arg 308 // OR it's an explicit local relative path like `./file:name.txt`. 309 return "", arg 310 } 311 312 return parts[0], parts[1] 313 }