This commit is contained in:
partisan 2025-03-09 11:42:53 +01:00
commit d0187f94d7
23 changed files with 2489 additions and 0 deletions

382
spm/appindex.go Normal file
View file

@ -0,0 +1,382 @@
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 // from "P:"
Version string // from "V:"
Release string // from "R:"
Arch string // from "A:"
OS string // from "p:"
Type string // from "o:"
DownloadURL string // from "d:"
Maintainer string // from "m:"
Icon string // from "I:"
Screenshots []string // from "S:"
Tags []string // from "T:"
Description string // from "X:"
URL string // from "U:"
License string // from "L:"
Dependencies []string // from "D:" (split comma-delimited)
Notes string // from "r:"
CompressedFile string // from "C:"
UncompressedFile string // from "c:"
}
// 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()
// "C:" signals the start of a new entry. Append the previous one.
if strings.HasPrefix(line, "C:") {
// Start of a new entry
if entry.Name != "" {
entries = append(entries, entry)
}
entry = AppIndexEntry{}
continue
}
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 "o":
entry.Type = value
case "d":
entry.DownloadURL = value
case "X":
entry.Description = value
case "U":
entry.URL = value
case "L":
entry.License = value
case "m":
entry.Maintainer = value
case "D":
entry.Dependencies = strings.Split(value, ",")
case "I":
entry.Icon = value
case "S":
entry.Screenshots = strings.Split(value, ",")
case "T":
entry.Tags = strings.Split(value, ",")
case "r":
entry.Notes = value
}
}
// Append the last entry if we didn't encounter another "C:"
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
}