zsvo/pkg/packager/packager.go
itexpert228 e351ee62d1
кал
2026-03-09 22:40:05 +03:00

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 }