File archive.go of Package obs-service-dotnet_packages
package main
import (
"archive/tar"
"compress/bzip2"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"io/fs"
"log/slog"
"os"
"path"
"path/filepath"
"time"
"github.com/aibor/cpio"
"github.com/klauspost/compress/zstd"
)
type compressionType string
const (
compressionTypeNone = "none"
compressionTypeGZip = "gz"
compressionTypeZstd = "zst"
)
func (c *compressionType) String() string {
if c == nil {
return "<nil>"
}
return string(*c)
}
func (c *compressionType) Set(value string) error {
switch value {
case compressionTypeNone, compressionTypeGZip, compressionTypeZstd:
*c = compressionType(value)
return nil
}
return fmt.Errorf("invalid copmression type %s", value)
}
func createArchive(sourceDir, outputBase string, compressionType compressionType) error {
var extension string
compress := func(w io.Writer) (io.Writer, error) { return w, nil }
switch compressionType {
case compressionTypeNone:
extension = ".tar"
case compressionTypeGZip:
extension = ".tar.gz"
compress = func(w io.Writer) (io.Writer, error) { return gzip.NewWriter(w), nil }
case compressionTypeZstd:
extension = ".tar.zst"
compress = func(w io.Writer) (io.Writer, error) { return zstd.NewWriter(w) }
}
outputPath := outputBase + extension
temporaryPattern := filepath.Base(outputBase) + ".*" + extension
outputFile, err := os.CreateTemp(filepath.Dir(outputBase), temporaryPattern)
if err != nil {
return err
}
defer func() {
_ = outputFile.Close()
_ = os.Remove(outputFile.Name())
}()
compressWriter, err := compress(outputFile)
if err != nil {
return err
}
tarWriter := tar.NewWriter(compressWriter)
// Use a custom walk function to avoid embedding user/group info into the archive.
dirFS := os.DirFS(sourceDir)
err = fs.WalkDir(dirFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if path == "." {
return nil
}
info, err := d.Info()
if err != nil {
return err
}
if !d.IsDir() && !info.Mode().IsRegular() {
return fmt.Errorf("failed to handle non-regular file %s", path)
}
h, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
h.Name = path
if d.IsDir() {
h.Name += "/"
}
h.Uid = 0
h.Uname = ""
h.Gid = 0
h.Gname = ""
if err := tarWriter.WriteHeader(h); err != nil {
return err
}
if info.Mode().IsRegular() {
f, err := dirFS.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(tarWriter, f)
return err
}
return nil
})
if err != nil {
return err
}
if err := tarWriter.Close(); err != nil {
return err
}
if closer, ok := compressWriter.(io.Closer); ok {
if err := closer.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
return err
}
}
if err := outputFile.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
return err
}
if err := os.Chmod(outputFile.Name(), 0o644); err != nil {
return fmt.Errorf("failed to set permissions for package file: %w", err)
}
return os.Rename(outputFile.Name(), outputPath)
}
// Extract an archive, returning the names of the solution files.
func extractArchive(ctx context.Context, archivePath, outDir string) ([]string, error) {
slog.InfoContext(ctx, "extracting archive", "archive", archivePath)
switch filepath.Ext(archivePath) {
case ".cpio", ".obscpio":
return extractCpio(ctx, archivePath, outDir)
case ".tar", ".tar.gz", ".tar.zst":
return extractTar(ctx, archivePath, outDir)
}
return nil, fmt.Errorf("unsupported archive format %s", filepath.Ext(archivePath))
}
type fileInfo struct {
name string // relative name including path; not (necessarily) base name.
fs.FileInfo
accessTime time.Time
isLink bool // is a hard link (only for tar files)
linkName string // link target, for hard links and symlinks.
}
func writeFile(ctx context.Context, outDir string, reader io.Reader, fileInfo fileInfo) error {
outPath := filepath.Join(outDir, fileInfo.name)
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
return fmt.Errorf("failed to ensure parent directory %s: %w", filepath.Dir(outPath), err)
}
switch {
case fileInfo.Mode().IsDir():
if err := os.MkdirAll(outPath, fileInfo.Mode()); err != nil {
return fmt.Errorf("error creating directory %s: %w", fileInfo.name, err)
}
case fileInfo.isLink:
if err := os.Link(filepath.Join(outDir, fileInfo.linkName), outPath); err != nil {
return fmt.Errorf("failed to create hard link %s: %w", fileInfo.name, err)
}
case fileInfo.Mode()&fs.ModeType == fs.ModeSymlink:
if err := os.Symlink(filepath.Join(outDir, fileInfo.linkName), outPath); err != nil {
return fmt.Errorf("failed to create symlink %s: %w", fileInfo.name, err)
}
case fileInfo.Mode()&fs.ModeType == 0:
outFile, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY, fileInfo.Mode()&fs.ModePerm)
if err != nil {
return fmt.Errorf("failed to create member %s: %w", fileInfo.name, err)
}
n, err := io.Copy(outFile, reader)
if err != nil {
return fmt.Errorf("failed to extract member %s: %w", fileInfo.name, err)
}
if n < fileInfo.Size() {
return fmt.Errorf("short write extracting memeber %s: %d/%d bytes", fileInfo.name, n, fileInfo.Size())
}
default:
slog.WarnContext(ctx, "skipping unsupported file type", "member", fileInfo.name)
return nil
}
if err := os.Chmod(outPath, fileInfo.Mode()); err != nil {
slog.WarnContext(ctx, "error setting file mode", "member", fileInfo.name, "error", err)
}
if err := os.Chtimes(outPath, fileInfo.accessTime, fileInfo.ModTime()); err != nil {
slog.WarnContext(ctx, "failed to set file times", "member", fileInfo.name, "error", err)
}
return nil
}
func extractTar(ctx context.Context, archivePath, outDir string) ([]string, error) {
rawReader, err := os.Open(archivePath)
if err != nil {
return nil, err
}
defer rawReader.Close()
var decompressor io.Reader
switch filepath.Ext(archivePath) {
case ".tar":
decompressor = rawReader
case ".gz":
decompressor, err = gzip.NewReader(rawReader)
case ".bz2":
decompressor = bzip2.NewReader(rawReader)
case ".zst":
decompressor, err = zstd.NewReader(rawReader)
default:
err = fmt.Errorf("could not detect tar compression for %s", archivePath)
}
if err != nil {
return nil, err
}
var solutions []string
reader := tar.NewReader(decompressor)
for {
header, err := reader.Next()
if errors.Is(err, io.EOF) {
return solutions, nil
}
if err != nil {
return nil, fmt.Errorf("failed to read archive %s: %w", archivePath, err)
}
fileInfo := fileInfo{
name: header.Name,
FileInfo: header.FileInfo(),
accessTime: header.AccessTime,
isLink: header.Typeflag == tar.TypeLink,
linkName: header.Linkname,
}
if err := writeFile(ctx, outDir, reader, fileInfo); err != nil {
return nil, err
}
if path.Ext(header.Name) == ".sln" {
solutions = append(solutions, header.Name)
}
}
}
func extractCpio(ctx context.Context, archivePath, outDir string) ([]string, error) {
file, err := os.Open(archivePath)
if err != nil {
return nil, fmt.Errorf("failed to open archive %s: %w", archivePath, err)
}
defer file.Close()
reader := cpio.NewReader(file)
var solutions []string
for {
header, err := reader.Next()
if errors.Is(err, io.EOF) {
return solutions, nil
}
if err != nil {
return nil, fmt.Errorf("failed to read cpio record: %w", err)
}
fileInfo := fileInfo{
name: header.Name,
FileInfo: header.FileInfo(),
accessTime: time.Time{},
}
if fileInfo.Mode()&fs.ModeType == fs.ModeSymlink {
buf, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("failed to read symlink %s: %w", header.Name, err)
}
fileInfo.linkName = string(buf)
}
if err := writeFile(ctx, outDir, reader, fileInfo); err != nil {
return nil, err
}
if filepath.Ext(header.Name) == ".sln" {
solutions = append(solutions, header.Name)
}
}
}