404 lines
9.9 KiB
Go
404 lines
9.9 KiB
Go
package installer
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"zsvo/pkg/packager"
|
|
"zsvo/pkg/types"
|
|
)
|
|
|
|
// Installer handles installing and removing packages
|
|
type Installer struct {
|
|
rootDir string
|
|
pkgDB string
|
|
}
|
|
|
|
// NewInstaller creates a new installer
|
|
func NewInstaller(rootDir string) *Installer {
|
|
return &Installer{
|
|
rootDir: rootDir,
|
|
pkgDB: filepath.Join(rootDir, "var", "lib", "pkgdb"),
|
|
}
|
|
}
|
|
|
|
// Install installs a package
|
|
func (i *Installer) Install(packagePath string) error {
|
|
// Extract package to temporary directory
|
|
tmpDir, err := os.MkdirTemp("", "install-")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Extract package
|
|
pkg := packager.NewPackager(i.rootDir)
|
|
if err := pkg.Extract(packagePath, tmpDir); err != nil {
|
|
return fmt.Errorf("failed to extract package: %w", err)
|
|
}
|
|
|
|
// Read package info
|
|
pkgInfo, err := pkg.ReadPkgInfo(packagePath)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read package info: %w", err)
|
|
}
|
|
|
|
// Check dependencies
|
|
if err := i.checkDependencies(pkgInfo.Dependencies); err != nil {
|
|
return fmt.Errorf("dependency check failed: %w", err)
|
|
}
|
|
|
|
// Install files
|
|
if err := i.installFiles(tmpDir, pkgInfo); err != nil {
|
|
return fmt.Errorf("failed to install files: %w", err)
|
|
}
|
|
|
|
// Register package
|
|
if err := i.registerPackage(pkgInfo); err != nil {
|
|
return fmt.Errorf("failed to register package: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Remove removes a package
|
|
func (i *Installer) Remove(packageName string) error {
|
|
// Get package info
|
|
pkgInfo, err := i.getPackageInfo(packageName)
|
|
if err != nil {
|
|
return fmt.Errorf("package not found: %w", err)
|
|
}
|
|
|
|
// Remove files
|
|
if err := i.removeFiles(pkgInfo); err != nil {
|
|
return fmt.Errorf("failed to remove files: %w", err)
|
|
}
|
|
|
|
// Unregister package
|
|
if err := i.unregisterPackage(packageName); err != nil {
|
|
return fmt.Errorf("failed to unregister package: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListInstalled lists all installed packages
|
|
func (i *Installer) ListInstalled() ([]string, error) {
|
|
var packages []string
|
|
|
|
// Check if pkgdb directory exists
|
|
if _, err := os.Stat(i.pkgDB); os.IsNotExist(err) {
|
|
return packages, nil
|
|
}
|
|
|
|
// Read pkgdb directory
|
|
entries, err := os.ReadDir(i.pkgDB)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read package database: %w", err)
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
packages = append(packages, entry.Name())
|
|
}
|
|
}
|
|
|
|
return packages, nil
|
|
}
|
|
|
|
// GetPackageInfo gets information about an installed package
|
|
func (i *Installer) GetPackageInfo(packageName string) (*types.PkgInfo, error) {
|
|
return i.getPackageInfo(packageName)
|
|
}
|
|
|
|
// checkDependencies checks if all dependencies are installed
|
|
func (i *Installer) checkDependencies(deps []string) error {
|
|
installed, err := i.ListInstalled()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, dep := range deps {
|
|
found := false
|
|
for _, installedPkg := range installed {
|
|
if installedPkg == dep {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("dependency not installed: %s", dep)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// installFiles installs files from package
|
|
func (i *Installer) installFiles(tmpDir string, pkgInfo *types.PkgInfo) error {
|
|
// Validate input paths
|
|
if tmpDir == "" {
|
|
return fmt.Errorf("temporary directory cannot be empty")
|
|
}
|
|
if i.rootDir == "" {
|
|
return fmt.Errorf("root directory cannot be empty")
|
|
}
|
|
|
|
// Clean and normalize paths
|
|
tmpDir = filepath.Clean(tmpDir)
|
|
i.rootDir = filepath.Clean(i.rootDir)
|
|
|
|
// Create root directory if it doesn't exist
|
|
if err := os.MkdirAll(i.rootDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create root directory %s: %w", i.rootDir, err)
|
|
}
|
|
|
|
// Copy files to root directory
|
|
for _, file := range pkgInfo.Files {
|
|
// Validate file path
|
|
if file == "" {
|
|
return fmt.Errorf("file path cannot be empty")
|
|
}
|
|
|
|
// Clean file path to prevent directory traversal
|
|
file = filepath.Clean(file)
|
|
if strings.HasPrefix(file, "..") {
|
|
return fmt.Errorf("invalid file path: %s", file)
|
|
}
|
|
|
|
src := filepath.Join(tmpDir, file)
|
|
dst := filepath.Join(i.rootDir, file)
|
|
|
|
// Create parent directory
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
|
return fmt.Errorf("failed to create directory %s: %w", filepath.Dir(dst), err)
|
|
}
|
|
|
|
// Copy file
|
|
if err := i.copyFile(src, dst); err != nil {
|
|
return fmt.Errorf("failed to copy file %s: %w", file, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// removeFiles removes files of a package
|
|
func (i *Installer) removeFiles(pkgInfo *types.PkgInfo) error {
|
|
for _, file := range pkgInfo.Files {
|
|
path := filepath.Join(i.rootDir, file)
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("failed to remove file %s: %w", file, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// registerPackage registers a package in the database
|
|
func (i *Installer) registerPackage(pkgInfo *types.PkgInfo) error {
|
|
// Create package database directory
|
|
pkgDir := filepath.Join(i.pkgDB, pkgInfo.Name)
|
|
if err := os.MkdirAll(pkgDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create package database directory: %w", err)
|
|
}
|
|
|
|
// Write package info
|
|
pkgInfoFile := filepath.Join(pkgDir, ".pkginfo")
|
|
file, err := os.Create(pkgInfoFile)
|
|
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
|
|
}
|
|
|
|
// unregisterPackage unregisters a package from the database
|
|
func (i *Installer) unregisterPackage(packageName string) error {
|
|
pkgDir := filepath.Join(i.pkgDB, packageName)
|
|
return os.RemoveAll(pkgDir)
|
|
}
|
|
|
|
// getPackageInfo gets package info from database
|
|
func (i *Installer) getPackageInfo(packageName string) (*types.PkgInfo, error) {
|
|
pkgDir := filepath.Join(i.pkgDB, packageName)
|
|
pkgInfoFile := filepath.Join(pkgDir, ".pkginfo")
|
|
|
|
// Check if package directory exists
|
|
if _, err := os.Stat(pkgDir); os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("package %s not found in database", packageName)
|
|
}
|
|
|
|
// Check if .pkginfo file exists
|
|
if _, err := os.Stat(pkgInfoFile); os.IsNotExist(err) {
|
|
return nil, fmt.Errorf("package info file not found for %s", packageName)
|
|
}
|
|
|
|
file, err := os.Open(pkgInfoFile)
|
|
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
|
|
}
|
|
|
|
// copyFile copies a file from src to dst
|
|
func (i *Installer) copyFile(src, dst string) error {
|
|
sourceFile, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer sourceFile.Close()
|
|
|
|
destFile, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer destFile.Close()
|
|
|
|
_, err = io.Copy(destFile, sourceFile)
|
|
return err
|
|
}
|
|
|
|
// GetRootDir returns the root directory
|
|
func (i *Installer) GetRootDir() string {
|
|
return i.rootDir
|
|
}
|
|
|
|
// GetPkgDB returns the package database directory
|
|
func (i *Installer) GetPkgDB() string {
|
|
return i.pkgDB
|
|
}
|
|
|
|
// IsInstalled checks if a package is installed
|
|
func (i *Installer) IsInstalled(packageName string) bool {
|
|
pkgDir := filepath.Join(i.pkgDB, packageName)
|
|
_, err := os.Stat(pkgDir)
|
|
return err == nil
|
|
}
|
|
|
|
// GetInstalledVersion gets the installed version of a package
|
|
func (i *Installer) GetInstalledVersion(packageName string) (string, error) {
|
|
pkgInfo, err := i.getPackageInfo(packageName)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return pkgInfo.Version, nil
|
|
}
|
|
|
|
// GetInstalledFiles gets the list of installed files for a package
|
|
func (i *Installer) GetInstalledFiles(packageName string) ([]string, error) {
|
|
pkgInfo, err := i.getPackageInfo(packageName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return pkgInfo.Files, nil
|
|
}
|
|
|
|
// VerifyPackage verifies that all files of a package exist
|
|
func (i *Installer) VerifyPackage(packageName string) error {
|
|
pkgInfo, err := i.getPackageInfo(packageName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, file := range pkgInfo.Files {
|
|
path := filepath.Join(i.rootDir, file)
|
|
if _, err := os.Stat(path); err != nil {
|
|
return fmt.Errorf("missing file: %s", file)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetPackageSize gets the total size of installed package files
|
|
func (i *Installer) GetPackageSize(packageName string) (int64, error) {
|
|
pkgInfo, err := i.getPackageInfo(packageName)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var totalSize int64
|
|
for _, file := range pkgInfo.Files {
|
|
path := filepath.Join(i.rootDir, file)
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
totalSize += info.Size()
|
|
}
|
|
|
|
return totalSize, nil
|
|
}
|