package dockerhelper
import (
"archive/tar"
"fmt"
"io"
"io/ioutil"
"path/filepath"
"strings"
docker "github.com/fsouza/go-dockerclient"
s2itar "github.com/openshift/source-to-image/pkg/tar"
s2iutil "github.com/openshift/source-to-image/pkg/util"
)
// removeLeadingDirectoryAdapter wraps a tar.Reader and strips the first leading
// directory name of all of the files in the archive. An error is returned in
// the case that files with differing first leading directory names are
// encountered.
type removeLeadingDirectoryAdapter struct {
s2itar.Reader
leadingDir string
setLeadingDir bool
}
func (adapter removeLeadingDirectoryAdapter) Next() (*tar.Header, error) {
for {
header, err := adapter.Reader.Next()
if err != nil {
return nil, err
}
trimmedName := strings.Trim(header.Name, "/")
paths := strings.SplitN(trimmedName, "/", 2)
// ensure leading directory is consistent throughout the tar file
if !adapter.setLeadingDir {
adapter.setLeadingDir = true
adapter.leadingDir = paths[0]
}
if adapter.leadingDir != paths[0] {
return nil, fmt.Errorf("inconsistent leading directory at %s, type %c", header.Name, header.Typeflag)
}
if len(paths) == 1 {
if header.Typeflag == tar.TypeDir {
// this is the leading directory itself: drop it
_, err = io.Copy(ioutil.Discard, adapter)
if err != nil {
return nil, err
}
continue
}
return nil, fmt.Errorf("unexpected non-directory %s, type %c", header.Name, header.Typeflag)
}
header.Name = paths[1]
return header, err
}
}
func newContainerDownloader(client *docker.Client, container, path string) io.ReadCloser {
r, w := io.Pipe()
go func() {
opts := docker.DownloadFromContainerOptions{
Path: path,
OutputStream: w,
}
w.CloseWithError(client.DownloadFromContainer(container, opts))
}()
return r
}
func newContainerUploader(client *docker.Client, container, path string) (io.WriteCloser, <-chan error) {
r, w := io.Pipe()
errch := make(chan error, 1)
go func() {
opts := docker.UploadToContainerOptions{
Path: path,
InputStream: r,
}
errch <- client.UploadToContainer(container, opts)
}()
return w, errch
}
type readCloser struct {
io.Reader
io.Closer
}
// StreamFileFromContainer returns an io.ReadCloser from which the contents of a
// file in a remote container can be read.
func StreamFileFromContainer(client *docker.Client, container, src string) (io.ReadCloser, error) {
downloader := newContainerDownloader(client, container, src)
tarReader := tar.NewReader(downloader)
header, err := tarReader.Next()
if err != nil {
return nil, err
}
if header.Name != filepath.Base(src) || (header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeRegA) {
return nil, fmt.Errorf("unexpected tar file content %s, type %c", header.Name, header.Typeflag)
}
return readCloser{Reader: tarReader, Closer: downloader}, nil
}
// DownloadDirFromContainer downloads an entire directory of files from a remote
// container.
func DownloadDirFromContainer(client *docker.Client, container, src, dst string) error {
downloader := newContainerDownloader(client, container, src)
defer downloader.Close()
tarReader := &removeLeadingDirectoryAdapter{Reader: tar.NewReader(downloader)}
t := s2itar.New(s2iutil.NewFileSystem())
return t.ExtractTarStreamFromTarReader(dst, tarReader, nil)
}
// UploadFileToContainer uploads a file to a remote container.
func UploadFileToContainer(client *docker.Client, container, src, dest string) error {
uploader, errch := newContainerUploader(client, container, filepath.Dir(dest))
t := s2itar.New(s2iutil.NewFileSystem())
tarWriter := s2itar.RenameAdapter{Writer: tar.NewWriter(uploader), Old: filepath.Base(src), New: filepath.Base(dest)}
err := t.CreateTarStreamToTarWriter(src, true, tarWriter, nil)
if err == nil {
err = tarWriter.Close()
}
uploader.Close()
if err != nil {
return err
}
return <-errch
}