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 }