updated spm package

This commit is contained in:
partisan 2025-02-04 17:11:27 +01:00
parent 3f67a0a6de
commit 7373e47c1d
11 changed files with 594 additions and 475 deletions

View file

@ -7,12 +7,12 @@ import (
)
// pendingUpdates holds info about packages that have been downloaded/decompressed
// but not yet moved to the final install location.
// but not yet moved to the final install location, cuz Windows has this stupid file locking mechanism
var pendingUpdates []AppIndexEntry
// AutoDownloadUpdates downloads the APPINDEX file, parses it, compares against
// currently installed packages, and if it finds a newer version, downloads
// and decompresses it into a temporary folder. The result is stored in pendingUpdates.
// and decompresses it into a temporary folder. The result is stored in pendingUpdates, so it can be used by AutoInstallUpdates().
func AutoDownloadUpdates() error {
// 1) Download the APPINDEX file to a temporary location
appIndexPath := filepath.Join(os.TempDir(), "APPINDEX")
@ -115,10 +115,10 @@ func AutoDownloadUpdates() error {
return nil
}
// AutoInstallUpdates installs any packages that were decompressed by AutoDownloadUpdates.
// AutoInstallUpdates installs any packages that were downloaded and decompressed by AutoDownloadUpdates.
// It moves files from their temp directories to the final location and updates installed.ini.
func AutoInstallUpdates() error {
installDir, err := GetDefaultInstallDir()
installDir, err := GetInstallDir()
if err != nil {
return err
}
@ -155,3 +155,107 @@ func AutoInstallUpdates() error {
pendingUpdates = nil
return nil
}
func AutoDownloadSpecified(specs []AppIndexEntry) error {
// 1) Download the APPINDEX file to a temporary location
appIndexPath := filepath.Join(os.TempDir(), "APPINDEX")
fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath)
if err := DownloadAppIndex(appIndexPath); err != nil {
return fmt.Errorf("[ERROR] Failed to download APPINDEX: %w", err)
}
fmt.Println("[INFO] APPINDEX downloaded successfully")
// 2) Parse the APPINDEX file
fmt.Println("[INFO] Parsing APPINDEX file:", appIndexPath)
entries, err := ParseAppIndex(appIndexPath)
if err != nil {
return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err)
}
fmt.Printf("[INFO] Parsed APPINDEX successfully, found %d entries\n", len(entries))
// 3) Get install directory to check for updates
installDir, err := GetInstallDir()
if err != nil {
return err
}
fmt.Println("[INFO] Install directory:", installDir)
// 4) For each item in the passed specs, attempt to download if update is needed
for _, spec := range specs {
fmt.Printf("[INFO] Checking requested package: %+v\n", spec)
// Find matching entry from the parsed APPINDEX
var matchingEntry *AppIndexEntry
for _, e := range entries {
if e.Name == spec.Name &&
e.Release == spec.Release &&
e.Type == spec.Type &&
e.OS == spec.OS &&
e.Arch == spec.Arch {
matchingEntry = &e
break
}
}
if matchingEntry == nil {
fmt.Printf("[WARN] No matching APPINDEX entry found for package: %s (%s)\n", spec.Name, spec.Release)
continue
}
fmt.Printf("[INFO] Found matching APPINDEX entry: %+v\n", *matchingEntry)
// // Check if an update is needed
// updateNeeded, err := IsUpdateNeeded(
// installDir,
// matchingEntry.Name,
// matchingEntry.Release,
// matchingEntry.Version,
// matchingEntry.Arch,
// matchingEntry.OS,
// )
// if err != nil {
// return fmt.Errorf("[ERROR] Failed to check if update is needed for %s: %w", matchingEntry.Name, err)
// }
// if !updateNeeded {
// fmt.Printf("[INFO] No update needed for package '%s'\n", matchingEntry.Name)
// continue
// }
// 5) Download the package
downloadDir := GetTempDir()
fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n", matchingEntry.Name, downloadDir)
if err := DownloadPackageFromAppIndex(
appIndexPath,
matchingEntry.Name,
matchingEntry.Release,
matchingEntry.Type,
downloadDir,
); err != nil {
return fmt.Errorf("[ERROR] Failed to download package '%s': %w", matchingEntry.Name, err)
}
fmt.Printf("[INFO] Package '%s' downloaded successfully to: %s\n", matchingEntry.Name, downloadDir)
// 6) Decompress the package
fmt.Printf("[INFO] Decompressing package '%s'\n", matchingEntry.Name)
tempDir, err := DecompressPackage(
downloadDir,
matchingEntry.Name,
matchingEntry.Arch,
matchingEntry.OS,
matchingEntry.Type,
matchingEntry.Release,
matchingEntry.Version,
)
if err != nil {
return fmt.Errorf("[ERROR] Failed to decompress package '%s': %w", matchingEntry.Name, err)
}
fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir)
// 7) Store in pendingUpdates for AutoInstallUpdates
fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name)
pendingUpdates = append(pendingUpdates, *matchingEntry)
}
fmt.Println("[INFO] AutoDownloadSpecifiedPackages completed successfully")
return nil
}

