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) }