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 }