kubesphere.io/s2irun@v3.2.1+incompatible/pkg/tar/tar.go (about)

     1  package tar
     2  
     3  import (
     4  	"archive/tar"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/kubesphere/s2irun/pkg/utils/fs"
    15  	utilglog "github.com/kubesphere/s2irun/pkg/utils/glog"
    16  
    17  	s2ierr "github.com/kubesphere/s2irun/pkg/errors"
    18  	"github.com/kubesphere/s2irun/pkg/utils"
    19  )
    20  
    21  var glog = utilglog.StderrLog
    22  
    23  // defaultTimeout is the amount of time that the untar will wait for a tar
    24  // stream to extract a single file. A timeout is needed to guard against broken
    25  // connections in which it would wait for a long time to untar and nothing would happen
    26  const defaultTimeout = 30 * time.Second
    27  
    28  // DefaultExclusionPattern is the pattern of files that will not be included in a tar
    29  // file when creating one. By default it is any file inside a .git metadata directory
    30  var DefaultExclusionPattern = regexp.MustCompile(`(^|/)\.git(/|$)`)
    31  
    32  // Tar can create and extract tar files used in an STI build
    33  type Tar interface {
    34  	// SetExclusionPattern sets the exclusion pattern for tar
    35  	// creation
    36  	SetExclusionPattern(*regexp.Regexp)
    37  
    38  	// CreateTarFile creates a tar file in the base directory
    39  	// using the contents of dir directory
    40  	// The name of the new tar file is returned if successful
    41  	CreateTarFile(base, dir string) (string, error)
    42  
    43  	// CreateTarStreamToTarWriter creates a tar from the given directory
    44  	// and streams it to the given writer.
    45  	// An error is returned if an error occurs during streaming.
    46  	// Archived file names are written to the logger if provided
    47  	CreateTarStreamToTarWriter(dir string, includeDirInPath bool, writer Writer, logger io.Writer) error
    48  
    49  	// CreateTarStream creates a tar from the given directory
    50  	// and streams it to the given writer.
    51  	// An error is returned if an error occurs during streaming.
    52  	CreateTarStream(dir string, includeDirInPath bool, writer io.Writer) error
    53  
    54  	// CreateTarStreamReader returns an io.ReadCloser from which a tar stream can be
    55  	// read.  The tar stream is created using CreateTarStream.
    56  	CreateTarStreamReader(dir string, includeDirInPath bool) io.ReadCloser
    57  
    58  	// ExtractTarStream extracts files from a given tar stream.
    59  	// Times out if reading from the stream for any given file
    60  	// exceeds the value of timeout.
    61  	ExtractTarStream(dir string, reader io.Reader) error
    62  
    63  	// ExtractTarStreamWithLogging extracts files from a given tar stream.
    64  	// Times out if reading from the stream for any given file
    65  	// exceeds the value of timeout.
    66  	// Extracted file names are written to the logger if provided.
    67  	ExtractTarStreamWithLogging(dir string, reader io.Reader, logger io.Writer) error
    68  
    69  	// ExtractTarStreamFromTarReader extracts files from a given tar stream.
    70  	// Times out if reading from the stream for any given file
    71  	// exceeds the value of timeout.
    72  	// Extracted file names are written to the logger if provided.
    73  	ExtractTarStreamFromTarReader(dir string, tarReader Reader, logger io.Writer) error
    74  }
    75  
    76  // Reader is an interface which tar.Reader implements.
    77  type Reader interface {
    78  	io.Reader
    79  	Next() (*tar.Header, error)
    80  }
    81  
    82  // Writer is an interface which tar.Writer implements.
    83  type Writer interface {
    84  	io.WriteCloser
    85  	Flush() error
    86  	WriteHeader(hdr *tar.Header) error
    87  }
    88  
    89  // ChmodAdapter changes the mode of files and directories inline as a tarfile is
    90  // being written
    91  type ChmodAdapter struct {
    92  	Writer
    93  	NewFileMode     int64
    94  	NewExecFileMode int64
    95  	NewDirMode      int64
    96  }
    97  
    98  // WriteHeader changes the mode of files and directories inline as a tarfile is
    99  // being written
   100  func (a ChmodAdapter) WriteHeader(hdr *tar.Header) error {
   101  	if hdr.FileInfo().Mode()&os.ModeSymlink == 0 {
   102  		newMode := hdr.Mode &^ 0777
   103  		if hdr.FileInfo().IsDir() {
   104  			newMode |= a.NewDirMode
   105  		} else if hdr.FileInfo().Mode()&0010 != 0 { // S_IXUSR
   106  			newMode |= a.NewExecFileMode
   107  		} else {
   108  			newMode |= a.NewFileMode
   109  		}
   110  		hdr.Mode = newMode
   111  	}
   112  	return a.Writer.WriteHeader(hdr)
   113  }
   114  
   115  // RenameAdapter renames files and directories inline as a tarfile is being
   116  // written
   117  type RenameAdapter struct {
   118  	Writer
   119  	Old string
   120  	New string
   121  }
   122  
   123  // WriteHeader renames files and directories inline as a tarfile is being
   124  // written
   125  func (a RenameAdapter) WriteHeader(hdr *tar.Header) error {
   126  	if hdr.Name == a.Old {
   127  		hdr.Name = a.New
   128  	} else if strings.HasPrefix(hdr.Name, a.Old+"/") {
   129  		hdr.Name = a.New + hdr.Name[len(a.Old):]
   130  	}
   131  
   132  	return a.Writer.WriteHeader(hdr)
   133  }
   134  
   135  // New creates a new Tar
   136  func New(fs fs.FileSystem) Tar {
   137  	return &stiTar{
   138  		FileSystem: fs,
   139  		exclude:    DefaultExclusionPattern,
   140  		timeout:    defaultTimeout,
   141  	}
   142  }
   143  
   144  // NewParanoid creates a new Tar that has restrictions
   145  // on what it can do while extracting files.
   146  func NewParanoid(fs fs.FileSystem) Tar {
   147  	return &stiTar{
   148  		FileSystem:           fs,
   149  		exclude:              DefaultExclusionPattern,
   150  		timeout:              defaultTimeout,
   151  		disallowOverwrite:    true,
   152  		disallowOutsidePaths: true,
   153  		disallowSpecialFiles: true,
   154  	}
   155  }
   156  
   157  // stiTar is an implementation of the Tar interface
   158  type stiTar struct {
   159  	fs.FileSystem
   160  	timeout              time.Duration
   161  	exclude              *regexp.Regexp
   162  	includeDirInPath     bool
   163  	disallowOverwrite    bool
   164  	disallowOutsidePaths bool
   165  	disallowSpecialFiles bool
   166  }
   167  
   168  // SetExclusionPattern sets the exclusion pattern for tar creation.  The
   169  // exclusion pattern always uses UNIX-style (/) path separators, even on
   170  // Windows.
   171  func (t *stiTar) SetExclusionPattern(p *regexp.Regexp) {
   172  	t.exclude = p
   173  }
   174  
   175  // CreateTarFile creates a tar file from the given directory
   176  // while excluding files that match the given exclusion pattern
   177  // It returns the name of the created file
   178  func (t *stiTar) CreateTarFile(base, dir string) (string, error) {
   179  	tarFile, err := ioutil.TempFile(base, "tar")
   180  	defer tarFile.Close()
   181  	if err != nil {
   182  		return "", err
   183  	}
   184  	if err = t.CreateTarStream(dir, false, tarFile); err != nil {
   185  		return "", err
   186  	}
   187  	return tarFile.Name(), nil
   188  }
   189  
   190  func (t *stiTar) shouldExclude(path string) bool {
   191  	return t.exclude != nil && t.exclude.String() != "" && t.exclude.MatchString(filepath.ToSlash(path))
   192  }
   193  
   194  // CreateTarStream calls CreateTarStreamToTarWriter with a nil logger
   195  func (t *stiTar) CreateTarStream(dir string, includeDirInPath bool, writer io.Writer) error {
   196  	tarWriter := tar.NewWriter(writer)
   197  	defer tarWriter.Close()
   198  
   199  	return t.CreateTarStreamToTarWriter(dir, includeDirInPath, tarWriter, nil)
   200  }
   201  
   202  // CreateTarStreamReader returns an io.ReadCloser from which a tar stream can be
   203  // read.  The tar stream is created using CreateTarStream.
   204  func (t *stiTar) CreateTarStreamReader(dir string, includeDirInPath bool) io.ReadCloser {
   205  	r, w := io.Pipe()
   206  	go func() {
   207  		w.CloseWithError(t.CreateTarStream(dir, includeDirInPath, w))
   208  	}()
   209  	return r
   210  }
   211  
   212  // CreateTarStreamToTarWriter creates a tar stream on the given writer from
   213  // the given directory while excluding files that match the given
   214  // exclusion pattern.
   215  func (t *stiTar) CreateTarStreamToTarWriter(dir string, includeDirInPath bool, tarWriter Writer, logger io.Writer) error {
   216  	dir = filepath.Clean(dir) // remove relative paths and extraneous slashes
   217  	glog.V(5).Infof("Adding %q to tar ...", dir)
   218  	err := t.Walk(dir, func(path string, info os.FileInfo, err error) error {
   219  		if err != nil {
   220  			return err
   221  		}
   222  		// on Windows, directory symlinks report as a directory and as a symlink.
   223  		// They should be treated as symlinks.
   224  		if !t.shouldExclude(path) {
   225  			// if file is a link just writing header info is enough
   226  			if info.Mode()&os.ModeSymlink != 0 {
   227  				if dir == path {
   228  					return nil
   229  				}
   230  				if err = t.writeTarHeader(tarWriter, dir, path, info, includeDirInPath, logger); err != nil {
   231  					glog.Errorf("Error writing header for %q: %v", info.Name(), err)
   232  				}
   233  				// on Windows, filepath.Walk recurses into directory symlinks when it
   234  				// shouldn't.  https://github.com/golang/go/issues/17540
   235  				if err == nil && info.Mode()&os.ModeDir != 0 {
   236  					return filepath.SkipDir
   237  				}
   238  				return err
   239  			}
   240  			if info.IsDir() {
   241  				if dir == path {
   242  					return nil
   243  				}
   244  				if err = t.writeTarHeader(tarWriter, dir, path, info, includeDirInPath, logger); err != nil {
   245  					glog.Errorf("Error writing header for %q: %v", info.Name(), err)
   246  				}
   247  				return err
   248  			}
   249  
   250  			// regular files are copied into tar, if accessible
   251  			file, err := os.Open(path)
   252  			if err != nil {
   253  				glog.Errorf("Ignoring file %s: %v", path, err)
   254  				return nil
   255  			}
   256  			defer file.Close()
   257  			if err = t.writeTarHeader(tarWriter, dir, path, info, includeDirInPath, logger); err != nil {
   258  				glog.Errorf("Error writing header for %q: %v", info.Name(), err)
   259  				return err
   260  			}
   261  			if _, err = io.Copy(tarWriter, file); err != nil {
   262  				glog.Errorf("Error copying file %q to tar: %v", path, err)
   263  				return err
   264  			}
   265  		}
   266  		return nil
   267  	})
   268  
   269  	if err != nil {
   270  		glog.Errorf("Error writing tar: %v", err)
   271  		return err
   272  	}
   273  
   274  	return nil
   275  }
   276  
   277  // writeTarHeader writes tar header for given file, returns error if operation fails
   278  func (t *stiTar) writeTarHeader(tarWriter Writer, dir string, path string, info os.FileInfo, includeDirInPath bool, logger io.Writer) error {
   279  	var (
   280  		link string
   281  		err  error
   282  	)
   283  	if info.Mode()&os.ModeSymlink != 0 {
   284  		link, err = os.Readlink(path)
   285  		if err != nil {
   286  			return err
   287  		}
   288  	}
   289  	header, err := tar.FileInfoHeader(info, link)
   290  	if err != nil {
   291  		return err
   292  	}
   293  	// on Windows, tar.FileInfoHeader incorrectly interprets directory symlinks
   294  	// as directories.  https://github.com/golang/go/issues/17541
   295  	if info.Mode()&os.ModeSymlink != 0 && info.Mode()&os.ModeDir != 0 {
   296  		header.Typeflag = tar.TypeSymlink
   297  		header.Mode &^= 040000 // c_ISDIR
   298  		header.Mode |= 0120000 // c_ISLNK
   299  		header.Linkname = link
   300  	}
   301  	prefix := dir
   302  	if includeDirInPath {
   303  		prefix = filepath.Dir(prefix)
   304  	}
   305  	fileName := path
   306  	if prefix != "." {
   307  		fileName = path[1+len(prefix):]
   308  	}
   309  	header.Name = filepath.ToSlash(fileName)
   310  	header.Linkname = filepath.ToSlash(header.Linkname)
   311  	logFile(logger, header.Name)
   312  	glog.V(5).Infof("Adding to tar: %s as %s", path, header.Name)
   313  	return tarWriter.WriteHeader(header)
   314  }
   315  
   316  // ExtractTarStream calls ExtractTarStreamFromTarReader with a default reader and nil logger
   317  func (t *stiTar) ExtractTarStream(dir string, reader io.Reader) error {
   318  	tarReader := tar.NewReader(reader)
   319  	return t.ExtractTarStreamFromTarReader(dir, tarReader, nil)
   320  }
   321  
   322  // ExtractTarStreamWithLogging calls ExtractTarStreamFromTarReader with a default reader
   323  func (t *stiTar) ExtractTarStreamWithLogging(dir string, reader io.Reader, logger io.Writer) error {
   324  	tarReader := tar.NewReader(reader)
   325  	return t.ExtractTarStreamFromTarReader(dir, tarReader, logger)
   326  }
   327  
   328  // ExtractTarStreamFromTarReader extracts files from a given tar stream.
   329  // Times out if reading from the stream for any given file
   330  // exceeds the value of timeout
   331  func (t *stiTar) ExtractTarStreamFromTarReader(dir string, tarReader Reader, logger io.Writer) error {
   332  	err := utils.TimeoutAfter(t.timeout, "", func(timeoutTimer *time.Timer) error {
   333  		for {
   334  			header, err := tarReader.Next()
   335  			if !timeoutTimer.Stop() {
   336  				return &utils.TimeoutError{}
   337  			}
   338  			timeoutTimer.Reset(t.timeout)
   339  			if err == io.EOF {
   340  				return nil
   341  			}
   342  			if err != nil {
   343  				glog.Errorf("Error reading next tar header: %v", err)
   344  				return err
   345  			}
   346  
   347  			if t.disallowSpecialFiles {
   348  				switch header.Typeflag {
   349  				case tar.TypeReg, tar.TypeRegA, tar.TypeLink, tar.TypeSymlink, tar.TypeDir, tar.TypeGNUSparse:
   350  				default:
   351  					glog.Warningf("Skipping special file %s, type: %v", header.Name, header.Typeflag)
   352  					continue
   353  				}
   354  			}
   355  
   356  			p := header.Name
   357  			if t.disallowOutsidePaths {
   358  				p = filepath.Clean(filepath.Join(dir, p))
   359  				if !strings.HasPrefix(p, dir) {
   360  					glog.Warningf("Skipping relative path file in tar: %s", header.Name)
   361  					continue
   362  				}
   363  			}
   364  
   365  			if header.FileInfo().IsDir() {
   366  				dirPath := filepath.Join(dir, filepath.Clean(header.Name))
   367  				glog.V(3).Infof("Creating directory %s", dirPath)
   368  				if err = os.MkdirAll(dirPath, 0700); err != nil {
   369  					glog.Errorf("Error creating dir %q: %v", dirPath, err)
   370  					return err
   371  				}
   372  				t.Chmod(dirPath, header.FileInfo().Mode())
   373  			} else {
   374  				fileDir := filepath.Dir(header.Name)
   375  				dirPath := filepath.Join(dir, filepath.Clean(fileDir))
   376  				glog.V(3).Infof("Creating directory %s", dirPath)
   377  				if err = os.MkdirAll(dirPath, 0700); err != nil {
   378  					glog.Errorf("Error creating dir %q: %v", dirPath, err)
   379  					return err
   380  				}
   381  				if header.Typeflag == tar.TypeSymlink {
   382  					if err := t.extractLink(dir, header, tarReader); err != nil {
   383  						glog.Errorf("Error extracting link %q: %v", header.Name, err)
   384  						return err
   385  					}
   386  					continue
   387  				}
   388  				logFile(logger, header.Name)
   389  				if err := t.extractFile(dir, header, tarReader); err != nil {
   390  					glog.Errorf("Error extracting file %q: %v", header.Name, err)
   391  					return err
   392  				}
   393  			}
   394  		}
   395  	})
   396  
   397  	if err != nil {
   398  		glog.Error("Error extracting tar stream")
   399  	} else {
   400  		glog.V(2).Info("Done extracting tar stream")
   401  	}
   402  
   403  	if utils.IsTimeoutError(err) {
   404  		err = s2ierr.NewTarTimeoutError()
   405  	}
   406  
   407  	return err
   408  }
   409  
   410  func (t *stiTar) extractLink(dir string, header *tar.Header, tarReader io.Reader) error {
   411  	dest := filepath.Join(dir, header.Name)
   412  	source := header.Linkname
   413  
   414  	if t.disallowOutsidePaths {
   415  		target := filepath.Clean(filepath.Join(dest, "..", source))
   416  		if !strings.HasPrefix(target, dir) {
   417  			glog.Warningf("Skipping symlink that points to relative path: %s", header.Linkname)
   418  			return nil
   419  		}
   420  	}
   421  
   422  	if t.disallowOverwrite {
   423  		if _, err := os.Stat(dest); !os.IsNotExist(err) {
   424  			glog.Warningf("Refusing to overwrite existing file: %s", dest)
   425  			return nil
   426  		}
   427  	}
   428  
   429  	glog.V(3).Infof("Creating symbolic link from %q to %q", dest, source)
   430  
   431  	// TODO: set mtime for symlink (unfortunately we can't use os.Chtimes() and probably should use syscall)
   432  	return os.Symlink(source, dest)
   433  }
   434  
   435  func (t *stiTar) extractFile(dir string, header *tar.Header, tarReader io.Reader) error {
   436  	path := filepath.Join(dir, header.Name)
   437  	if t.disallowOverwrite {
   438  		if _, err := os.Stat(path); !os.IsNotExist(err) {
   439  			glog.Warningf("Refusing to overwrite existing file: %s", path)
   440  			return nil
   441  		}
   442  	}
   443  
   444  	glog.V(3).Infof("Creating %s", path)
   445  
   446  	file, err := os.Create(path)
   447  	if err != nil {
   448  		return err
   449  	}
   450  	// The file times need to be modified after it's been closed thus this function
   451  	// is deferred after the file close (LIFO order for defer)
   452  	defer os.Chtimes(path, time.Now(), header.FileInfo().ModTime())
   453  	defer file.Close()
   454  	glog.V(3).Infof("Extracting/writing %s", path)
   455  	written, err := io.Copy(file, tarReader)
   456  	if err != nil {
   457  		return err
   458  	}
   459  	if written != header.Size {
   460  		return fmt.Errorf("wrote %d bytes, expected to write %d", written, header.Size)
   461  	}
   462  	return t.Chmod(path, header.FileInfo().Mode())
   463  }
   464  
   465  func logFile(logger io.Writer, name string) {
   466  	if logger == nil {
   467  		return
   468  	}
   469  	fmt.Fprintf(logger, "%s\n", name)
   470  }