Some checks failed
CloudSave/pipeline/head Something is wrong with the build of this commit
141 lines
3.1 KiB
Go
141 lines
3.1 KiB
Go
package archive
|
||
|
||
import (
|
||
"archive/tar"
|
||
"compress/gzip"
|
||
"fmt"
|
||
"io"
|
||
"log/slog"
|
||
"os"
|
||
"path/filepath"
|
||
)
|
||
|
||
const (
|
||
// Tune these to your app’s needs
|
||
maxCompressedUpload = 500 << 20 // 500 MiB compressed
|
||
maxUncompressedOutput = 1000 << 20 // 100 MiB after inflate
|
||
)
|
||
|
||
func Untar(file io.Reader, path string) error {
|
||
gzr, err := gzip.NewReader(file)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer gzr.Close()
|
||
|
||
tr := tar.NewReader(gzr)
|
||
|
||
for {
|
||
header, err := tr.Next()
|
||
|
||
switch {
|
||
|
||
// if no more files are found return
|
||
case err == io.EOF:
|
||
return nil
|
||
|
||
// return any other error
|
||
case err != nil:
|
||
return err
|
||
|
||
// if the header is nil, just skip it (not sure how this happens)
|
||
case header == nil:
|
||
continue
|
||
}
|
||
|
||
// the target location where the dir/file should be created
|
||
target := filepath.Clean(filepath.Join(path, filepath.Clean(header.Name)))
|
||
|
||
// the following switch could also be done using fi.Mode(), not sure if there
|
||
// a benefit of using one vs. the other.
|
||
// fi := header.FileInfo()
|
||
|
||
// check the file type
|
||
switch header.Typeflag {
|
||
|
||
// if its a dir and it doesn't exist create it
|
||
case tar.TypeDir:
|
||
if _, err := os.Stat(target); err != nil {
|
||
if err := os.MkdirAll(target, 0600); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
// if it's a file create it
|
||
case tar.TypeReg:
|
||
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, header.FileInfo().Mode())
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
limited := &io.LimitedReader{R: gzr, N: maxUncompressedOutput}
|
||
|
||
// copy over contents
|
||
if _, err := io.Copy(f, limited); err != nil {
|
||
return err
|
||
}
|
||
|
||
// manually close here after each file operation; defering would cause each file close
|
||
// to wait until all operations have completed.
|
||
if err := f.Close(); err != nil {
|
||
slog.Error("failed to close file", "err", err)
|
||
}
|
||
|
||
if limited.N == 0 {
|
||
// Limit exhausted → likely bomb
|
||
return fmt.Errorf("payload too large after decompression")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func Tar(file io.Writer, root string) error {
|
||
gw := gzip.NewWriter(file)
|
||
defer gw.Close()
|
||
|
||
tw := tar.NewWriter(gw)
|
||
defer tw.Close()
|
||
|
||
// Walk again to add files
|
||
err := filepath.Walk(root, func(path string, info os.FileInfo, walkErr error) error {
|
||
if walkErr != nil {
|
||
return fmt.Errorf("failed to walk through the directory: %w", walkErr)
|
||
}
|
||
|
||
relpath, err := filepath.Rel(root, path)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to make relative path: %w", err)
|
||
}
|
||
|
||
// Create tar header
|
||
header, err := tar.FileInfoHeader(info, path)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to make file info header: %w", err)
|
||
}
|
||
header.Name = relpath
|
||
|
||
if err := tw.WriteHeader(header); err != nil {
|
||
return fmt.Errorf("failed to write header: %w", err)
|
||
}
|
||
|
||
if !info.Mode().IsRegular() {
|
||
return nil
|
||
}
|
||
|
||
file, err := os.Open(filepath.Clean(path))
|
||
if err != nil {
|
||
return fmt.Errorf("failed to open file: %w", err)
|
||
}
|
||
defer file.Close()
|
||
if _, err := io.Copy(tw, file); err != nil {
|
||
return fmt.Errorf("failed to copy file: %w", err)
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return fmt.Errorf("writing tar entries: %w", err)
|
||
}
|
||
|
||
return nil
|
||
}
|