350 lines
9.1 KiB
Go
350 lines
9.1 KiB
Go
package spm
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gopkg.in/ini.v1"
|
|
)
|
|
|
|
// 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
|
|
)
|
|
|
|
// 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(url)
|
|
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
|
|
}
|
|
|
|
// parseAppIndexFromReader parses an APPINDEX from any io.Reader.
|
|
func parseAppIndexFromReader(r io.Reader) ([]AppIndexEntry, error) {
|
|
var entries []AppIndexEntry
|
|
scanner := bufio.NewScanner(r)
|
|
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)
|
|
}
|
|
|
|
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, 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
|
|
}
|