294 lines
7.1 KiB
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)
|
|
}
|