90
spm/decompress.go Normal file
View file

@ -0,0 +1,90 @@
package spm
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// DecompressPackage determines the package format and decompresses it
func DecompressPackage(downloadDir, packageName, arch, osName, pkgType, release, version string) (string, error) {
// 1) Construct the .tar.gz name
expectedFileName := fmt.Sprintf(
"%s@%s@%s@%s@%s@%s.tar.gz",
packageName, arch, osName, pkgType, release, version,
)
packagePath := filepath.Join(downloadDir, expectedFileName)
// Check that file exists
if _, err := os.Stat(packagePath); os.IsNotExist(err) {
return "", fmt.Errorf("package file not found: %s", packagePath)
}
// 2) Build the folder path (minus ".tar.gz")
folderName := strings.TrimSuffix(expectedFileName, ".tar.gz")
tempDir := GetTempDir() // e.g. C:\Users\<User>\AppData\Local\Temp\spm_temp_164326
decompressDir := filepath.Join(tempDir, folderName)
// Ensure the folder
if err := os.MkdirAll(decompressDir, 0755); err != nil {
return "", fmt.Errorf("failed to create decompressDir: %w", err)
}
// 3) Decompress everything into `decompressDir`
if err := decompressTarGz(packagePath, decompressDir); err != nil {
return "", fmt.Errorf("failed to decompress: %w", err)
}
// Return the folder path we used
return decompressDir, nil
}
func decompressTarGz(srcFile, destDir string) error {
f, err := os.Open(srcFile)
if err != nil {
return err
}
defer f.Close()
gzr, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gzr.Close()
tarReader := tar.NewReader(gzr)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
outPath := filepath.Join(destDir, header.Name)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(outPath, os.FileMode(header.Mode)); err != nil {
return err
}
case tar.TypeReg:
outFile, err := os.Create(outPath)
if err != nil {
return err
}
_, err = io.Copy(outFile, tarReader)
outFile.Close()
if err != nil {
return err
}
default:
// huh
}
}
return nil
}

144
spm/dirs.go Normal file
View file

@ -0,0 +1,144 @@
package spm
import (
"fmt"
"math/rand"
"os"
"path/filepath"
"runtime"
"sync"
)
// global sync and variable for generated temp dir
var (
tempDirOnce sync.Once
tempDirPath string
)
// global variables for install dir
var (
installMu sync.Mutex
installedDir string
installEnvVar = "SPITFIRE_INSTALL_DIR"
)
// GetTempDir generates or retrieves a unique temp dir.
func GetTempDir() string {
tempDirOnce.Do(func() {
// Generate a unique temp dir name
tempDirPath = filepath.Join(os.TempDir(), fmt.Sprintf("spm_temp_%d", rand.Intn(1000000)))
// Ensure the dir exists
if err := os.MkdirAll(tempDirPath, os.ModePerm); err != nil {
fmt.Printf("[ERROR] Failed to create temp directory: %v\n", err)
} else {
fmt.Printf("[INFO] Using temp directory: %s\n", tempDirPath)
}
})
return tempDirPath
}
// GetDefaultInstallDir generates the default installation dir
// based on the OS and environment, then also sets it via SetInstallDir.
//
// Please use GetInstallDir() instead of GetDefaultInstallDir() when interacting with spm.
func GetDefaultInstallDir() (string, error) {
var installDir string
switch runtime.GOOS {
case "windows":
// Use C:\Program Files
programFiles := os.Getenv("ProgramFiles")
if programFiles == "" {
return "", fmt.Errorf("unable to determine default install directory on Windows")
}
installDir = filepath.Join(programFiles, "Spitfire")
case "darwin":
// Use ~/Library/Application Support on macOS
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("unable to determine home directory on macOS: %w", err)
}
installDir = filepath.Join(homeDir, "Library", "Application Support", "Spitfire")
case "linux":
// Use ~/.local/share/Spitfire on Linux
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("unable to determine home directory on Linux: %w", err)
}
installDir = filepath.Join(homeDir, ".local", "share", "Spitfire")
default:
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
}
// Also store it globally so future calls to GetInstallDir() return the same
SetInstallDir(installDir)
return installDir, nil
}
// SetDownloadFolder ensures customDir exists, returns it
func SetDownloadFolder(customDir string) (string, error) {
if err := os.MkdirAll(customDir, os.ModePerm); err != nil {
return "", err
}
return customDir, nil
}
// SetInstallDir sets the global install dir variable and updates the persistent environment variable.
func SetInstallDir(path string) error {
installMu.Lock()
defer installMu.Unlock()
installedDir = path
// Persist the environment variable on Windows
if runtime.GOOS == "windows" {
err := persistSystemEnvVar(installEnvVar, path)
if err != nil {
return err
}
} else {
// For non-Windows platforms, just set it in the current process environment
err := os.Setenv(installEnvVar, path)
if err != nil {
return err
}
}
return nil
}
// GetInstallDir returns the currently set install dir if available.
// Otherwise, it calls GetDefaultInstallDir() and sets that.
func GetInstallDir() (string, error) {
// If already set, return it
if installedDir != "" {
return installedDir, nil
}
// Check if it's stored in the system environment variable
if envDir := os.Getenv(installEnvVar); envDir != "" {
installedDir = envDir
return installedDir, nil
}
// Compute and store the default dir if not already set
defDir, err := GetDefaultInstallDir()
if err != nil {
return "", err
}
installedDir = defDir
// Persist the default dir as an environment variable on Windows
if runtime.GOOS == "windows" {
_ = persistSystemEnvVar(installEnvVar, defDir)
} else {
_ = os.Setenv(installEnvVar, defDir)
}
return defDir, nil
}

