This commit is contained in:
partisan 2025-03-01 17:59:26 +01:00
parent 66f4191c5f
commit 44d20eb983
10 changed files with 319 additions and 72 deletions

View file

@ -47,9 +47,9 @@ func main() {
}
// -- Download --
// spm.AutoDownloadSpecified(specs) downloads specified packages to temp dir and decompresses them, making them ready for install by running "spm.AutoInstallUpdates()".
// spm.DownloadSpecified(specs) downloads specified packages to temp dir and decompresses them, making them ready for install by running "spm.InstallUpdates()".
fmt.Println("Starting download and decompression...")
if err := spm.AutoDownloadSpecified(specs); err != nil {
if err := spm.DownloadSpecified(specs); err != nil {
fmt.Println("Error downloading packages:", err)
return
}
@ -57,15 +57,15 @@ func main() {
fmt.Println("Download complete. Proceeding with installation...")
// -- Install --
// Install and Download are separate as you cannot replace running binaries on Windows. So the final move to the correct folder is done by "spm.AutoInstallUpdates()".
if err := spm.AutoInstallUpdates(); err != nil {
// Install and Download are separate as you cannot replace running binaries on Windows. So the final move to the correct folder is done by "spm.InstallUpdates()".
if err := spm.InstallUpdates(); err != nil {
fmt.Println("Error during installation:", err)
return
}
// -- Register --
// spm.RegisterApp() is primarily used to modify the Windows registry so it recognizes Spitfire Browser as an installed program.
// You shouldnt need to run it more than once during installation. Also this function requires administrative privileges on Windows to work correctly.
// spm.RegisterApp() is primarily used to modify the Windows registry so it recognizes Spitfire Browser as an installed program.
// You shouldnt need to run it more than once during installation. Also this function requires administrative privileges on Windows to work correctly.
if err := spm.RegisterApp(); err != nil {
fmt.Println("Error registering app:", err)
return

View file

@ -6,14 +6,46 @@ import (
"io"
"net/http"
"os"
"path/filepath"
"sort"
"strings"
"gopkg.in/ini.v1"
)
const appIndexURL = "https://downloads.sourceforge.net/project/spitfire-browser/APPINDEX"
// AppIndexEntry represents a single entry in an app index.
type AppIndexEntry struct {
Name string
Version string
Release string // "nightly" / "stable" / etc.
Arch string // e.g. "amd64", "386"
OS string // e.g. "windows", "linux"
Type string // "browser", "addon", "theme", etc.
DownloadURL string
}
func DownloadAppIndex(dest string) error {
// RemoteIndex represents a remote APPINDEX repository.
type RemoteIndex struct {
Name string `json:"name"`
Link string `json:"link"`
}
var (
// defaultRemoteIndexes holds the default remote index.
defaultRemoteIndexes = []RemoteIndex{
{
Name: "default",
Link: "https://downloads.sourceforge.net/project/spitfire-browser/APPINDEX",
},
}
// remoteIndexes holds the current remote indexes in use.
remoteIndexes = defaultRemoteIndexes
)
// downloadAppIndex downloads an APPINDEX from the given URL and writes it to dest.
func downloadAppIndex(url, dest string) error {
UpdateProgress(0, "Downloading APPINDEX")
resp, err := http.Get(appIndexURL)
resp, err := http.Get(url)
if err != nil {
return err
}
@ -52,25 +84,10 @@ func DownloadAppIndex(dest string) error {
return nil
}
type AppIndexEntry struct {
Name string
Version string
Release string // "nightly" / "stable" / etc.
Arch string // e.g. "amd64", "386"
OS string // e.g. "windows", "linux"
Type string // "browser", "addon", "theme", etc.
DownloadURL string
}
func ParseAppIndex(filePath string) ([]AppIndexEntry, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
// parseAppIndexFromReader parses an APPINDEX from any io.Reader.
func parseAppIndexFromReader(r io.Reader) ([]AppIndexEntry, error) {
var entries []AppIndexEntry
scanner := bufio.NewScanner(file)
scanner := bufio.NewScanner(r)
entry := AppIndexEntry{}
for scanner.Scan() {
@ -112,12 +129,222 @@ func ParseAppIndex(filePath string) ([]AppIndexEntry, error) {
entries = append(entries, entry)
}
// Log all parsed entries
fmt.Printf("[INFO] Total parsed entries: %d\n", len(entries))
return entries, scanner.Err()
}
// parseAppIndex reads the APPINDEX file at filePath and parses its contents.
func parseAppIndex(filePath string) ([]AppIndexEntry, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
entries, err := parseAppIndexFromReader(file)
if err != nil {
return nil, err
}
fmt.Printf("[INFO] Total parsed entries from %s: %d\n", filePath, len(entries))
for _, e := range entries {
fmt.Printf(" - Name: %s, Release: %s, Type: %s, OS: %s, Arch: %s, Version: %s, URL: %s\n",
e.Name, e.Release, e.Type, e.OS, e.Arch, e.Version, e.DownloadURL)
}
return entries, scanner.Err()
return entries, nil
}
// saveIndex saves the current list of remote indexes to an INI file located in
// the spm directory under installDir, but only if it's different from the default.
func saveIndex() error {
// Only save if remoteIndexes differs from defaultRemoteIndexes.
if len(remoteIndexes) == len(defaultRemoteIndexes) {
same := true
for i, ri := range remoteIndexes {
if ri != defaultRemoteIndexes[i] {
same = false
break
}
}
if same {
return nil
}
}
installDir, err := GetInstallDir()
if err != nil {
return err
}
spmDir := filepath.Join(installDir, "spm")
if err := os.MkdirAll(spmDir, 0755); err != nil {
return err
}
filePath := filepath.Join(spmDir, "sources.ini")
cfg := ini.Empty()
sec, err := cfg.NewSection("RemoteIndexes")
if err != nil {
return err
}
// Save each remote index as a key/value pair.
for _, ri := range remoteIndexes {
if _, err := sec.NewKey(ri.Name, ri.Link); err != nil {
return err
}
}
return cfg.SaveTo(filePath)
}
// loadIndex loads the list of remote indexes from an INI file located in
// the spm directory under installDir. If the file is missing, it sets remoteIndexes to the default.
func loadIndex() error {
installDir, err := GetInstallDir()
if err != nil {
return err
}
spmDir := filepath.Join(installDir, "spm")
filePath := filepath.Join(spmDir, "sources.ini")
cfg, err := ini.Load(filePath)
if err != nil {
// If file is missing or can't be loaded, use the default.
remoteIndexes = defaultRemoteIndexes
return nil
}
sec := cfg.Section("RemoteIndexes")
var loaded []RemoteIndex
for _, key := range sec.Keys() {
loaded = append(loaded, RemoteIndex{
Name: key.Name(),
Link: key.Value(),
})
}
remoteIndexes = loaded
return nil
}
// UpdateIndex downloads fresh APPINDEX files from all remote sources and saves them
// into the temp directory. If the app is registered, it loads the remote indexes
// from the INI file (or uses the default if not available) and saves them after updating.
func UpdateIndex() error {
tempDir := GetTempDir()
var sources []RemoteIndex
if IsRegistered() {
// Try to load persisted remote indexes.
if err := loadIndex(); err != nil {
// If loading fails, fall back to the default remote indexes.
sources = defaultRemoteIndexes
} else {
sources = remoteIndexes
}
} else {
// Not registered: use default remote indexes.
sources = defaultRemoteIndexes
}
// Download each APPINDEX file.
for _, ri := range sources {
localPath := filepath.Join(tempDir, fmt.Sprintf("appindex_%s.txt", ri.Name))
if err := downloadAppIndex(ri.Link, localPath); err != nil {
return fmt.Errorf("[WARN] AppIndex: failed downloading %s: %v", ri.Link, err)
}
}
// If registered, save the current remote indexes.
if IsRegistered() {
if err := saveIndex(); err != nil {
return fmt.Errorf("[WARN] AppIndex: failed saving indexes: %v", err)
}
}
return nil
}
// GetIndex parses APPINDEX data from local files in the temp directory.
// If a file is missing, it downloads the corresponding APPINDEX first.
// If the app is registered, it loads remote indexes from the INI file.
// Otherwise, it uses the default remote index.
func GetIndex() ([]AppIndexEntry, error) {
var allEntries []AppIndexEntry
tempDir := GetTempDir()
var sources []RemoteIndex
if IsRegistered() {
if err := loadIndex(); err != nil {
sources = defaultRemoteIndexes
} else {
sources = remoteIndexes
}
} else {
sources = defaultRemoteIndexes
}
// For each remote source, ensure the APPINDEX file exists (downloading if needed),
// then parse its contents.
for _, ri := range sources {
localPath := filepath.Join(tempDir, fmt.Sprintf("appindex_%s.txt", ri.Name))
if _, err := os.Stat(localPath); os.IsNotExist(err) {
if err := downloadAppIndex(ri.Link, localPath); err != nil {
return nil, fmt.Errorf("[WARN] AppIndex: failed downloading %s: %v", ri.Link, err)
}
}
entries, err := parseAppIndex(localPath)
if err != nil {
return nil, fmt.Errorf("[WARN] AppIndex: failed parsing %s: %v", localPath, err)
}
allEntries = append(allEntries, entries...)
}
return allEntries, nil
}
// AddIndex adds a new remote index (name and link) into the list,
// sorts the list by name, and if the app is registered, saves the updated list.
func AddIndex(name, link string) error {
// If registered, load current indexes first.
if IsRegistered() {
if err := loadIndex(); err != nil {
return fmt.Errorf("[WARN] AppIndex: failed loading indexes: %w", err)
}
}
ri := RemoteIndex{
Name: name,
Link: link,
}
remoteIndexes = append(remoteIndexes, ri)
sort.Slice(remoteIndexes, func(i, j int) bool {
return remoteIndexes[i].Name < remoteIndexes[j].Name
})
// If registered, persist the changes.
if IsRegistered() {
if err := saveIndex(); err != nil {
return fmt.Errorf("[WARN] AppIndex: failed saving indexes: %w", err)
}
}
return nil
}
// RemoveIndex removes any remote index with the given name from the list,
// and if the app is registered, saves the updated list.
func RemoveIndex(name string) error {
// If registered, load current indexes first.
if IsRegistered() {
if err := loadIndex(); err != nil {
return fmt.Errorf("[WARN] AppIndex: failed loading indexes: %w", err)
}
}
var updated []RemoteIndex
for _, ri := range remoteIndexes {
if ri.Name != name {
updated = append(updated, ri)
}
}
remoteIndexes = updated
// If registered, persist the changes.
if IsRegistered() {
if err := saveIndex(); err != nil {
return fmt.Errorf("[WARN] AppIndex: failed saving indexes: %w", err)
}
}
return nil
}

38
auto.go
View file

@ -2,7 +2,6 @@ package spm
import (
"fmt"
"os"
"path/filepath"
)
@ -10,22 +9,19 @@ import (
// 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
// DownloadUpdates 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, so it can be used by AutoInstallUpdates().
func AutoDownloadUpdates() error {
// and decompresses it into a temporary folder. The result is stored in pendingUpdates, so it can be used by InstallUpdates().
func DownloadUpdates() error {
// 1) Download the APPINDEX file to a temporary location
appIndexPath := filepath.Join(os.TempDir(), "APPINDEX")
fmt.Println("[INFO] Starting APPINDEX download to:", appIndexPath)
err := DownloadAppIndex(appIndexPath)
err := UpdateIndex()
if 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)
entries, err := GetIndex()
if err != nil {
return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err)
}
@ -84,7 +80,7 @@ func AutoDownloadUpdates() error {
downloadDir := GetTempDir()
fmt.Printf("[INFO] Downloading package '%s' to temporary folder: %s\n", matchingEntry.Name, downloadDir)
err = DownloadPackageFromAppIndex(appIndexPath, matchingEntry.Name, matchingEntry.Release, matchingEntry.Type, downloadDir)
err = DownloadPackageFromAppIndex(matchingEntry.Name, matchingEntry.Release, matchingEntry.Type, downloadDir)
if err != nil {
return fmt.Errorf("[ERROR] Failed to download package '%s': %w", matchingEntry.Name, err)
}
@ -99,7 +95,7 @@ func AutoDownloadUpdates() error {
}
fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir)
// 7) Store in pendingUpdates so that AutoInstallUpdates can finish the job
// 7) Store in pendingUpdates so that InstallUpdates can finish the job
fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name)
pendingUpdates = append(pendingUpdates, AppIndexEntry{
Name: matchingEntry.Name,
@ -111,13 +107,13 @@ func AutoDownloadUpdates() error {
})
}
fmt.Println("[INFO] AutoDownloadUpdates completed successfully")
fmt.Println("[INFO] DownloadUpdates completed successfully")
return nil
}
// AutoInstallUpdates installs any packages that were downloaded and decompressed by AutoDownloadUpdates.
// InstallUpdates installs any packages that were downloaded and decompressed by DownloadUpdates.
// It moves files from their temp directories to the final location and updates installed.ini.
func AutoInstallUpdates() error {
func InstallUpdates() error {
installDir, err := GetInstallDir()
if err != nil {
return err
@ -146,7 +142,7 @@ func AutoInstallUpdates() error {
}
// 5) Finalize
err = FinalizeInstall(entry.Name, entry.Release, entry.Version, entry.Arch, entry.OS)
err = finalizeInstall(entry.Name, entry.Release, entry.Version, entry.Arch, entry.OS)
if err != nil {
return fmt.Errorf("failed to finalize install for %s: %w", entry.Name, err)
}
@ -156,18 +152,15 @@ func AutoInstallUpdates() error {
return nil
}
func AutoDownloadSpecified(specs []AppIndexEntry) error {
func DownloadSpecified(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 {
if err := UpdateIndex(); 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)
entries, err := GetIndex()
if err != nil {
return fmt.Errorf("[ERROR] Failed to parse APPINDEX: %w", err)
}
@ -225,7 +218,6 @@ func AutoDownloadSpecified(specs []AppIndexEntry) error {
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,
@ -251,7 +243,7 @@ func AutoDownloadSpecified(specs []AppIndexEntry) error {
}
fmt.Printf("[INFO] Package '%s' decompressed successfully to: %s\n", matchingEntry.Name, tempDir)
// 7) Store in pendingUpdates for AutoInstallUpdates
// 7) Store in pendingUpdates for InstallUpdates
fmt.Printf("[INFO] Adding '%s' to pending updates\n", matchingEntry.Name)
pendingUpdates = append(pendingUpdates, *matchingEntry)
}

View file

@ -108,7 +108,11 @@ func decompressTarGz(srcFile, destDir string, updateProgress func(int, string))
// Update progress after extracting each file.
if updateProgress != nil {
percent := int((progressReader.BytesRead * 100) / totalSize)
updateProgress(percent, fmt.Sprintf("Extracted: %s", header.Name))
name := header.Name
if len(name) > 50 {
name = name[len(name)-50:]
}
updateProgress(percent, fmt.Sprintf("Extracted: %s", name))
}
}

View file

@ -11,9 +11,9 @@ import (
)
// 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(packageName string, release string, pkgType string, destDir string) error {
// Parse the APPINDEX
entries, err := ParseAppIndex(appIndexPath)
entries, err := GetIndex()
if err != nil {
return fmt.Errorf("failed to parse APPINDEX: %w", err)
}
@ -99,8 +99,8 @@ func DownloadPackageFromAppIndex(appIndexPath string, packageName string, releas
expectedFilePath := filepath.Join(destDir, expectedFileName)
// I dont know why is this happening, I dont want to know but sometimes some process is helding up the donwloaded files so thats why it retries here
maxRetries := 5
// I dont know why is this happening, I dont want to know but sometimes some process is helding up the downloaded files so thats why it retries here
maxRetries := 10
for i := 0; i < maxRetries; i++ {
err = os.Rename(downloadedFilePath, expectedFilePath)
if err == nil {
@ -115,7 +115,7 @@ func DownloadPackageFromAppIndex(appIndexPath string, packageName string, releas
f.Close()
if i < maxRetries-1 {
time.Sleep(500 * time.Millisecond) // Wait before retrying
time.Sleep(250 * time.Millisecond) // Wait before retrying
}
}

View file

@ -287,8 +287,8 @@ func copyFile(src, dst string) error {
return os.Chmod(dst, info.Mode())
}
// FinalizeInstall finalizes the installation by updating installed.ini.
func FinalizeInstall(packageName, release, version, arch, osName string) error {
// 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

View file

@ -4,7 +4,11 @@
package spm
import "fmt"
import (
"fmt"
"os"
"path/filepath"
)
// RegisterApp is not supported on non-Windows platforms.
func RegisterApp() error {
@ -15,3 +19,19 @@ func RegisterApp() error {
func UnregisterApp() error {
return fmt.Errorf("UnregisterApp is only available on Windows")
}
// IsRegistered returns true if the application is detected as installed.
// On Linux, we assume it is installed if the main executable exists in the install directory.
func IsRegistered() bool {
installDir, err := GetInstallDir()
if err != nil {
return false
}
// Assume the executable is named "spitfire" and is located in installDir.
exePath := filepath.Join(installDir, "browser", "spitfire")
if _, err := os.Stat(exePath); err == nil {
return true
}
return false
}

View file

@ -10,7 +10,7 @@ import (
"golang.org/x/sys/windows/registry"
)
// RegisterApp writes the necessary registry keys, making it appear as offically installed app
// RegisterApp writes the necessary registry keys, making it appear as officially installed app
func RegisterApp() error {
exePath, err := GetInstallDir()
if err != nil {
@ -133,3 +133,15 @@ func deleteRegistryTree(root registry.Key, path string) error {
// Finally, delete the (now empty) key.
return registry.DeleteKey(root, path)
}
// IsRegistered returns true if the application is registered (installed) in the registry.
func IsRegistered() bool {
// Try to open the uninstall key with read-only access.
key, err := registry.OpenKey(registry.LOCAL_MACHINE, `Software\Microsoft\Windows\CurrentVersion\Uninstall\SpitfireBrowser`, registry.READ)
if err != nil {
// If the key cannot be opened, assume the app is not registered.
return false
}
defer key.Close()
return true
}

View file

@ -9,7 +9,6 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"syscall"
)
@ -21,9 +20,6 @@ func Run() error {
}
exePath := filepath.Join(installDir, "browser", "spitfire.exe")
if runtime.GOOS != "windows" {
exePath = filepath.Join(installDir, "browser", "spitfire")
}
cmd := exec.Command(exePath)
cmd.Dir = filepath.Join(installDir, "browser")

View file

@ -9,7 +9,6 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"syscall"
)
@ -21,9 +20,6 @@ func Run() error {
}
exePath := filepath.Join(installDir, "browser", "spitfire.exe")
if runtime.GOOS != "windows" {
exePath = filepath.Join(installDir, "browser", "spitfire")
}
cmd := exec.Command(exePath)
cmd.Dir = filepath.Join(installDir, "browser")