diff --git a/update_appindex.go b/update_appindex.go new file mode 100644 index 0000000..dc39410 --- /dev/null +++ b/update_appindex.go @@ -0,0 +1,362 @@ +package main + +import ( + "crypto/sha1" + "flag" + "fmt" + "io" + "log" + "os" + "runtime" + "strings" + "time" +) + +// PackageAPPINDEX updates the local APPINDEX file by removing any old +// entry matching (P, R, A, o, p) and appending a new entry. +// +// It includes: +// - Compressed/uncompressed file measurement (size + checksums). +// - A user-defined remotePath for the final download URL. +// - Additional flags for icon, screenshots, tags, and notes. +func PackageAPPINDEX( + name string, // P: Package name, e.g., "spitfire-browser" + release string, // R: Release type, e.g., "nightly" + version string, // V: Version, e.g., "2025.02.07" + arch string, // A: Architecture, e.g., "amd64" + description string, // X: Short description, e.g., "Short summary" + url string, // U: Project URL, e.g., "https://spitfirebrowser.xyz/" + license string, // L: License, e.g., "AGPL-3.0" + origin string, // o: Origin, e.g., "browser" + maintainer string, // m: Maintainer, e.g., "Internet Addict" + dependencies string, // D: Dependencies, e.g., "default-theme, browser" + platform string, // p: Platform, e.g., "linux" + remotePath string, // d: Remote file path, e.g., "browser/amd64/nightly/2025.02.07/browser-amd64-nightly-windows.tar.gz" + icon string, // I: Icon URL, e.g., "https://weforge.xyz/Spitfire/Branding/raw/branch/main/active/browser/icon.svg" + screenshots string, // S: Screenshots URL, e.g., "https://spitfirebrowser.xyz/static/images/screenshots/1.png" + tags string, // T: Tags, e.g., "browser,experimental,testing" + notes string, // r: Notes, e.g., "Automated build of Spitfire" + compressedFile string, // Path to the compressed file, used for size & checksum + uncompressedFile string, // Path to the uncompressed file, used for size & checksum +) error { + //---------------------------------------------------------------------- + // 1) Measure compressed file + //---------------------------------------------------------------------- + compSizeBytes := measureFileSize(compressedFile) + compSize := fmt.Sprintf("%d", compSizeBytes) + compChecksum := calcFileChecksum(compressedFile) + + //---------------------------------------------------------------------- + // 2) Measure uncompressed file + //---------------------------------------------------------------------- + uncompSizeBytes := measureFileSize(uncompressedFile) + uncompSize := fmt.Sprintf("%d", uncompSizeBytes) + uncompChecksum := calcFileChecksum(uncompressedFile) + + //---------------------------------------------------------------------- + // 3) Remove old entry from APPINDEX (based on P,R,A,o,p). + //---------------------------------------------------------------------- + removeExistingEntry(name, release, arch, origin, platform) + + //---------------------------------------------------------------------- + // 4) Check for existence of APPINDEX + //---------------------------------------------------------------------- + appIndexPath := "./APPINDEX" + if _, err := os.Stat(appIndexPath); os.IsNotExist(err) { + fmt.Println("APPINDEX does not exist. Creating a new one...") + } + + //---------------------------------------------------------------------- + // 5) Open (or create) the APPINDEX file for appending. + //---------------------------------------------------------------------- + file, err := os.OpenFile(appIndexPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open APPINDEX file: %v", err) + } + defer file.Close() + + //---------------------------------------------------------------------- + // 6) Build the final download URL from `remotePath` + //---------------------------------------------------------------------- + downloadURL := fmt.Sprintf("https://downloads.sourceforge.net/project/spitfire-browser/%s", remotePath) + + //---------------------------------------------------------------------- + // 7) Current Unix timestamp for t: + //---------------------------------------------------------------------- + timestamp := time.Now().Unix() + + //---------------------------------------------------------------------- + // 8) Format final entry + // + // We'll store: + // C:%s => compressed checksum + // S:%s => compressed size + // I:%s => uncompressed size + // c:%s => uncompressed checksum + // X:%s => short text/description (custom field, formerly "T:") + //---------------------------------------------------------------------- + entry := fmt.Sprintf(` +C:%s +P:%s +R:%s +V:%s +A:%s +S:%s +I:%s +X:%s +U:%s +L:%s +o:%s +m:%s +t:%d +D:%s +p:%s +q: +d:%s +I:%s +S:%s +T:%s +r:%s +c:%s +`, + compChecksum, // C: compressed checksum + name, // P: + release, // R: + version, // V: + arch, // A: + compSize, // S: compressed size + uncompSize, // I: uncompressed size + description, // X: short text description + url, // U: + license, // L: + origin, // o: + maintainer, // m: + timestamp, // t: + dependencies, // D: + platform, // p: + downloadURL, // d: + icon, // I: icon URL + screenshots, // S: screenshots URL + tags, // T: tags + notes, // r: notes + uncompChecksum, // c: uncompressed checksum + ) + + // Trim leading newline for neatness + entry = strings.TrimPrefix(entry, "\n") + + //---------------------------------------------------------------------- + // 9) Write the new entry to APPINDEX + //---------------------------------------------------------------------- + if _, err := file.WriteString(entry + "\n"); err != nil { + return fmt.Errorf("failed to write to APPINDEX: %v", err) + } + + fmt.Println("APPINDEX has been updated successfully.") + return nil +} + +// calcFileChecksum calculates SHA-1 of an entire file. Returns "" if file not found or error. +func calcFileChecksum(path string) string { + f, err := os.Open(path) + if err != nil { + return "" + } + defer f.Close() + + h := sha1.New() + if _, err := io.Copy(h, f); err != nil { + return "" + } + return fmt.Sprintf("%x", h.Sum(nil)) +} + +// measureFileSize returns file size in bytes. Returns 0 if file not found or error. +func measureFileSize(path string) int64 { + info, err := os.Stat(path) + if err != nil { + return 0 + } + return info.Size() +} + +// removeExistingEntry removes an existing entry from APPINDEX if it matches +// P (name) = "P:" +// R (release) = "R:" +// A (arch) = "A:" +// o (origin) = "o:" +// p (platform) = "p:" +func removeExistingEntry(name, release, arch, origin, platform string) { + content, err := os.ReadFile("./APPINDEX") + if err != nil { + if os.IsNotExist(err) { + // No file to remove from + return + } + log.Fatalf("Failed to read APPINDEX: %v", err) + } + + lines := strings.Split(string(content), "\n") + var newLines []string + var currentEntry []string + inEntry := false + + // We'll track P:, R:, A:, o:, p: fields for each entry + var pVal, rVal, aVal, oVal, plVal string + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + + // Each entry *starts* with "C:..." + if strings.HasPrefix(trimmed, "C:") { + // We've hit the start of a new entry. Decide whether + // we keep the old one we were collecting. + if inEntry && len(currentEntry) > 0 { + if !(pVal == name && rVal == release && aVal == arch && oVal == origin && plVal == platform) { + newLines = append(newLines, currentEntry...) + newLines = append(newLines, "") // optional blank line + } + } + // Start a new entry + currentEntry = []string{trimmed} + inEntry = true + pVal, rVal, aVal, oVal, plVal = "", "", "", "", "" + continue + } + + if inEntry { + currentEntry = append(currentEntry, trimmed) + // Check if it's one of the fields we track + switch { + case strings.HasPrefix(trimmed, "P:"): + pVal = strings.TrimPrefix(trimmed, "P:") + case strings.HasPrefix(trimmed, "R:"): + rVal = strings.TrimPrefix(trimmed, "R:") + case strings.HasPrefix(trimmed, "A:"): + aVal = strings.TrimPrefix(trimmed, "A:") + case strings.HasPrefix(trimmed, "o:"): + oVal = strings.TrimPrefix(trimmed, "o:") + case strings.HasPrefix(trimmed, "p:"): + plVal = strings.TrimPrefix(trimmed, "p:") + } + } else if trimmed != "" { + // Lines outside an entry + newLines = append(newLines, trimmed) + } + } + + // After the loop, handle any last entry we collected + if inEntry && len(currentEntry) > 0 { + if !(pVal == name && rVal == release && aVal == arch && oVal == origin && plVal == platform) { + newLines = append(newLines, currentEntry...) + } + } + + finalContent := strings.Join(newLines, "\n") + if !strings.HasSuffix(finalContent, "\n") { + finalContent += "\n" + } + + if err := os.WriteFile("./APPINDEX", []byte(finalContent), 0644); err != nil { + log.Fatalf("Failed to update APPINDEX: %v", err) + } +} + +func main() { + //---------------------------------------------------------------------- + // Default flags for basic fields + //---------------------------------------------------------------------- + defaultRelease := "nightly" + defaultVersion := time.Now().Format("2006.01.02") // e.g. "2025.02.07" + defaultArch := runtime.GOARCH // e.g. "amd64" + + //---------------------------------------------------------------------- + // Primary fields: P, R, V, A, X (desc), U, L, o, m, D, p + //---------------------------------------------------------------------- + name := flag.String("name", "spitfire-browser", "P: Package name") + release := flag.String("release", defaultRelease, "R: Release (nightly, stable, etc.)") + version := flag.String("version", defaultVersion, "V: Version (date or semantic)") + arch := flag.String("arch", defaultArch, "A: Architecture (default=GOARCH)") + description := flag.String("description", "Spitfire build", "X: Short text description") + url := flag.String("url", "https://spitfirebrowser.xyz/", "U: Project URL") + license := flag.String("license", "AGPL-3.0", "L: License") + origin := flag.String("origin", "browser", "o: Origin name") + maintainer := flag.String("maintainer", "Internet Addict", "m: Maintainer name") + dependencies := flag.String("dependencies", "", "D: Dependencies (comma-separated)") + platform := flag.String("platform", "windows", "p: Platform (linux, windows, etc.)") + + //---------------------------------------------------------------------- + // Additional flags for icon, screenshots, tags, notes + //---------------------------------------------------------------------- + icon := flag.String("icon", + "https://weforge.xyz/Spitfire/Branding/raw/branch/main/active/browser/icon.svg", + "I: Icon URL") + screenshots := flag.String("screenshots", + "https://spitfirebrowser.xyz/static/images/screenshots/1.png", + "S: Screenshot(s) URL(s)") + tags := flag.String("tags", + "browser,experimental,testing", + "T: Comma-separated tags") + notes := flag.String("notes", + "Automated build of Spitfire", + "r: Additional notes") + + //---------------------------------------------------------------------- + // remotePath is the path under: + // https://downloads.sourceforge.net/project/spitfire-browser/ + // Example: "browser/amd64/nightly/2025.02.07/browser-amd64-nightly-windows.tar.gz" + //---------------------------------------------------------------------- + remotePath := flag.String("remotePath", "", + "Path under https://downloads.sourceforge.net/project/spitfire-browser/...") + + //---------------------------------------------------------------------- + // Compressed & uncompressed artifact paths + //---------------------------------------------------------------------- + compressedFile := flag.String("compressedFile", + "browser-amd64-nightly-windows.tar.gz", + "Local path to compressed artifact (for size/checksum)") + + uncompressedFile := flag.String("uncompressedFile", + "browser-amd64-nightly-windows", + "Local path to uncompressed artifact (for size/checksum)") + + flag.Parse() + + //---------------------------------------------------------------------- + // If user didn't specify remotePath, we build a naive default: + // "browser////---.tar.gz" + //---------------------------------------------------------------------- + if *remotePath == "" { + *remotePath = fmt.Sprintf( + "browser/%s/%s/%s/%s-%s-%s-%s.tar.gz", + *arch, *release, *version, *origin, *arch, *release, *platform, + ) + } + + //---------------------------------------------------------------------- + // Execute the logic + //---------------------------------------------------------------------- + err := PackageAPPINDEX( + *name, + *release, + *version, + *arch, + *description, + *url, + *license, + *origin, + *maintainer, + *dependencies, + *platform, + *remotePath, + *icon, + *screenshots, + *tags, + *notes, + *compressedFile, + *uncompressedFile, + ) + if err != nil { + log.Fatalf("APPINDEX update failed: %v", err) + } +}