Init
This commit is contained in:
commit
d0187f94d7
23 changed files with 2489 additions and 0 deletions
382
spm/appindex.go
Normal file
382
spm/appindex.go
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue