SPM/appindex.go

351 lines
9.1 KiB
Go
Raw Normal View History

2025-02-25 20:15:39 +01:00
package spm
import (
"bufio"
"fmt"
"io"
"net/http"
"os"
2025-03-01 17:59:26 +01:00
"path/filepath"
"sort"
2025-02-25 20:15:39 +01:00
"strings"
2025-03-01 17:59:26 +01:00
"gopkg.in/ini.v1"
2025-02-25 20:15:39 +01:00
)
2025-03-01 17:59:26 +01:00
// 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
}
// 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
)
2025-02-25 20:15:39 +01:00
2025-03-01 17:59:26 +01:00
// downloadAppIndex downloads an APPINDEX from the given URL and writes it to dest.
func downloadAppIndex(url, dest string) error {
2025-02-25 20:15:39 +01:00
UpdateProgress(0, "Downloading APPINDEX")
2025-03-01 17:59:26 +01:00
resp, err := http.Get(url)
2025-02-25 20:15:39 +01:00
if err != nil {
return err
}
defer resp.Body.Close()
out, err := os.Create(dest)
if err != nil {
return err
}
defer out.Close()
totalSize := resp.ContentLength
var downloaded int64
// Track progress as bytes are downloaded
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, "Downloading APPINDEX")
if _, err := out.Write(buf[:n]); err != nil {
return err
}
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
UpdateProgress(100, "APPINDEX downloaded")
return nil
}
2025-03-01 17:59:26 +01:00
// parseAppIndexFromReader parses an APPINDEX from any io.Reader.
func parseAppIndexFromReader(r io.Reader) ([]AppIndexEntry, error) {
2025-02-25 20:15:39 +01:00
var entries []AppIndexEntry
2025-03-01 17:59:26 +01:00
scanner := bufio.NewScanner(r)
2025-02-25 20:15:39 +01:00
entry := AppIndexEntry{}
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "C:") {
// Start of a new entry
if entry.Name != "" {
entries = append(entries, entry)
entry = AppIndexEntry{}
}
}
parts := strings.SplitN(line, ":", 2)
if len(parts) < 2 {
continue
}
key, value := parts[0], parts[1]
switch key {
case "P":
entry.Name = value
case "R":
entry.Release = value
case "V":
entry.Version = value
case "A":
entry.Arch = value
case "p":
entry.OS = value
case "d":
entry.DownloadURL = value
case "o":
entry.Type = value
}
}
// Append the last entry if any
if entry.Name != "" {
entries = append(entries, entry)
}
2025-03-01 17:59:26 +01:00
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))
2025-02-25 20:15:39 +01:00
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)
}
2025-03-01 17:59:26 +01:00
return entries, nil
}
2025-02-25 20:15:39 +01:00
2025-03-01 17:59:26 +01:00
// 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
2025-02-25 20:15:39 +01:00
}