This commit is contained in:
partisan 2025-03-09 11:42:53 +01:00
commit d0187f94d7
23 changed files with 2489 additions and 0 deletions

304
spm/install.go Normal file
View file

@ -0,0 +1,304 @@
package spm
import (
"archive/tar"
"bufio"
"compress/gzip"
"fmt"
"io"
"math/rand"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sync"
"time"
)
func DecompressToTemp(filePath string) (string, error) {
UpdateProgress(0, "Decompressing package")
// 1) Base temp dir
baseTempDir := GetTempDir()
// 2) Create a unique subfolder inside the base temp dir
subfolderName := fmt.Sprintf("spm_decompress_%d", rand.Intn(1000000))
decompressDir := filepath.Join(baseTempDir, subfolderName)
if err := os.MkdirAll(decompressDir, 0755); err != nil {
return "", fmt.Errorf("failed to create decompress dir: %w", err)
}
// 3) Open the tar.gz file
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
gzr, err := gzip.NewReader(file)
if err != nil {
return "", err
}
defer gzr.Close()
tarReader := tar.NewReader(gzr)
// 4) Count total files
var totalFiles, processedFiles int
for {
_, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
totalFiles++
}
// 5) Reset file position and tar reader
if _, err := file.Seek(0, io.SeekStart); err != nil {
return "", err
}
if err := gzr.Reset(file); err != nil {
return "", err
}
tarReader = tar.NewReader(gzr)
// 6) Extract into `decompressDir`
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
targetPath := filepath.Join(decompressDir, header.Name)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
return "", err
}
case tar.TypeReg:
outFile, err := os.Create(targetPath)
if err != nil {
return "", err
}
if _, err := io.Copy(outFile, tarReader); err != nil {
outFile.Close()
return "", err
}
outFile.Close()
}
processedFiles++
UpdateProgress(int(float64(processedFiles)/float64(totalFiles)*100), "Decompressing package")
}
UpdateProgress(100, "Package decompressed")
return decompressDir, nil
}
// tailLogFile continuously reads new lines from logFile until done is closed.
// It prints each new line and searches for a percentage (e.g. " 47%") to call UpdateProgress.
func tailLogFile(logFile string, done <-chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
var offset int64
re := regexp.MustCompile(`\s+(\d+)%`)
for {
select {
case <-done:
return
default:
f, err := os.Open(logFile)
if err != nil {
time.Sleep(200 * time.Millisecond)
continue
}
// Seek to the last known offset.
f.Seek(offset, io.SeekStart)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
fmt.Printf("[ROBOPROGRESS-LOG] %s\n", line)
// Look for a percentage.
if matches := re.FindStringSubmatch(line); len(matches) == 2 {
var percent int
_, err := fmt.Sscanf(matches[1], "%d", &percent)
if err == nil {
fmt.Printf("[ROBOPROGRESS] Parsed progress: %d%%\n", percent)
UpdateProgress(percent, "Copying files to install directory")
}
}
}
// Update the offset.
newOffset, _ := f.Seek(0, io.SeekCurrent)
offset = newOffset
f.Close()
time.Sleep(200 * time.Millisecond)
}
}
}
// MoveFilesToInstallDir copies files from tempDir to installDir.
// On Windows it uses robocopy with logging so that as much output as possible is printed,
// and a separate goroutine tails the log file to update progress.
// The log file is saved (not automatically deleted) so you can inspect it.
func MoveFilesToInstallDir(tempDir, installDir, pkgType string) error {
// Ensure tempDir exists.
if _, err := os.Stat(tempDir); os.IsNotExist(err) {
return fmt.Errorf("tempDir does not exist: %s", tempDir)
}
// If package type is "browser", adjust installDir.
if pkgType == "browser" {
installDir = filepath.Join(installDir, "browser")
}
// Ensure destination exists.
if err := os.MkdirAll(installDir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create installDir: %w", err)
}
if runtime.GOOS == "windows" {
// Create a temporary log file.
logFile := filepath.Join(os.TempDir(), fmt.Sprintf("robocopy_%d.log", time.Now().UnixNano()))
// Print out the log file path so you can locate it manually.
fmt.Printf("[INFO] Robocopy log file: %s\n", logFile)
// Build robocopy command.
// /E: copy subdirectories (including empty ones)
// /TEE: output to console as well as to the log file.
// /LOG:<logFile>: write output to the log file.
// We remove extra suppression flags so that robocopy prints as much as possible.
cmd := exec.Command("robocopy", tempDir, installDir, "/E", "/TEE", fmt.Sprintf("/LOG:%s", logFile))
// Start robocopy.
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start robocopy: %w", err)
}
// Set up a goroutine to tail the log file.
doneTail := make(chan struct{})
var wg sync.WaitGroup
wg.Add(1)
go tailLogFile(logFile, doneTail, &wg)
// Wait for robocopy to complete.
err := cmd.Wait()
// Signal the tail goroutine to stop.
close(doneTail)
wg.Wait()
// Robocopy returns exit codes less than 8 as success.
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
if exitCode := exitErr.ExitCode(); exitCode >= 8 {
return fmt.Errorf("robocopy failed: exit status %d", exitCode)
}
} else {
return fmt.Errorf("robocopy failed: %w", err)
}
}
// Mark progress as complete.
UpdateProgress(100, "Copying files to install directory")
// (Optional) If you want the log file to be removed automatically, uncomment the next line.
// os.Remove(logFile)
} else {
// Non-Windows fallback: copy files one-by-one.
var totalFiles, copiedFiles int
err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
totalFiles++
}
return nil
})
if err != nil {
return err
}
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(tempDir, path)
if err != nil {
return err
}
targetPath := filepath.Join(installDir, relPath)
if info.IsDir() {
if err := os.MkdirAll(targetPath, os.ModePerm); err != nil {
return err
}
} else {
if err := copyFile(path, targetPath); err != nil {
return err
}
copiedFiles++
UpdateProgress(int(float64(copiedFiles)/float64(totalFiles)*100), "Copying files to install directory")
}
return nil
})
if err != nil {
return err
}
}
// Clean up temporary directory.
UpdateProgress(100, "Cleaning up temporary files")
return os.RemoveAll(tempDir)
}
// copyFile copies the contents of the source file to the destination file.
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
// Create the destination file
destinationFile, err := os.Create(dst)
if err != nil {
return err
}
defer destinationFile.Close()
// Copy the file content
if _, err := io.Copy(destinationFile, sourceFile); err != nil {
return err
}
// Preserve file permissions
info, err := sourceFile.Stat()
if err != nil {
return err
}
return os.Chmod(dst, info.Mode())
}
// finalizeInstall finalizes the installation by updating installed.ini.
func finalizeInstall(packageName, release, version, arch, osName string) error {
installDir, err := GetInstallDir()
if err != nil {
return err
}
pkgInfo := AppIndexEntry{
Name: packageName,
Version: version,
Release: release,
Arch: arch,
OS: osName,
}
return UpdateInstalledPackage(installDir, pkgInfo)
}