309 lines
8.3 KiB
Go
309 lines
8.3 KiB
Go
package packager
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"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.Dependencies,
|
|
Files: files,
|
|
InstallDate: time.Now().Format(time.RFC3339),
|
|
}
|
|
|
|
// Create package info file
|
|
pkgInfoFile := filepath.Join(packageDir, ".pkginfo")
|
|
if err := p.writePkgInfo(pkgInfoFile, pkgInfo); err != nil {
|
|
return fmt.Errorf("failed to write package info to %s: %w", pkgInfoFile, err)
|
|
}
|
|
|
|
// Create package archive
|
|
if err := p.createArchive(stagingDir, packageFile, pkgInfoFile); 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()
|
|
|
|
// Write package info in simple format
|
|
content := fmt.Sprintf(
|
|
"name = %q\nversion = %q\ndescription = %q\ndependencies = %v\nfiles = %v\ninstall_date = %q\n",
|
|
pkgInfo.Name, pkgInfo.Version, pkgInfo.Description, pkgInfo.Dependencies, pkgInfo.Files, pkgInfo.InstallDate,
|
|
)
|
|
_, err = file.WriteString(content)
|
|
return err
|
|
}
|
|
|
|
// createArchive creates a compressed archive
|
|
func (p *Packager) createArchive(sourceDir, archivePath, pkgInfoFile string) error {
|
|
// Create archive using archiver.Archive
|
|
paths := []string{sourceDir, pkgInfoFile}
|
|
return archiver.Archive(paths, archivePath)
|
|
}
|
|
|
|
// 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, ".pkginfo")
|
|
|
|
// Check if .pkginfo 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()
|
|
|
|
var pkgInfo types.PkgInfo
|
|
// Simple parser for our package info format
|
|
// In a real implementation, you'd want to use a proper TOML parser
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
|
|
parts := strings.SplitN(line, "=", 2)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
|
|
key := strings.TrimSpace(parts[0])
|
|
value := strings.TrimSpace(parts[1])
|
|
|
|
// Remove quotes from value
|
|
value = strings.Trim(value, "\"")
|
|
|
|
switch key {
|
|
case "name":
|
|
pkgInfo.Name = value
|
|
case "version":
|
|
pkgInfo.Version = value
|
|
case "description":
|
|
pkgInfo.Description = value
|
|
case "dependencies":
|
|
// Parse dependencies array
|
|
value = strings.Trim(value, "[]")
|
|
if value != "" {
|
|
pkgInfo.Dependencies = strings.Split(value, ",")
|
|
for i, dep := range pkgInfo.Dependencies {
|
|
pkgInfo.Dependencies[i] = strings.TrimSpace(strings.Trim(dep, "\""))
|
|
}
|
|
}
|
|
case "files":
|
|
// Parse files array
|
|
value = strings.Trim(value, "[]")
|
|
if value != "" {
|
|
pkgInfo.Files = strings.Split(value, ",")
|
|
for i, file := range pkgInfo.Files {
|
|
pkgInfo.Files[i] = strings.TrimSpace(strings.Trim(file, "\""))
|
|
}
|
|
}
|
|
case "install_date":
|
|
pkgInfo.InstallDate = value
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate that we actually parsed some data
|
|
if pkgInfo.Name == "" {
|
|
return nil, fmt.Errorf("invalid package info: missing name")
|
|
}
|
|
|
|
return &pkgInfo, nil
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// pkgInfoFileInfo implements os.FileInfo for package info
|
|
type pkgInfoFileInfo struct {
|
|
name string
|
|
size int64
|
|
}
|
|
|
|
func (f *pkgInfoFileInfo) Name() string { return f.name }
|
|
func (f *pkgInfoFileInfo) Size() int64 { return f.size }
|
|
func (f *pkgInfoFileInfo) Mode() os.FileMode { return 0644 }
|
|
func (f *pkgInfoFileInfo) ModTime() time.Time { return time.Now() }
|
|
func (f *pkgInfoFileInfo) IsDir() bool { return false }
|
|
func (f *pkgInfoFileInfo) Sys() interface{} { return nil }
|