added SPM
All checks were successful
/ test-on-windows (push) Successful in 9s
/ test-on-alpine (push) Successful in 4s

This commit is contained in:
partisan 2024-12-25 10:58:31 +01:00
parent 17bb547c74
commit 9e5457c2ec
8 changed files with 779 additions and 18 deletions

108
spm/appindex.go Normal file
View file

@ -0,0 +1,108 @@
package spm
import (
"bufio"
"io"
"net/http"
"os"
"strings"
)
const appIndexURL = "https://downloads.sourceforge.net/project/spitfire-browser/APPINDEX"
func DownloadAppIndex(dest string) error {
UpdateProgress(0, "Downloading APPINDEX")
resp, err := http.Get(appIndexURL)
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
}
type AppIndexEntry struct {
Name string
Version string
Release string
Arch string
OS string
DownloadURL string
}
func ParseAppIndex(filePath string) ([]AppIndexEntry, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer file.Close()
var entries []AppIndexEntry
scanner := bufio.NewScanner(file)
entry := AppIndexEntry{}
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "C:") {
if entry.Name != "" {
entries = append(entries, entry)
entry = AppIndexEntry{}
}
}
parts := strings.SplitN(line, ":", 2)
if len(parts) < 2 {
continue
}
switch parts[0] {
case "P":
entry.Name = parts[1]
case "R":
entry.Release = parts[1]
case "V":
entry.Version = parts[1]
case "A":
entry.Arch = parts[1]
case "p":
entry.OS = parts[1]
case "d":
entry.DownloadURL = parts[1]
}
}
if entry.Name != "" {
entries = append(entries, entry)
}
return entries, scanner.Err()
}

91
spm/download.go Normal file
View file

@ -0,0 +1,91 @@
package spm
import (
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
)
type Package struct {
Name string
Arch string
OS string
DownloadURL string
}
func DownloadPackage(pkg Package, destDir string) error {
UpdateProgress(0, fmt.Sprintf("Downloading %s", pkg.Name))
resp, err := http.Get(pkg.DownloadURL)
if err != nil {
return err
}
defer resp.Body.Close()
filePath := filepath.Join(destDir, filepath.Base(pkg.DownloadURL))
out, err := os.Create(filePath)
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, fmt.Sprintf("Downloading %s", pkg.Name))
if _, err := out.Write(buf[:n]); err != nil {
return err
}
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
UpdateProgress(100, fmt.Sprintf("%s downloaded", pkg.Name))
return nil
}
func FindPackage(entries []AppIndexEntry, name, release, arch, os string) (*AppIndexEntry, error) {
for _, entry := range entries {
if entry.Name == name && entry.Release == release && entry.Arch == arch && entry.OS == os {
return &entry, nil
}
}
return nil, errors.New("package not found")
}
func DownloadPackageFromAppIndex(appIndexPath, packageName, release, destDir string) error {
arch := runtime.GOARCH
osName := runtime.GOOS
entries, err := ParseAppIndex(appIndexPath)
if err != nil {
return err
}
entry, err := FindPackage(entries, packageName, release, arch, osName)
if err != nil {
return err
}
return DownloadPackage(Package{
Name: entry.Name,
Arch: entry.Arch,
OS: entry.OS,
DownloadURL: entry.DownloadURL,
}, destDir)
}

161
spm/install.go Normal file
View file