View file

@ -10,98 +10,6 @@ import (
"time"
)
// func DownloadPackage(pkg Package, destDir string, version, release, pkgType string) error {
// client := &http.Client{}
// var resp *http.Response
// var err error
// for i := 0; i < 3; i++ { // Retry up to 3 times
// fmt.Printf("[INFO] Attempting to download package from URL: %s (Attempt %d)\n", pkg.DownloadURL, i+1)
// resp, err = client.Get(pkg.DownloadURL)
// if err == nil && resp.StatusCode == http.StatusOK {
// break
// }
// if err != nil {
// fmt.Printf("[ERROR] Attempt %d failed: %v\n", i+1, err)
// }
// if resp != nil && resp.StatusCode != http.StatusOK {
// fmt.Printf("[ERROR] Server returned status: %d\n", resp.StatusCode)
// }
// if i < 2 {
// time.Sleep(2 * time.Second) // Delay between retries
// }
// }
// if err != nil {
// return fmt.Errorf("[ERROR] Failed to download %s after 3 retries: %w", pkg.Name, err)
// }
// defer resp.Body.Close()
// // Check content type
// contentType := resp.Header.Get("Content-Type")
// if contentType != "application/gzip" && contentType != "application/x-tar" {
// return fmt.Errorf("[ERROR] Invalid content type: %s. Expected a .tar.gz file.", contentType)
// }
// // Generate the filename using the desired format
// filename := fmt.Sprintf("%s@%s@%s@%s@%s@%s.tar.gz",
// pkg.Name, // Name of the package
// pkg.Arch, // Architecture (e.g., amd64)
// pkg.OS, // Operating System (e.g., windows, linux)
// pkgType, // Type of the package (e.g., nightly, stable)
// release, // Release (e.g., nightly, stable)
// version, // Version of the package
// )
// // Construct the full file path
// filePath := filepath.Join(destDir, filename)
// fmt.Printf("[INFO] Saving package to: %s\n", filePath)
// // Create the destination directory if it doesn't exist
// err = os.MkdirAll(destDir, 0755)
// if err != nil {
// return fmt.Errorf("[ERROR] Failed to create destination directory %s: %w", destDir, err)
// }
// // Create the file to save the download
// out, err := os.Create(filePath)
// if err != nil {
// return fmt.Errorf("[ERROR] Failed to create file %s: %w", filePath, err)
// }
// defer out.Close()
// // Track download progress
// totalSize := resp.ContentLength
// var downloaded int64
// buf := make([]byte, 1024)
// for {
// n, err := resp.Body.Read(buf)
// if n > 0 {
// downloaded += int64(n)
// percentage := int(float64(downloaded) / float64(totalSize) * 100)
// UpdateProgress(percentage, fmt.Sprintf("Downloading %s", pkg.Name))
// if _, err := out.Write(buf[:n]); err != nil {
// return fmt.Errorf("[ERROR] Failed to write to file %s: %w", filePath, err)
// }
// }
// if err == io.EOF {
// break
// }
// if err != nil {
// return fmt.Errorf("[ERROR] Error reading response body: %w", err)
// }
// }
// UpdateProgress(100, fmt.Sprintf("%s downloaded", pkg.Name))
// fmt.Printf("[INFO] Package %s downloaded successfully to: %s\n", pkg.Name, filePath)
// // Validate that the file is a valid gzip or tar file
// if _, err := os.Stat(filePath); err != nil {
// return fmt.Errorf("[ERROR] Downloaded file does not exist: %w", err)
// }
// return nil
// }
// DownloadPackageFromAppIndex selects and downloads the correct package from the APPINDEX.
func DownloadPackageFromAppIndex(appIndexPath string, packageName string, release string, pkgType string, destDir string) error {
// Parse the APPINDEX
@ -129,7 +37,7 @@ func DownloadPackageFromAppIndex(appIndexPath string, packageName string, releas
}
// Check if the package is already installed and up-to-date
installDir, err := GetDefaultInstallDir()
installDir, err := GetInstallDir()
if err != nil {
return fmt.Errorf("failed to get install directory: %w", err)
}

View file

@ -2,12 +2,18 @@ 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) {
@ -96,65 +102,159 @@ func DecompressToTemp(filePath string) (string, error) {
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 before processing
// Ensure tempDir exists.
if _, err := os.Stat(tempDir); os.IsNotExist(err) {
return fmt.Errorf("tempDir does not exist: %s", tempDir)
}
// If the package type is "browser", set the subdirectory to "browser"
// If package type is "browser", adjust installDir.
if pkgType == "browser" {
installDir = filepath.Join(installDir, "browser")
}
// Count total files to copy
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
// Ensure destination exists.
if err := os.MkdirAll(installDir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create installDir: %w", err)
}
// Copy files and track progress
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
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
}
relPath, err := filepath.Rel(tempDir, path)
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
}
targetPath := filepath.Join(installDir, relPath)
if info.IsDir() {
// Create directories in the install directory
if err := os.MkdirAll(targetPath, os.ModePerm); err != nil {
return err
}
} else {
// Copy files to the install directory
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
// Clean up temporary directory.
UpdateProgress(100, "Cleaning up temporary files")
return os.RemoveAll(tempDir)
}
@ -189,7 +289,7 @@ func copyFile(src, dst string) error {
// FinalizeInstall finalizes the installation by updating installed.ini.
func FinalizeInstall(packageName, release, version, arch, osName string) error {
installDir, err := GetDefaultInstallDir()
installDir, err := GetInstallDir()
if err != nil {
return err
}

View file

@ -21,6 +21,7 @@ func Run() error {
}
cmd := exec.Command(exePath)
cmd.Dir = filepath.Join(installDir, "browser")
return cmd.Start()
}

View file

@ -1,32 +0,0 @@
package spm
import (
"fmt"
"math/rand"
"os"
"path/filepath"
"sync"
"time"
)
var (
tempDirOnce sync.Once
tempDirPath string
)
// GetTempDir generates or retrieves a unique temp directory.
func GetTempDir() string {
tempDirOnce.Do(func() {
// Generate a unique temp directory name
rand.Seed(time.Now().UnixNano())
tempDirPath = filepath.Join(os.TempDir(), fmt.Sprintf("spm_temp_%d", rand.Intn(1000000)))
// Ensure the directory exists
if err := os.MkdirAll(tempDirPath, os.ModePerm); err != nil {
fmt.Printf("[ERROR] Failed to create temp directory: %v\n", err)
} else {
fmt.Printf("[INFO] Using temp directory: %s\n", tempDirPath)
}
})
return tempDirPath
}

View file

@ -1,84 +1,12 @@
package spm
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
)
// global mutable variable to store the chosen install directory
var (
mu sync.Mutex
installedDir string
envVar = "SPITFIRE_INSTALL_DIR" // Environment variable name
)
// SetInstallDir sets the global install directory variable and updates the persistent environment variable.
func SetInstallDir(path string) error {
mu.Lock()
defer mu.Unlock()
installedDir = path
// Persist the environment variable on Windows
if runtime.GOOS == "windows" {
err := persistSystemEnvVar(envVar, path)
if err != nil {
return err
}
} else {
// For non-Windows platforms, just set it in the current process environment
err := os.Setenv(envVar, path)
if err != nil {
return err
}
}
return nil
}
// GetInstallDir returns the currently set install directory if available;
// otherwise, it calls GetDefaultInstallDir() and sets that.
func GetInstallDir() (string, error) {
mu.Lock()
defer mu.Unlock()
// If already set, return it
if installedDir != "" {
return installedDir, nil
}
// Check if it's stored in the system environment variable
if envDir := os.Getenv(envVar); envDir != "" {
installedDir = envDir
return installedDir, nil
}
// Compute and store the default directory if not already set
defDir, err := GetDefaultInstallDir()
if err != nil {
return "", err
}
installedDir = defDir
// Persist the default directory as an environment variable on Windows
if runtime.GOOS == "windows" {
_ = persistSystemEnvVar(envVar, defDir)
} else {
_ = os.Setenv(envVar, defDir)
}
return defDir, nil
}
// persistSystemEnvVar sets a persistent environment variable on Windows using the `setx` command.
// Perhaps support for other systems would be needed, but all of this "Launcher" thingy is probably going to end up
// being Windows-specific, as other superior systems have their own package managers.
func persistSystemEnvVar(key, value string) error {
cmd := exec.Command("cmd", "/C", "setx", key, value)
err := cmd.Run()
@ -88,52 +16,6 @@ func persistSystemEnvVar(key, value string) error {
return nil
}
// GetDefaultInstallDir generates the default installation directory
// based on the OS and environment, then also sets it via SetInstallDir.
func GetDefaultInstallDir() (string, error) {
var installDir string
switch runtime.GOOS {
case "windows":
programFiles := os.Getenv("ProgramFiles")
if programFiles == "" {
return "", fmt.Errorf("unable to determine default install directory on Windows")
}
installDir = filepath.Join(programFiles, "Spitfire")
case "darwin":
// Use ~/Library/Application Support on macOS
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("unable to determine home directory on macOS: %w", err)
}
installDir = filepath.Join(homeDir, "Library", "Application Support", "Spitfire")
case "linux":
// Use ~/.local/share/Spitfire on Linux
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("unable to determine home directory on Linux: %w", err)
}
installDir = filepath.Join(homeDir, ".local", "share", "Spitfire")
default:
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
}
// Also store it globally so future calls to GetInstallDir() return the same
SetInstallDir(installDir)
return installDir, nil
}
// SetDownloadFolder ensures customDir exists, returns it
func SetDownloadFolder(customDir string) (string, error) {
if err := os.MkdirAll(customDir, os.ModePerm); err != nil {
return "", err
}
return customDir, nil
}
// IsMatchingEntry checks if a package entry matches the requested specs
func IsMatchingEntry(e AppIndexEntry, name, release, arch, osName, pkgType string) bool {
return e.Name == name &&
@ -142,83 +24,3 @@ func IsMatchingEntry(e AppIndexEntry, name, release, arch, osName, pkgType strin
e.OS == osName &&
e.Type == pkgType
}
// DecompressPackage determines the package format and decompresses it
// DecompressPackage: uses a consistent folder name based on "expectedFileName".
func DecompressPackage(downloadDir, packageName, arch, osName, pkgType, release, version string) (string, error) {
// 1) Construct the .tar.gz name
expectedFileName := fmt.Sprintf(
"%s@%s@%s@%s@%s@%s.tar.gz",
packageName, arch, osName, pkgType, release, version,
)
packagePath := filepath.Join(downloadDir, expectedFileName)
// Check that file exists
if _, err := os.Stat(packagePath); os.IsNotExist(err) {
return "", fmt.Errorf("package file not found: %s", packagePath)
}
// 2) Build the folder path (minus ".tar.gz")
folderName := strings.TrimSuffix(expectedFileName, ".tar.gz")
tempDir := GetTempDir() // e.g. C:\Users\<User>\AppData\Local\Temp\spm_temp_164326
decompressDir := filepath.Join(tempDir, folderName)
// Ensure the folder
if err := os.MkdirAll(decompressDir, 0755); err != nil {
return "", fmt.Errorf("failed to create decompressDir: %w", err)
}
// 3) Decompress everything into `decompressDir`
if err := decompressTarGz(packagePath, decompressDir); err != nil {
return "", fmt.Errorf("failed to decompress: %w", err)
}
// Return the folder path we used
return decompressDir, nil
}
func decompressTarGz(srcFile, destDir string) error {
f, err := os.Open(srcFile)
if err != nil {
return err
}
defer f.Close()
gzr, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gzr.Close()
tarReader := tar.NewReader(gzr)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
outPath := filepath.Join(destDir, header.Name)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(outPath, os.FileMode(header.Mode)); err != nil {
return err
}
case tar.TypeReg:
outFile, err := os.Create(outPath)
if err != nil {
return err
}
_, err = io.Copy(outFile, tarReader)
outFile.Close()
if err != nil {
return err
}
default:
// handle symlinks etc. if needed
}
}
return nil
}