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 }