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 }