zsvo/pkg/packager/packager.go

294 lines
7.1 KiB
Go

package packager
import (
"archive/tar"
"fmt"
"io"
"os"
"path/filepath"
"time"
"github.com/klauspost/compress/zstd"
"github.com/mholt/archiver/v3"
"zsvo/pkg/recipe"
"zsvo/pkg/types"
)
// Packager handles creating and extracting package archives
type Packager struct {
workDir string
}
// NewPackager creates a new packager
func NewPackager(workDir string) *Packager {
return &Packager{
workDir: workDir,
}
}
// Package creates a package archive from staging directory
func (p *Packager) Package(recipe *recipe.Recipe) error {
// Validate recipe
if recipe == nil {
return fmt.Errorf("recipe cannot be nil")
}
if recipe.Name == "" {
return fmt.Errorf("recipe name cannot be empty")
}
if recipe.Version == "" {
return fmt.Errorf("recipe version cannot be empty")
}
stagingDir := recipe.GetStagingDir(p.workDir)
packageDir := recipe.GetPackageDir(p.workDir)
packageFile := filepath.Join(packageDir, recipe.GetPackageFileName())
// Validate paths
if stagingDir == "" {
return fmt.Errorf("staging directory cannot be empty")
}
if packageDir == "" {
return fmt.Errorf("package directory cannot be empty")
}
if packageFile == "" {
return fmt.Errorf("package file path cannot be empty")
}
// Clean and normalize paths
stagingDir = filepath.Clean(stagingDir)
packageDir = filepath.Clean(packageDir)
packageFile = filepath.Clean(packageFile)
// Create package directory
if err := os.MkdirAll(packageDir, 0755); err != nil {
return fmt.Errorf("failed to create package directory %s: %w", packageDir, err)
}
// List files in staging directory
files, err := p.listFiles(stagingDir)
if err != nil {
return fmt.Errorf("failed to list files in %s: %w", stagingDir, err)
}
// Create package info
pkgInfo := &types.PkgInfo{
Name: recipe.Name,
Version: recipe.Version,
Description: recipe.Description,
Dependencies: recipe.Deps,
Files: files,
InstallDate: time.Now().Format(time.RFC3339),
}
// Create package info file
pkgInfoFile := filepath.Join(stagingDir, types.PackageMetadataFile)
if err := p.writePkgInfo(pkgInfoFile, pkgInfo); err != nil {
return fmt.Errorf("failed to write package info to %s: %w", pkgInfoFile, err)
}
defer os.Remove(pkgInfoFile)
// Create package archive
if err := p.createArchive(stagingDir, packageFile); err != nil {
return fmt.Errorf("failed to create package archive %s: %w", packageFile, err)
}
return nil
}
// listFiles lists all files in a directory recursively
func (p *Packager) listFiles(dir string) ([]string, error) {
var files []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
// Convert to relative path
relPath, err := filepath.Rel(dir, path)
if err != nil {
return err
}
files = append(files, relPath)
}
return nil
})
return files, err
}
// writePkgInfo writes package info to file
func (p *Packager) writePkgInfo(path string, pkgInfo *types.PkgInfo) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
return types.WritePkgInfo(file, pkgInfo)
}
// createArchive creates a compressed archive
func (p *Packager) createArchive(sourceDir, archivePath string) error {
outFile, err := os.Create(archivePath)
if err != nil {
return err
}
defer outFile.Close()
zstdWriter, err := zstd.NewWriter(outFile)
if err != nil {
return err
}
defer zstdWriter.Close()
tarWriter := tar.NewWriter(zstdWriter)
defer tarWriter.Close()
return filepath.Walk(sourceDir, func(path string, info os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
if path == sourceDir {
return nil
}
relPath, err := filepath.Rel(sourceDir, path)
if err != nil {
return err
}
relPath = filepath.ToSlash(relPath)
var linkTarget string
if info.Mode()&os.ModeSymlink != 0 {
linkTarget, err = os.Readlink(path)
if err != nil {
return err
}
}
header, err := tar.FileInfoHeader(info, linkTarget)
if err != nil {
return err
}
header.Name = relPath
if err := tarWriter.WriteHeader(header); err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
if _, err := io.Copy(tarWriter, file); err != nil {
file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
return nil
})
}
// Extract extracts a package archive
func (p *Packager) Extract(archivePath, destDir string) error {
// Create destination directory
if err := os.MkdirAll(destDir, 0755); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}
// Extract archive using archiver.Unarchive
return archiver.Unarchive(archivePath, destDir)
}
// ReadPkgInfo reads package info from archive
func (p *Packager) ReadPkgInfo(archivePath string) (*types.PkgInfo, error) {
// Extract to temporary directory
tmpDir, err := os.MkdirTemp("", "pkginfo-")
if err != nil {
return nil, fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
if err := p.Extract(archivePath, tmpDir); err != nil {
return nil, fmt.Errorf("failed to extract archive: %w", err)
}
// Read package info
pkgInfoFile := filepath.Join(tmpDir, types.PackageMetadataFile)
// Check if metadata file exists.
if _, err := os.Stat(pkgInfoFile); os.IsNotExist(err) {
return nil, fmt.Errorf("package info file not found in archive")
}
return p.readPkgInfoFromFile(pkgInfoFile)
}
// readPkgInfoFromFile reads package info from file
func (p *Packager) readPkgInfoFromFile(path string) (*types.PkgInfo, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
return types.ReadPkgInfo(file)
}
// VerifyPackage verifies package integrity
func (p *Packager) VerifyPackage(archivePath string) error {
// Check if file exists
if _, err := os.Stat(archivePath); err != nil {
return fmt.Errorf("package file does not exist: %w", err)
}
// Try to extract to verify integrity
tmpDir, err := os.MkdirTemp("", "verify-")
if err != nil {
return fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
return p.Extract(archivePath, tmpDir)
}
// ListPackageContents lists contents of a package
func (p *Packager) ListPackageContents(archivePath string) ([]string, error) {
// Extract to temporary directory
tmpDir, err := os.MkdirTemp("", "list-")
if err != nil {
return nil, fmt.Errorf("failed to create temp directory: %w", err)
}
defer os.RemoveAll(tmpDir)
if err := p.Extract(archivePath, tmpDir); err != nil {
return nil, fmt.Errorf("failed to extract archive: %w", err)
}
// List files
return p.listFiles(tmpDir)
}
// GetPackageDir returns the package directory
func (p *Packager) GetPackageDir(recipe *recipe.Recipe) string {
return recipe.GetPackageDir(p.workDir)
}
// GetPackageFile returns the package file path
func (p *Packager) GetPackageFile(recipe *recipe.Recipe) string {
return filepath.Join(p.GetPackageDir(recipe), recipe.GetPackageFileName())
}
// Clean removes package files
func (p *Packager) Clean(recipe *recipe.Recipe) error {
packageDir := p.GetPackageDir(recipe)
return os.RemoveAll(packageDir)
}