github.com/pluralsh/plural-cli@v0.9.5/pkg/utils/tar.go (about)

     1  package utils
     2  
     3  import (
     4  	"archive/tar"
     5  	"compress/gzip"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"log"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/pluralsh/plural-cli/pkg/utils/pathing"
    17  )
    18  
    19  func Tar(src string, w io.Writer, regex string) error {
    20  	// ensure the src actually exists before trying to tar it
    21  	dir := filepath.Dir(src)
    22  	if _, err := os.Stat(src); err != nil {
    23  		return fmt.Errorf("Unable to tar files: %w", err)
    24  	}
    25  
    26  	gzw := gzip.NewWriter(w)
    27  	defer func(gzw *gzip.Writer) {
    28  		_ = gzw.Close()
    29  	}(gzw)
    30  
    31  	tw := tar.NewWriter(gzw)
    32  	defer func(tw *tar.Writer) {
    33  		_ = tw.Close()
    34  	}(tw)
    35  
    36  	// walk path
    37  	return filepath.Walk(src, func(file string, fi os.FileInfo, err error) error {
    38  		if regex != "" {
    39  			matched, err := regexp.MatchString(regex, file)
    40  			if matched || err != nil {
    41  				return err
    42  			}
    43  		}
    44  
    45  		if err != nil {
    46  			return err
    47  		}
    48  
    49  		if !fi.Mode().IsRegular() {
    50  			return nil
    51  		}
    52  
    53  		// create a new dir/file header
    54  		header, err := tar.FileInfoHeader(fi, fi.Name())
    55  		if err != nil {
    56  			return err
    57  		}
    58  		header.Name = strings.TrimPrefix(strings.ReplaceAll(file, dir, ""), string(filepath.Separator))
    59  
    60  		if err := tw.WriteHeader(header); err != nil {
    61  			return err
    62  		}
    63  
    64  		f, err := os.Open(file)
    65  		if err != nil {
    66  			return err
    67  		}
    68  
    69  		if _, err := io.Copy(tw, f); err != nil {
    70  			return err
    71  		}
    72  
    73  		if err := f.Close(); err != nil {
    74  			return err
    75  		}
    76  
    77  		return nil
    78  	})
    79  }
    80  
    81  func Untar(r io.Reader, dir, relpath string) error {
    82  	return untar(r, dir, relpath)
    83  }
    84  
    85  func untar(r io.Reader, dir, relpath string) (err error) {
    86  	t0 := time.Now()
    87  	nFiles := 0
    88  	madeDir := map[string]bool{}
    89  	zr, err := gzip.NewReader(r)
    90  	if err != nil {
    91  		return fmt.Errorf("requires gzip-compressed body: %w", err)
    92  	}
    93  	tr := tar.NewReader(zr)
    94  	loggedChtimesError := false
    95  	for {
    96  		f, err := tr.Next()
    97  		if errors.Is(err, io.EOF) {
    98  			break
    99  		}
   100  		if err != nil {
   101  			return fmt.Errorf("tar error: %w", err)
   102  		}
   103  		if !validRelPath(f.Name) {
   104  			return fmt.Errorf("tar contained invalid name error %q", f.Name)
   105  		}
   106  		rel, err := filepath.Rel(relpath, filepath.FromSlash(f.Name))
   107  		if err != nil {
   108  			return err
   109  		}
   110  		abs := pathing.SanitizeFilepath(filepath.Join(dir, rel))
   111  
   112  		fi := f.FileInfo()
   113  		mode := fi.Mode()
   114  		switch {
   115  		case mode.IsRegular():
   116  			// Make the directory. This is redundant because it should
   117  			// already be made by a directory entry in the tar
   118  			// beforehand. Thus, don't check for errors; the next
   119  			// write will fail with the same error.
   120  			dir := filepath.Dir(abs)
   121  			if !madeDir[dir] {
   122  				if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
   123  					return err
   124  				}
   125  				madeDir[dir] = true
   126  			}
   127  			wf, err := os.OpenFile(abs, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode.Perm())
   128  			if err != nil {
   129  				return err
   130  			}
   131  			n, err := io.Copy(wf, tr)
   132  			if closeErr := wf.Close(); closeErr != nil && err == nil {
   133  				err = closeErr
   134  			}
   135  			if err != nil {
   136  				return fmt.Errorf("error writing to %s: %w", abs, err)
   137  			}
   138  			if n != f.Size {
   139  				return fmt.Errorf("only wrote %d bytes to %s; expected %d", n, abs, f.Size)
   140  			}
   141  			modTime := f.ModTime
   142  			if modTime.After(t0) {
   143  				// Clamp modtimes at system time. See
   144  				// golang.org/issue/19062 when clock on
   145  				// buildlet was behind the gitmirror server
   146  				// doing the git-archive.
   147  				modTime = t0
   148  			}
   149  			if !modTime.IsZero() {
   150  				if err := os.Chtimes(abs, modTime, modTime); err != nil && !loggedChtimesError {
   151  					// benign error. Gerrit doesn't even set the
   152  					// modtime in these, and we don't end up relying
   153  					// on it anywhere (the gomote push command relies
   154  					// on digests only), so this is a little pointless
   155  					// for now.
   156  					log.Printf("error changing modtime: %v (further Chtimes errors suppressed)", err)
   157  					loggedChtimesError = true // once is enough
   158  				}
   159  			}
   160  			nFiles++
   161  		case mode.IsDir():
   162  			if err := os.MkdirAll(abs, 0755); err != nil {
   163  				return err
   164  			}
   165  			madeDir[abs] = true
   166  		default:
   167  			return fmt.Errorf("tar file entry %s contained unsupported file type %v", f.Name, mode)
   168  		}
   169  	}
   170  	return nil
   171  }
   172  
   173  func validRelPath(p string) bool {
   174  	if p == "" || strings.Contains(p, `\`) || strings.HasPrefix(p, "/") || strings.Contains(p, "../") {
   175  		return false
   176  	}
   177  	return true
   178  }