@ -0,0 +1,161 @@
package spm
import (
"archive/tar"
"compress/gzip"
"io"
"os"
"path/filepath"
)
func DecompressToTemp(filePath string) (string, error) {
UpdateProgress(0, "Decompressing package")
tempDir, err := os.MkdirTemp("", "spm_decompress")
if err != nil {
return "", err
}
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
gzr, err := gzip.NewReader(file)
if err != nil {
return "", err
}
defer gzr.Close()
tarReader := tar.NewReader(gzr)
var totalFiles, processedFiles int
// Count total files
for {
_, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
totalFiles++
}
file.Seek(0, io.SeekStart)
gzr.Reset(file)
tarReader = tar.NewReader(gzr)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
targetPath := filepath.Join(tempDir, header.Name)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil {
return "", err
}
case tar.TypeReg:
outFile, err := os.Create(targetPath)
if err != nil {
return "", err
}
if _, err := io.Copy(outFile, tarReader); err != nil {
return "", err
}
outFile.Close()
}
processedFiles++
UpdateProgress(int(float64(processedFiles)/float64(totalFiles)*100), "Decompressing package")
}
UpdateProgress(100, "Package decompressed")
return tempDir, nil
}
func MoveFilesToInstallDir(tempDir, installDir string) error {
// Count total files to copy
var totalFiles, copiedFiles int
err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
totalFiles++
}
return nil
})
if err != nil {
return err
}
// Copy files and track progress
err = filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
relPath, err := filepath.Rel(tempDir, path)
if err != nil {
return err
}
targetPath := filepath.Join(installDir, relPath)
if info.IsDir() {
// Create directories in the install directory
if err := os.MkdirAll(targetPath, os.ModePerm); err != nil {
return err
}
} else {
// Copy files to the install directory
if err := copyFile(path, targetPath); err != nil {
return err
}
copiedFiles++
UpdateProgress(int(float64(copiedFiles)/float64(totalFiles)*100), "Copying files to install directory")
}
return nil
})
if err != nil {
return err
}
// Clean up temporary directory
UpdateProgress(100, "Cleaning up temporary files")
return os.RemoveAll(tempDir)
}
// copyFile copies the contents of the source file to the destination file.
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
// Create the destination file
destinationFile, err := os.Create(dst)
if err != nil {
return err
}
defer destinationFile.Close()
// Copy the file content
if _, err := io.Copy(destinationFile, sourceFile); err != nil {
return err
}
// Preserve file permissions
info, err := sourceFile.Stat()
if err != nil {
return err
}
return os.Chmod(dst, info.Mode())
}

30
spm/progress.go Normal file
View file

@ -0,0 +1,30 @@
package spm
import (
"fmt"
"sync"
)
type Progress struct {
mu sync.Mutex
percentage int
task string
}
var progress = Progress{}
// UpdateProgress sets the current percentage and task.
func UpdateProgress(percentage int, task string) {
progress.mu.Lock()
defer progress.mu.Unlock()
progress.percentage = percentage
progress.task = task
fmt.Printf("\r[%3d%%] %s", percentage, task) // Print progress to the terminal
}
// GetProgress returns the current progress state.
func GetProgress() (int, string) {
progress.mu.Lock()
defer progress.mu.Unlock()
return progress.percentage, progress.task
}

64
spm/utils.go Normal file
View file

@ -0,0 +1,64 @@
package spm
import (
"fmt"
"os"
"path/filepath"
"runtime"
)
func GetTempDownloadDir() string {
dir, err := os.MkdirTemp("", "spm_downloads")
if err != nil {
panic(err)
}
return dir
}
func SetDownloadFolder(customDir string) (string, error) {
if err := os.MkdirAll(customDir, os.ModePerm); err != nil {
return "", err
}
return customDir, nil
}
// GetDefaultInstallDir generates the default installation directory based on the OS and environment.
func GetDefaultInstallDir() (string, error) {
var installDir string
switch runtime.GOOS {
case "windows":
// Use %APPDATA% or Program Files on Windows
appData := os.Getenv("APPDATA")
if appData != "" {
installDir = filepath.Join(appData, "Spitfire")
} else {
programFiles := os.Getenv("ProgramFiles")
if programFiles == "" {
return "", fmt.Errorf("unable to determine default install directory on Windows")
}
installDir = filepath.Join(programFiles, "Spitfire")
}
case "darwin":
// Use ~/Library/Application Support on macOS
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("unable to determine home directory on macOS: %w", err)
}
installDir = filepath.Join(homeDir, "Library", "Application Support", "Spitfire")
case "linux":
// Use ~/.local/share or /opt on Linux
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("unable to determine home directory on Linux: %w", err)
}
installDir = filepath.Join(homeDir, ".local", "share", "Spitfire")
default:
return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
}
return installDir, nil
}