updated spm package
This commit is contained in:
parent
3f67a0a6de
commit
7373e47c1d
11 changed files with 594 additions and 475 deletions
|
@ -25,7 +25,7 @@ func InitBackground(width, height int) {
|
||||||
for i := range particles {
|
for i := range particles {
|
||||||
particles[i].Pos = rl.Vector2{X: float32(rng.Intn(width)), Y: float32(rng.Intn(height))}
|
particles[i].Pos = rl.Vector2{X: float32(rng.Intn(width)), Y: float32(rng.Intn(height))}
|
||||||
particles[i].Vel = rl.Vector2{X: (rng.Float32() - 0.5) * 0.2, Y: (rng.Float32() - 0.5) * 0.2}
|
particles[i].Vel = rl.Vector2{X: (rng.Float32() - 0.5) * 0.2, Y: (rng.Float32() - 0.5) * 0.2}
|
||||||
particles[i].Size = rng.Float32()*1.5 + 0.5 // Particles size ~0.5-2.0
|
particles[i].Size = rng.Float32()*1.5 + 0.5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
196
gui.go
196
gui.go
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -10,110 +11,117 @@ import (
|
||||||
|
|
||||||
// ShowUpdateWindow displays the update GUI.
|
// ShowUpdateWindow displays the update GUI.
|
||||||
func ShowUpdateWindow() {
|
func ShowUpdateWindow() {
|
||||||
screenW := 300
|
|
||||||
screenH := 450
|
|
||||||
rl.SetConfigFlags(rl.FlagWindowUndecorated)
|
|
||||||
rl.InitWindow(int32(screenW), int32(screenH), "Updating Spitfire")
|
|
||||||
|
|
||||||
// 1) Use actual monitor refresh rate
|
|
||||||
monitor := rl.GetCurrentMonitor()
|
|
||||||
refreshRate := rl.GetMonitorRefreshRate(monitor)
|
|
||||||
rl.SetTargetFPS(int32(refreshRate))
|
|
||||||
|
|
||||||
InitBackground(screenW, screenH)
|
|
||||||
|
|
||||||
logoPath := filepath.Join(getSpmDir(), "logo.png")
|
|
||||||
logo := rl.LoadTexture(logoPath)
|
|
||||||
defer rl.UnloadTexture(logo)
|
|
||||||
|
|
||||||
var displayed float32
|
|
||||||
scale := float32(0.5)
|
|
||||||
|
|
||||||
var sf StateFile
|
var sf StateFile
|
||||||
|
|
||||||
sf = ReadState()
|
sf = ReadState()
|
||||||
|
|
||||||
// 2) Update state asynchronously in a separate goroutine
|
if sf.IsUpdating == true {
|
||||||
go func() {
|
|
||||||
|
log.Println("Stage=Installing => show update window.")
|
||||||
|
|
||||||
|
screenW := 300
|
||||||
|
screenH := 450
|
||||||
|
rl.SetConfigFlags(rl.FlagWindowUndecorated)
|
||||||
|
rl.InitWindow(int32(screenW), int32(screenH), "Updating Spitfire")
|
||||||
|
|
||||||
|
monitor := rl.GetCurrentMonitor()
|
||||||
|
refreshRate := rl.GetMonitorRefreshRate(monitor)
|
||||||
|
rl.SetTargetFPS(int32(refreshRate))
|
||||||
|
|
||||||
|
InitBackground(screenW, screenH)
|
||||||
|
|
||||||
|
logoPath := filepath.Join(getSpmDir(), "logo.png")
|
||||||
|
logo := rl.LoadTexture(logoPath)
|
||||||
|
defer rl.UnloadTexture(logo)
|
||||||
|
|
||||||
|
var displayed float32
|
||||||
|
scale := float32(0.5)
|
||||||
|
|
||||||
|
// Update state asynchronously in a separate goroutine
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
sf = ReadState()
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
sf = ReadState()
|
tgt := float32(sf.Progress)
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
if rl.WindowShouldClose() {
|
||||||
tgt := float32(sf.Progress)
|
break
|
||||||
|
}
|
||||||
|
|
||||||
if rl.WindowShouldClose() {
|
// Smooth interpolation for displayed progress
|
||||||
break
|
displayed += (tgt - displayed) * 0.15
|
||||||
|
|
||||||
|
rl.BeginDrawing()
|
||||||
|
rl.ClearBackground(rl.Black)
|
||||||
|
|
||||||
|
// 2 px inside border for visibility
|
||||||
|
rl.DrawRectangleLinesEx(
|
||||||
|
rl.Rectangle{X: 1, Y: 1, Width: float32(screenW - 2), Height: float32(screenH - 2)},
|
||||||
|
2,
|
||||||
|
rl.White,
|
||||||
|
)
|
||||||
|
|
||||||
|
UpdateBackground(screenW, screenH)
|
||||||
|
DrawBackground(screenW, screenH)
|
||||||
|
|
||||||
|
// Draw logo scaled
|
||||||
|
lw := float32(logo.Width) * scale
|
||||||
|
lx := float32(screenW)/2 - lw/2
|
||||||
|
ly := float32(20)
|
||||||
|
rl.DrawTextureEx(logo, rl.Vector2{X: lx, Y: ly}, 0, scale, rl.White)
|
||||||
|
|
||||||
|
// Progress bar
|
||||||
|
barW := float32(screenW - 60)
|
||||||
|
barH := float32(20)
|
||||||
|
barX := float32(30)
|
||||||
|
barY := float32(screenH/2 + 40)
|
||||||
|
frac := displayed / 100.0
|
||||||
|
if frac < 0 {
|
||||||
|
frac = 0
|
||||||
|
} else if frac > 1 {
|
||||||
|
frac = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
fillRect := rl.Rectangle{X: barX, Y: barY, Width: barW * frac, Height: barH}
|
||||||
|
fullRect := rl.Rectangle{X: barX, Y: barY, Width: barW, Height: barH}
|
||||||
|
corner := float32(0.4)
|
||||||
|
|
||||||
|
rl.DrawRectangleRounded(fillRect, corner, 6, rl.RayWhite)
|
||||||
|
rl.DrawRectangleRoundedLines(fullRect, corner, 6, rl.White)
|
||||||
|
|
||||||
|
// Display status text
|
||||||
|
msg := updateStatusMsg
|
||||||
|
if !sf.IsUpdating {
|
||||||
|
msg = "Update complete!"
|
||||||
|
}
|
||||||
|
fontSize := int32(20)
|
||||||
|
txtW := rl.MeasureText(msg, fontSize)
|
||||||
|
txtX := (int32(screenW) - txtW) / 2
|
||||||
|
txtY := int32(barY) - 30
|
||||||
|
rl.DrawText(msg, txtX, txtY, fontSize, rl.White)
|
||||||
|
|
||||||
|
// Display numeric progress
|
||||||
|
progStr := fmt.Sprintf("%.0f%%", displayed)
|
||||||
|
pw := rl.MeasureText(progStr, 20)
|
||||||
|
px := (int32(screenW) - pw) / 2
|
||||||
|
py := int32(barY) + 30
|
||||||
|
rl.DrawText(progStr, px, py, 20, rl.White)
|
||||||
|
|
||||||
|
rl.EndDrawing()
|
||||||
|
|
||||||
|
// If no longer updating => break window loop
|
||||||
|
if !sf.IsUpdating {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Smooth interpolation for displayed progress
|
rl.CloseWindow()
|
||||||
displayed += (tgt - displayed) * 0.15
|
|
||||||
|
|
||||||
rl.BeginDrawing()
|
|
||||||
rl.ClearBackground(rl.Black)
|
|
||||||
|
|
||||||
// 2 px inside border for visibility
|
|
||||||
rl.DrawRectangleLinesEx(
|
|
||||||
rl.Rectangle{X: 1, Y: 1, Width: float32(screenW - 2), Height: float32(screenH - 2)},
|
|
||||||
2,
|
|
||||||
rl.White,
|
|
||||||
)
|
|
||||||
|
|
||||||
UpdateBackground(screenW, screenH)
|
|
||||||
DrawBackground(screenW, screenH)
|
|
||||||
|
|
||||||
// Draw logo scaled
|
|
||||||
lw := float32(logo.Width) * scale
|
|
||||||
lx := float32(screenW)/2 - lw/2
|
|
||||||
ly := float32(20)
|
|
||||||
rl.DrawTextureEx(logo, rl.Vector2{X: lx, Y: ly}, 0, scale, rl.White)
|
|
||||||
|
|
||||||
// Progress bar
|
|
||||||
barW := float32(screenW - 60)
|
|
||||||
barH := float32(20)
|
|
||||||
barX := float32(30)
|
|
||||||
barY := float32(screenH/2 + 40)
|
|
||||||
frac := displayed / 100.0
|
|
||||||
if frac < 0 {
|
|
||||||
frac = 0
|
|
||||||
} else if frac > 1 {
|
|
||||||
frac = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
fillRect := rl.Rectangle{X: barX, Y: barY, Width: barW * frac, Height: barH}
|
|
||||||
fullRect := rl.Rectangle{X: barX, Y: barY, Width: barW, Height: barH}
|
|
||||||
corner := float32(0.4)
|
|
||||||
|
|
||||||
rl.DrawRectangleRounded(fillRect, corner, 6, rl.RayWhite)
|
|
||||||
rl.DrawRectangleRoundedLines(fullRect, corner, 6, rl.White)
|
|
||||||
|
|
||||||
// Display status text
|
|
||||||
msg := updateStatusMsg
|
|
||||||
if !sf.IsUpdating {
|
|
||||||
msg = "Update complete!"
|
|
||||||
}
|
|
||||||
fontSize := int32(20)
|
|
||||||
txtW := rl.MeasureText(msg, fontSize)
|
|
||||||
txtX := (int32(screenW) - txtW) / 2
|
|
||||||
txtY := int32(barY) - 30
|
|
||||||
rl.DrawText(msg, txtX, txtY, fontSize, rl.White)
|
|
||||||
|
|
||||||
// Display numeric progress
|
|
||||||
progStr := fmt.Sprintf("%.0f%%", displayed)
|
|
||||||
pw := rl.MeasureText(progStr, 20)
|
|
||||||
px := (int32(screenW) - pw) / 2
|
|
||||||
py := int32(barY) + 30
|
|
||||||
rl.DrawText(progStr, px, py, 20, rl.White)
|
|
||||||
|
|
||||||
rl.EndDrawing()
|
|
||||||
|
|
||||||
// If no longer updating => break window loop
|
|
||||||
if !sf.IsUpdating {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rl.CloseWindow()
|
LaunchBrowser()
|
||||||
}
|
}
|
||||||
|
|
16
main.go
16
main.go
|
@ -26,7 +26,7 @@ var (
|
||||||
isBackgroundMode bool
|
isBackgroundMode bool
|
||||||
)
|
)
|
||||||
|
|
||||||
// StateFile holds the update state from INI
|
// StateFile holds the update state from our INI
|
||||||
type StateFile struct {
|
type StateFile struct {
|
||||||
IsUpdating bool
|
IsUpdating bool
|
||||||
Progress int32
|
Progress int32
|
||||||
|
@ -199,8 +199,8 @@ func runBackgroundUpdater() {
|
||||||
// Write the state to the update_state.ini file
|
// Write the state to the update_state.ini file
|
||||||
WriteState(isUpdating, int32(progress), task)
|
WriteState(isUpdating, int32(progress), task)
|
||||||
|
|
||||||
// Sleep for 1 second before updating the progress again
|
// Sleep for 1 hour before updating the progress again
|
||||||
time.Sleep(1 * time.Second)
|
time.Sleep(1 * time.Hour)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -249,15 +249,9 @@ func main() {
|
||||||
isBackgroundMode = true
|
isBackgroundMode = true
|
||||||
runBackgroundUpdater()
|
runBackgroundUpdater()
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Launcher started (foreground mode).")
|
|
||||||
sf := ReadState()
|
|
||||||
if sf.IsUpdating == true {
|
|
||||||
log.Println("Stage=Installing => show update window.")
|
|
||||||
ShowUpdateWindow()
|
|
||||||
} else {
|
} else {
|
||||||
LaunchBrowser()
|
log.Println("Launcher started (foreground mode).")
|
||||||
|
ShowUpdateWindow()
|
||||||
}
|
}
|
||||||
fmt.Println("Exiting launcher.")
|
fmt.Println("Exiting launcher.")
|
||||||
}
|
}
|
||||||
|
|
112
spm/auto.go
112
spm/auto.go
|
@ -7,12 +7,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// pendingUpdates holds info about packages that have been downloaded/decompressed
|
// 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
|
var pendingUpdates []AppIndexEntry
|
||||||
|
|
||||||
// AutoDownloadUpdates downloads the APPINDEX file, parses it, compares against
|
// AutoDownloadUpdates downloads the APPINDEX file, parses it, compares against
|
||||||
// currently installed packages, and if it finds a newer version, downloads
|
// 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 {
|
func AutoDownloadUpdates() error {
|
||||||
// 1) Download the APPINDEX file to a temporary location
|
// 1) Download the APPINDEX file to a temporary location
|
||||||
appIndexPath := filepath.Join(os.TempDir(), "APPINDEX")
|
appIndexPath := filepath.Join(os.TempDir(), "APPINDEX")
|
||||||
|
@ -115,10 +115,10 @@ func AutoDownloadUpdates() error {
|
||||||
return nil
|
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.
|
// It moves files from their temp directories to the final location and updates installed.ini.
|
||||||
func AutoInstallUpdates() error {
|
func AutoInstallUpdates() error {
|
||||||
installDir, err := GetDefaultInstallDir()
|
installDir, err := GetInstallDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -155,3 +155,107 @@ func AutoInstallUpdates() error {
|
||||||
pendingUpdates = nil
|
pendingUpdates = nil
|
||||||
return 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
90
spm/decompress.go
Normal 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
144
spm/dirs.go
Normal 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
|
||||||
|
}
|
|
@ -10,98 +10,6 @@ import (
|
||||||
"time"
|
"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.
|
// DownloadPackageFromAppIndex selects and downloads the correct package from the APPINDEX.
|
||||||
func DownloadPackageFromAppIndex(appIndexPath string, packageName string, release string, pkgType string, destDir string) error {
|
func DownloadPackageFromAppIndex(appIndexPath string, packageName string, release string, pkgType string, destDir string) error {
|
||||||
// Parse the APPINDEX
|
// 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
|
// Check if the package is already installed and up-to-date
|
||||||
installDir, err := GetDefaultInstallDir()
|
installDir, err := GetInstallDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get install directory: %w", err)
|
return fmt.Errorf("failed to get install directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
180
spm/install.go
180
spm/install.go
|
@ -2,12 +2,18 @@ package spm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
|
"bufio"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DecompressToTemp(filePath string) (string, error) {
|
func DecompressToTemp(filePath string) (string, error) {
|
||||||
|
@ -96,65 +102,159 @@ func DecompressToTemp(filePath string) (string, error) {
|
||||||
return decompressDir, nil
|
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 {
|
func MoveFilesToInstallDir(tempDir, installDir, pkgType string) error {
|
||||||
// Ensure tempDir exists before processing
|
// Ensure tempDir exists.
|
||||||
if _, err := os.Stat(tempDir); os.IsNotExist(err) {
|
if _, err := os.Stat(tempDir); os.IsNotExist(err) {
|
||||||
return fmt.Errorf("tempDir does not exist: %s", tempDir)
|
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" {
|
if pkgType == "browser" {
|
||||||
installDir = filepath.Join(installDir, "browser")
|
installDir = filepath.Join(installDir, "browser")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count total files to copy
|
// Ensure destination exists.
|
||||||
var totalFiles, copiedFiles int
|
if err := os.MkdirAll(installDir, os.ModePerm); err != nil {
|
||||||
err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
return fmt.Errorf("failed to create installDir: %w", err)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !info.IsDir() {
|
|
||||||
totalFiles++
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy files and track progress
|
if runtime.GOOS == "windows" {
|
||||||
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
// 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 {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
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")
|
UpdateProgress(100, "Cleaning up temporary files")
|
||||||
return os.RemoveAll(tempDir)
|
return os.RemoveAll(tempDir)
|
||||||
}
|
}
|
||||||
|
@ -189,7 +289,7 @@ func copyFile(src, dst string) error {
|
||||||
|
|
||||||
// FinalizeInstall finalizes the installation by updating installed.ini.
|
// FinalizeInstall finalizes the installation by updating installed.ini.
|
||||||
func FinalizeInstall(packageName, release, version, arch, osName string) error {
|
func FinalizeInstall(packageName, release, version, arch, osName string) error {
|
||||||
installDir, err := GetDefaultInstallDir()
|
installDir, err := GetInstallDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ func Run() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(exePath)
|
cmd := exec.Command(exePath)
|
||||||
|
cmd.Dir = filepath.Join(installDir, "browser")
|
||||||
return cmd.Start()
|
return cmd.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
202
spm/utils.go
202
spm/utils.go
|
@ -1,84 +1,12 @@
|
||||||
package spm
|
package spm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
|
||||||
"compress/gzip"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
"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.
|
// 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 {
|
func persistSystemEnvVar(key, value string) error {
|
||||||
cmd := exec.Command("cmd", "/C", "setx", key, value)
|
cmd := exec.Command("cmd", "/C", "setx", key, value)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
@ -88,52 +16,6 @@ func persistSystemEnvVar(key, value string) error {
|
||||||
return nil
|
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
|
// IsMatchingEntry checks if a package entry matches the requested specs
|
||||||
func IsMatchingEntry(e AppIndexEntry, name, release, arch, osName, pkgType string) bool {
|
func IsMatchingEntry(e AppIndexEntry, name, release, arch, osName, pkgType string) bool {
|
||||||
return e.Name == name &&
|
return e.Name == name &&
|
||||||
|
@ -142,83 +24,3 @@ func IsMatchingEntry(e AppIndexEntry, name, release, arch, osName, pkgType strin
|
||||||
e.OS == osName &&
|
e.OS == osName &&
|
||||||
e.Type == pkgType
|
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
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue