mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
290 lines
8.3 KiB
Go
290 lines
8.3 KiB
Go
package files
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bufio"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type tgz struct {
|
|
srcPath string
|
|
inclusions []string
|
|
exclusions []string
|
|
tarWriter *tar.Writer
|
|
filesWritten int
|
|
}
|
|
|
|
// Tgz will iterate over all files found in srcPath compressing them with gzip
|
|
// and archiving with Tar. Will invoke every given writer while generating the tgz.
|
|
// This is useful to generate checksums. Will exclude files matching the exclusions
|
|
// list blob if exclusions is not nil. Will include only the files matching the
|
|
// inclusions list if inclusions is not nil.
|
|
func Tgz(srcPath string, inclusions []string, exclusions []string, writers ...io.Writer) (int, error) {
|
|
if _, err := os.Stat(srcPath); err != nil {
|
|
return 0, fmt.Errorf("error inspecting srcPath %q: %w", srcPath, err)
|
|
}
|
|
|
|
gzw := gzip.NewWriter(io.MultiWriter(writers...))
|
|
defer gzw.Close()
|
|
|
|
return writeFile(srcPath, inclusions, exclusions, gzw)
|
|
}
|
|
|
|
// Tar will iterate over all files found in srcPath archiving with Tar. Will invoke every given writer while generating the tar.
|
|
// This is useful to generate checksums. Will exclude files matching the exclusions
|
|
// list blob if exclusions is not nil. Will include only the files matching the
|
|
// inclusions list if inclusions is not nil.
|
|
func Tar(srcPath string, inclusions []string, exclusions []string, writers ...io.Writer) (int, error) {
|
|
if _, err := os.Stat(srcPath); err != nil {
|
|
return 0, fmt.Errorf("error inspecting srcPath %q: %w", srcPath, err)
|
|
}
|
|
|
|
return writeFile(srcPath, inclusions, exclusions, io.MultiWriter(writers...))
|
|
}
|
|
|
|
func writeFile(srcPath string, inclusions []string, exclusions []string, writer io.Writer) (int, error) {
|
|
tw := tar.NewWriter(writer)
|
|
defer tw.Close()
|
|
|
|
t := &tgz{
|
|
srcPath: srcPath,
|
|
inclusions: inclusions,
|
|
exclusions: exclusions,
|
|
tarWriter: tw,
|
|
}
|
|
err := filepath.Walk(srcPath, t.tgzFile)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return t.filesWritten, nil
|
|
}
|
|
|
|
// Untgz will loop over the tar reader creating the file structure at dstPath.
|
|
// Callers must make sure dstPath is:
|
|
// - a full path
|
|
// - points to an empty directory or
|
|
// - points to a non-existing directory
|
|
func Untgz(dstPath string, r io.Reader, maxSize int64, preserveFileMode bool) error {
|
|
if !filepath.IsAbs(dstPath) {
|
|
return fmt.Errorf("dstPath points to a relative path: %s", dstPath)
|
|
}
|
|
|
|
gzr, err := gzip.NewReader(r)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading file: %w", err)
|
|
}
|
|
defer gzr.Close()
|
|
return untar(dstPath, io.LimitReader(gzr, maxSize), preserveFileMode)
|
|
}
|
|
|
|
// Untar will loop over the tar reader creating the file structure at dstPath.
|
|
// Callers must make sure dstPath is:
|
|
// - a full path
|
|
// - points to an empty directory or
|
|
// - points to a non-existing directory
|
|
func Untar(dstPath string, r io.Reader, maxSize int64, preserveFileMode bool) error {
|
|
if !filepath.IsAbs(dstPath) {
|
|
return fmt.Errorf("dstPath points to a relative path: %s", dstPath)
|
|
}
|
|
|
|
return untar(dstPath, io.LimitReader(r, maxSize), preserveFileMode)
|
|
}
|
|
|
|
// untar will loop over the tar reader creating the file structure at dstPath.
|
|
// Callers must make sure dstPath is:
|
|
// - a full path
|
|
// - points to an empty directory or
|
|
// - points to a non existing directory
|
|
func untar(dstPath string, r io.Reader, preserveFileMode bool) error {
|
|
tr := tar.NewReader(r)
|
|
|
|
for {
|
|
header, err := tr.Next()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return fmt.Errorf("error while iterating on tar reader: %w", err)
|
|
}
|
|
if header == nil || header.Name == "." || header.Name == "./" {
|
|
continue
|
|
}
|
|
|
|
target := filepath.Join(dstPath, header.Name)
|
|
// Sanity check to protect against zip-slip
|
|
if !Inbound(target, dstPath) {
|
|
return fmt.Errorf("illegal filepath in archive: %s", target)
|
|
}
|
|
|
|
switch header.Typeflag {
|
|
case tar.TypeDir:
|
|
var mode os.FileMode = 0o755
|
|
if preserveFileMode {
|
|
mode = os.FileMode(header.Mode)
|
|
}
|
|
err := os.MkdirAll(target, mode)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating nested folders: %w", err)
|
|
}
|
|
case tar.TypeSymlink:
|
|
// Sanity check to protect against symlink exploit
|
|
linkTarget := filepath.Join(filepath.Dir(target), header.Linkname)
|
|
realLinkTarget, err := filepath.EvalSymlinks(linkTarget)
|
|
if os.IsNotExist(err) {
|
|
realLinkTarget = linkTarget
|
|
} else if err != nil {
|
|
return fmt.Errorf("error checking symlink realpath: %w", err)
|
|
}
|
|
if !Inbound(realLinkTarget, dstPath) {
|
|
return fmt.Errorf("illegal filepath in symlink: %s", linkTarget)
|
|
}
|
|
|
|
// Relativizing all symlink targets because path.CheckOutOfBoundsSymlinks disallows any absolute symlinks
|
|
// and it makes more sense semantically to view symlinks in archives as relative.
|
|
// Inbound ensures that we never allow symlinks that break out of the target directory.
|
|
realLinkTarget, err = filepath.Rel(filepath.Dir(target), realLinkTarget)
|
|
if err != nil {
|
|
return fmt.Errorf("error relativizing link target: %w", err)
|
|
}
|
|
|
|
err = os.Symlink(realLinkTarget, target)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating symlink: %w", err)
|
|
}
|
|
case tar.TypeReg:
|
|
var mode os.FileMode = 0o644
|
|
if preserveFileMode {
|
|
mode = os.FileMode(header.Mode)
|
|
}
|
|
|
|
err := os.MkdirAll(filepath.Dir(target), 0o755)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating nested folders: %w", err)
|
|
}
|
|
|
|
f, err := os.OpenFile(target, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating file %q: %w", target, err)
|
|
}
|
|
w := bufio.NewWriter(f)
|
|
if _, err := io.Copy(w, tr); err != nil {
|
|
f.Close()
|
|
return fmt.Errorf("error writing tgz file: %w", err)
|
|
}
|
|
f.Close()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// tgzFile is used as a filepath.WalkFunc implementing the logic to write
|
|
// the given file in the tgz.tarWriter applying the exclusion pattern defined
|
|
// in tgz.exclusions, or the inclusion pattern defined in tgz.inclusions.
|
|
// Only regular files will be added in the tarball.
|
|
func (t *tgz) tgzFile(path string, fi os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return fmt.Errorf("error walking in %q: %w", t.srcPath, err)
|
|
}
|
|
|
|
base := filepath.Base(path)
|
|
|
|
relativePath, err := RelativePath(path, t.srcPath)
|
|
if err != nil {
|
|
return fmt.Errorf("relative path error: %w", err)
|
|
}
|
|
|
|
if t.inclusions != nil && base != "." && !fi.IsDir() {
|
|
included := false
|
|
for _, inclusionPattern := range t.inclusions {
|
|
found, err := filepath.Match(inclusionPattern, base)
|
|
if err != nil {
|
|
return fmt.Errorf("error verifying inclusion pattern %q: %w", inclusionPattern, err)
|
|
}
|
|
if found {
|
|
included = true
|
|
break
|
|
}
|
|
}
|
|
if !included {
|
|
return nil
|
|
}
|
|
}
|
|
if t.exclusions != nil {
|
|
for _, exclusionPattern := range t.exclusions {
|
|
found, err := filepath.Match(exclusionPattern, relativePath)
|
|
if err != nil {
|
|
return fmt.Errorf("error verifying exclusion pattern %q: %w", exclusionPattern, err)
|
|
}
|
|
if found {
|
|
if fi.IsDir() {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
if !supportedFileMode(fi) {
|
|
return nil
|
|
}
|
|
|
|
link := ""
|
|
if IsSymlink(fi) {
|
|
link, err = os.Readlink(path)
|
|
if err != nil {
|
|
return fmt.Errorf("error getting link target: %w", err)
|
|
}
|
|
}
|
|
|
|
header, err := tar.FileInfoHeader(fi, link)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating a tar file header: %w", err)
|
|
}
|
|
|
|
// update the name to correctly reflect the desired destination when untaring
|
|
header.Name = relativePath
|
|
|
|
if err := t.tarWriter.WriteHeader(header); err != nil {
|
|
return fmt.Errorf("error writing header: %w", err)
|
|
}
|
|
|
|
// Only regular files needs to have their content copied.
|
|
// Directories and symlinks are header only.
|
|
if fi.Mode().IsRegular() {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return fmt.Errorf("error opening file %q: %w", fi.Name(), err)
|
|
}
|
|
defer func() {
|
|
err := f.Close()
|
|
if err != nil {
|
|
log.Errorf("error closing file %q: %v", fi.Name(), err)
|
|
}
|
|
}()
|
|
|
|
if _, err := io.Copy(t.tarWriter, f); err != nil {
|
|
return fmt.Errorf("error copying tgz file to writers: %w", err)
|
|
}
|
|
t.filesWritten++
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// supportedFileMode will return true if the file mode is supported.
|
|
// Supported files means that it will be added to the tarball.
|
|
func supportedFileMode(fi os.FileInfo) bool {
|
|
mode := fi.Mode()
|
|
if mode.IsRegular() || mode.IsDir() || IsSymlink(fi) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|