github.com/argoproj/argo-cd/v3@v3.2.1/util/io/files/tar.go (about)

     1  package files
     2  
     3  import (
     4  	"archive/tar"
     5  	"bufio"
     6  	"compress/gzip"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  
    12  	log "github.com/sirupsen/logrus"
    13  )
    14  
    15  type tgz struct {
    16  	srcPath      string
    17  	inclusions   []string
    18  	exclusions   []string
    19  	tarWriter    *tar.Writer
    20  	filesWritten int
    21  }
    22  
    23  // Tgz will iterate over all files found in srcPath compressing them with gzip
    24  // and archiving with Tar. Will invoke every given writer while generating the tgz.
    25  // This is useful to generate checksums. Will exclude files matching the exclusions
    26  // list blob if exclusions is not nil. Will include only the files matching the
    27  // inclusions list if inclusions is not nil.
    28  func Tgz(srcPath string, inclusions []string, exclusions []string, writers ...io.Writer) (int, error) {
    29  	if _, err := os.Stat(srcPath); err != nil {
    30  		return 0, fmt.Errorf("error inspecting srcPath %q: %w", srcPath, err)
    31  	}
    32  
    33  	gzw := gzip.NewWriter(io.MultiWriter(writers...))
    34  	defer gzw.Close()
    35  
    36  	return writeFile(srcPath, inclusions, exclusions, gzw)
    37  }
    38  
    39  // Tar will iterate over all files found in srcPath archiving with Tar. Will invoke every given writer while generating the tar.
    40  // This is useful to generate checksums. Will exclude files matching the exclusions
    41  // list blob if exclusions is not nil. Will include only the files matching the
    42  // inclusions list if inclusions is not nil.
    43  func Tar(srcPath string, inclusions []string, exclusions []string, writers ...io.Writer) (int, error) {
    44  	if _, err := os.Stat(srcPath); err != nil {
    45  		return 0, fmt.Errorf("error inspecting srcPath %q: %w", srcPath, err)
    46  	}
    47  
    48  	return writeFile(srcPath, inclusions, exclusions, io.MultiWriter(writers...))
    49  }
    50  
    51  func writeFile(srcPath string, inclusions []string, exclusions []string, writer io.Writer) (int, error) {
    52  	tw := tar.NewWriter(writer)
    53  	defer tw.Close()
    54  
    55  	t := &tgz{
    56  		srcPath:    srcPath,
    57  		inclusions: inclusions,
    58  		exclusions: exclusions,
    59  		tarWriter:  tw,
    60  	}
    61  	err := filepath.Walk(srcPath, t.tgzFile)
    62  	if err != nil {
    63  		return 0, err
    64  	}
    65  
    66  	return t.filesWritten, nil
    67  }
    68  
    69  // Untgz will loop over the tar reader creating the file structure at dstPath.
    70  // Callers must make sure dstPath is:
    71  //   - a full path
    72  //   - points to an empty directory or
    73  //   - points to a non-existing directory
    74  func Untgz(dstPath string, r io.Reader, maxSize int64, preserveFileMode bool) error {
    75  	if !filepath.IsAbs(dstPath) {
    76  		return fmt.Errorf("dstPath points to a relative path: %s", dstPath)
    77  	}
    78  
    79  	gzr, err := gzip.NewReader(r)
    80  	if err != nil {
    81  		return fmt.Errorf("error reading file: %w", err)
    82  	}
    83  	defer gzr.Close()
    84  	return untar(dstPath, io.LimitReader(gzr, maxSize), preserveFileMode)
    85  }
    86  
    87  // Untar will loop over the tar reader creating the file structure at dstPath.
    88  // Callers must make sure dstPath is:
    89  //   - a full path
    90  //   - points to an empty directory or
    91  //   - points to a non-existing directory
    92  func Untar(dstPath string, r io.Reader, maxSize int64, preserveFileMode bool) error {
    93  	if !filepath.IsAbs(dstPath) {
    94  		return fmt.Errorf("dstPath points to a relative path: %s", dstPath)
    95  	}
    96  
    97  	return untar(dstPath, io.LimitReader(r, maxSize), preserveFileMode)
    98  }
    99  
   100  // untar will loop over the tar reader creating the file structure at dstPath.
   101  // Callers must make sure dstPath is:
   102  //   - a full path
   103  //   - points to an empty directory or
   104  //   - points to a non existing directory
   105  func untar(dstPath string, r io.Reader, preserveFileMode bool) error {
   106  	tr := tar.NewReader(r)
   107  
   108  	for {
   109  		header, err := tr.Next()
   110  		if err != nil {
   111  			if err == io.EOF {
   112  				break
   113  			}
   114  			return fmt.Errorf("error while iterating on tar reader: %w", err)
   115  		}
   116  		if header == nil || header.Name == "." || header.Name == "./" {
   117  			continue
   118  		}
   119  
   120  		target := filepath.Join(dstPath, header.Name)
   121  		// Sanity check to protect against zip-slip
   122  		if !Inbound(target, dstPath) {
   123  			return fmt.Errorf("illegal filepath in archive: %s", target)
   124  		}
   125  
   126  		switch header.Typeflag {
   127  		case tar.TypeDir:
   128  			var mode os.FileMode = 0o755
   129  			if preserveFileMode {
   130  				mode = os.FileMode(header.Mode)
   131  			}
   132  			err := os.MkdirAll(target, mode)
   133  			if err != nil {
   134  				return fmt.Errorf("error creating nested folders: %w", err)
   135  			}
   136  		case tar.TypeSymlink:
   137  			// Sanity check to protect against symlink exploit
   138  			linkTarget := filepath.Join(filepath.Dir(target), header.Linkname)
   139  			realLinkTarget, err := filepath.EvalSymlinks(linkTarget)
   140  			if os.IsNotExist(err) {
   141  				realLinkTarget = linkTarget
   142  			} else if err != nil {
   143  				return fmt.Errorf("error checking symlink realpath: %w", err)
   144  			}
   145  			if !Inbound(realLinkTarget, dstPath) {
   146  				return fmt.Errorf("illegal filepath in symlink: %s", linkTarget)
   147  			}
   148  
   149  			// Relativizing all symlink targets because path.CheckOutOfBoundsSymlinks disallows any absolute symlinks
   150  			// and it makes more sense semantically to view symlinks in archives as relative.
   151  			// Inbound ensures that we never allow symlinks that break out of the target directory.
   152  			realLinkTarget, err = filepath.Rel(filepath.Dir(target), realLinkTarget)
   153  			if err != nil {
   154  				return fmt.Errorf("error relativizing link target: %w", err)
   155  			}
   156  
   157  			err = os.Symlink(realLinkTarget, target)
   158  			if err != nil {
   159  				return fmt.Errorf("error creating symlink: %w", err)
   160  			}
   161  		case tar.TypeReg:
   162  			var mode os.FileMode = 0o644
   163  			if preserveFileMode {
   164  				mode = os.FileMode(header.Mode)
   165  			}
   166  
   167  			err := os.MkdirAll(filepath.Dir(target), 0o755)
   168  			if err != nil {
   169  				return fmt.Errorf("error creating nested folders: %w", err)
   170  			}
   171  
   172  			f, err := os.OpenFile(target, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
   173  			if err != nil {
   174  				return fmt.Errorf("error creating file %q: %w", target, err)
   175  			}
   176  			w := bufio.NewWriter(f)
   177  			if _, err := io.Copy(w, tr); err != nil {
   178  				f.Close()
   179  				return fmt.Errorf("error writing tgz file: %w", err)
   180  			}
   181  			f.Close()
   182  		}
   183  	}
   184  	return nil
   185  }
   186  
   187  // tgzFile is used as a filepath.WalkFunc implementing the logic to write
   188  // the given file in the tgz.tarWriter applying the exclusion pattern defined
   189  // in tgz.exclusions, or the inclusion pattern defined in tgz.inclusions.
   190  // Only regular files will be added in the tarball.
   191  func (t *tgz) tgzFile(path string, fi os.FileInfo, err error) error {
   192  	if err != nil {
   193  		return fmt.Errorf("error walking in %q: %w", t.srcPath, err)
   194  	}
   195  
   196  	base := filepath.Base(path)
   197  
   198  	relativePath, err := RelativePath(path, t.srcPath)
   199  	if err != nil {
   200  		return fmt.Errorf("relative path error: %w", err)
   201  	}
   202  
   203  	if t.inclusions != nil && base != "." && !fi.IsDir() {
   204  		included := false
   205  		for _, inclusionPattern := range t.inclusions {
   206  			found, err := filepath.Match(inclusionPattern, base)
   207  			if err != nil {
   208  				return fmt.Errorf("error verifying inclusion pattern %q: %w", inclusionPattern, err)
   209  			}
   210  			if found {
   211  				included = true
   212  				break
   213  			}
   214  		}
   215  		if !included {
   216  			return nil
   217  		}
   218  	}
   219  	if t.exclusions != nil {
   220  		for _, exclusionPattern := range t.exclusions {
   221  			found, err := filepath.Match(exclusionPattern, relativePath)
   222  			if err != nil {
   223  				return fmt.Errorf("error verifying exclusion pattern %q: %w", exclusionPattern, err)
   224  			}
   225  			if found {
   226  				if fi.IsDir() {
   227  					return filepath.SkipDir
   228  				}
   229  				return nil
   230  			}
   231  		}
   232  	}
   233  
   234  	if !supportedFileMode(fi) {
   235  		return nil
   236  	}
   237  
   238  	link := ""
   239  	if IsSymlink(fi) {
   240  		link, err = os.Readlink(path)
   241  		if err != nil {
   242  			return fmt.Errorf("error getting link target: %w", err)
   243  		}
   244  	}
   245  
   246  	header, err := tar.FileInfoHeader(fi, link)
   247  	if err != nil {
   248  		return fmt.Errorf("error creating a tar file header: %w", err)
   249  	}
   250  
   251  	// update the name to correctly reflect the desired destination when untaring
   252  	header.Name = relativePath
   253  
   254  	if err := t.tarWriter.WriteHeader(header); err != nil {
   255  		return fmt.Errorf("error writing header: %w", err)
   256  	}
   257  
   258  	// Only regular files needs to have their content copied.
   259  	// Directories and symlinks are header only.
   260  	if fi.Mode().IsRegular() {
   261  		f, err := os.Open(path)
   262  		if err != nil {
   263  			return fmt.Errorf("error opening file %q: %w", fi.Name(), err)
   264  		}
   265  		defer func() {
   266  			err := f.Close()
   267  			if err != nil {
   268  				log.Errorf("error closing file %q: %v", fi.Name(), err)
   269  			}
   270  		}()
   271  
   272  		if _, err := io.Copy(t.tarWriter, f); err != nil {
   273  			return fmt.Errorf("error copying tgz file to writers: %w", err)
   274  		}
   275  		t.filesWritten++
   276  	}
   277  
   278  	return nil
   279  }
   280  
   281  // supportedFileMode will return true if the file mode is supported.
   282  // Supported files means that it will be added to the tarball.
   283  func supportedFileMode(fi os.FileInfo) bool {
   284  	mode := fi.Mode()
   285  	if mode.IsRegular() || mode.IsDir() || IsSymlink(fi) {
   286  		return true
   287  	}
   288  	return false
   289  }