github.com/saucelabs/saucectl@v0.175.1/internal/archive/zip/zip.go (about)

     1  package zip
     2  
     3  import (
     4  	"archive/zip"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/rs/zerolog/log"
    13  	"github.com/saucelabs/saucectl/internal/sauceignore"
    14  )
    15  
    16  // Writer is a wrapper around zip.Writer and implements zip archiving for archive.Writer.
    17  type Writer struct {
    18  	W       *zip.Writer
    19  	M       sauceignore.Matcher
    20  	ZipFile *os.File
    21  }
    22  
    23  // NewFileWriter returns a new Writer that archives files to name.
    24  func NewFileWriter(name string, matcher sauceignore.Matcher) (Writer, error) {
    25  	f, err := os.Create(name)
    26  	if err != nil {
    27  		return Writer{}, err
    28  	}
    29  
    30  	w := Writer{W: zip.NewWriter(f), M: matcher, ZipFile: f}
    31  
    32  	return w, nil
    33  }
    34  
    35  // Add adds the file at src to the destination dst in the archive and returns a count of
    36  // the files added to the archive, as well the length of the longest path.
    37  // The added file names should not contain any backslashes according to the specification outlined in
    38  // https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT.
    39  // It's essential to adhere to this specification to ensure compatibility and
    40  // proper functioning of the files across different systems and platforms.
    41  func (w *Writer) Add(src, dst string) (count int, length int, err error) {
    42  	finfo, err := os.Stat(src)
    43  	if err != nil {
    44  		return 0, 0, err
    45  	}
    46  
    47  	// Only will be applied if we have .sauceignore file and have patterns to exclude files and folders
    48  	if w.M.Match(strings.Split(src, string(os.PathSeparator)), finfo.IsDir()) {
    49  		return 0, 0, nil
    50  	}
    51  
    52  	log.Debug().Str("name", src).Msg("Adding to archive")
    53  	target := path.Join(dst, finfo.Name())
    54  	if finfo.IsDir() {
    55  		// The trailing slash denotes a directory entry.
    56  		target = fmt.Sprintf("%s/", target)
    57  	}
    58  
    59  	finfoHeader, err := zip.FileInfoHeader(finfo)
    60  	if err != nil {
    61  		return 0, 0, err
    62  	}
    63  
    64  	finfoHeader.Name = filepath.ToSlash(target)
    65  	finfoHeader.Method = zip.Deflate
    66  	fileWriter, err := w.W.CreateHeader(finfoHeader)
    67  	if err != nil {
    68  		return 0, 0, err
    69  	}
    70  	count++
    71  
    72  	if !finfo.IsDir() {
    73  		f, err := os.Open(src)
    74  		if err != nil {
    75  			return 0, 0, err
    76  		}
    77  
    78  		if _, err := io.Copy(fileWriter, f); err != nil {
    79  			return 0, 0, err
    80  		}
    81  
    82  		if err := f.Close(); err != nil {
    83  			return 0, 0, err
    84  		}
    85  
    86  		return count, len(target), err
    87  	}
    88  
    89  	files, err := os.ReadDir(src)
    90  	if err != nil {
    91  		return 0, 0, err
    92  	}
    93  
    94  	for _, f := range files {
    95  		base := filepath.Base(src)
    96  		rebase := path.Join(dst, base)
    97  		fpath := filepath.Join(src, f.Name())
    98  		fileCount, pathLength, err := w.Add(fpath, rebase)
    99  		if err != nil {
   100  			return 0, 0, err
   101  		}
   102  
   103  		count += fileCount
   104  		if pathLength > length {
   105  			length = pathLength
   106  		}
   107  	}
   108  
   109  	return count, length, nil
   110  }
   111  
   112  // Close closes the archive. Adding more files to the archive is not possible after this.
   113  func (w *Writer) Close() error {
   114  	if err := w.W.Close(); err != nil {
   115  		return err
   116  	}
   117  	return w.ZipFile.Close()
   118  }
   119  
   120  func Extract(targetDir string, file *zip.File) error {
   121  	fullPath := path.Join(targetDir, file.Name)
   122  
   123  	relPath, err := filepath.Rel(targetDir, fullPath)
   124  	if err != nil {
   125  		return err
   126  	}
   127  	if strings.Contains(relPath, "..") {
   128  		return fmt.Errorf("file %s is relative to an outside folder", file.Name)
   129  	}
   130  
   131  	folder := path.Dir(fullPath)
   132  	if err := os.MkdirAll(folder, 0755); err != nil {
   133  		return err
   134  	}
   135  
   136  	fd, err := os.Create(fullPath)
   137  	if err != nil {
   138  		return err
   139  	}
   140  	defer fd.Close()
   141  
   142  	rd, err := file.Open()
   143  	if err != nil {
   144  		return err
   145  	}
   146  	defer rd.Close()
   147  
   148  	_, err = io.Copy(fd, rd)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	return fd.Close()
   153  }