Builder/spitfire/upload.go
2025-04-13 19:21:04 +02:00

309 lines
8.9 KiB
Go
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package spitfire
import (
"archive/tar"
"compress/gzip"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/mitchellh/go-homedir"
)
// Config struct to hold SourceForge configurations
type Config struct {
SFKeyPath string
SFUser string
SFHost string
SFProject string
}
// Load the SourceForge configuration from a file
func LoadConfig() (*Config, error) {
fmt.Println("🔑 Loading SourceForge configuration...")
file, err := os.Open("sourceforge_config.json")
if err != nil {
return nil, fmt.Errorf("❌ failed to open config file: %v", err)
}
defer file.Close()
config := &Config{}
if err := json.NewDecoder(file).Decode(config); err != nil {
return nil, fmt.Errorf("❌ failed to decode config file: %v", err)
}
fmt.Println("🔍 Expanding SSH key path...")
expandedPath, err := homedir.Expand(config.SFKeyPath)
if err != nil {
return nil, fmt.Errorf("❌ failed expanding key path: %v", err)
}
config.SFKeyPath = filepath.Clean(expandedPath)
fmt.Println("🔐 Validating SSH key...")
if _, err := os.Stat(config.SFKeyPath); os.IsNotExist(err) {
return nil, fmt.Errorf("❌ SSH key file not found at path: %s", config.SFKeyPath)
} else if err != nil {
return nil, fmt.Errorf("❌ error accessing SSH key file: %v", err)
}
fmt.Println("✅ Configuration loaded successfully")
return config, nil
}
// CompressDirectory compresses the build directory to a tar.gz file using PAX format for large file support
func CompressDirectory(srcDir, dstFile string) error {
fmt.Printf("🗜️ Compressing directory: %s → %s\n", srcDir, dstFile)
fmt.Println("📄 Creating destination file...")
f, err := os.Create(dstFile)
if err != nil {
return fmt.Errorf("❌ could not create file %s: %v", dstFile, err)
}
defer f.Close()
fmt.Println("📦 Initializing gzip writer...")
gw := gzip.NewWriter(f)
defer gw.Close()
fmt.Println("📦 Initializing tar writer (PAX format)...")
tw := tar.NewWriter(gw)
defer tw.Close()
fmt.Println("🔍 Walking directory structure...")
err = filepath.Walk(srcDir, func(file string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
// Create tar header using PAX format
header, err := tar.FileInfoHeader(fi, "")
if err != nil {
return err
}
// Set the correct header name, preserving the relative directory structure
relPath, err := filepath.Rel(srcDir, file)
if err != nil {
return err
}
// Normalize paths to UNIX-style for tar compatibility
header.Name = filepath.ToSlash(relPath)
// Explicitly set the type flag for directories
if fi.IsDir() {
header.Typeflag = tar.TypeDir
} else if fi.Mode()&os.ModeSymlink != 0 {
// Handle symlinks
linkTarget, err := os.Readlink(file)
if err != nil {
return err
}
header.Linkname = linkTarget
}
// Write the header to the tarball
if err := tw.WriteHeader(header); err != nil {
return err
}
// If it's a directory or symlink, skip writing its contents
if fi.IsDir() || fi.Mode()&os.ModeSymlink != 0 {
return nil
}
// Open the file for reading
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
// Copy the file content to the tar writer
if _, err := io.Copy(tw, f); err != nil {
return err
}
return nil
})
if err != nil {
return fmt.Errorf("❌ error walking the source directory %s: %v", srcDir, err)
}
fmt.Println("✅ Compression completed successfully")
return nil
}
// Upload the file to SourceForge, ensuring the local directory structure is created and uploaded
func Upload(config *Config, buildPath, remoteDir string) error {
fmt.Println("📤 Starting upload process...")
// Generate a random hash for the temp directory name
fmt.Println("🔒 Generating random hash for temp directory...")
randomHash, err := generateRandomHash(8) // 8 bytes = 16 hex characters
if err != nil {
return fmt.Errorf("❌ failed to generate random hash: %v", err)
}
// Create a temporary directory with the random hash appended
fmt.Printf("📂 Creating temporary directory with hash: %s...\n", randomHash)
tmpDir, err := os.MkdirTemp("", "spitfire-upload-"+randomHash)
if err != nil {
return fmt.Errorf("❌ failed to create temporary directory: %v", err)
}
// Create the required local directory structure inside the temporary directory
fmt.Printf("📁 Creating local directory structure: %s...\n", remoteDir)
localDir := filepath.Join(tmpDir, remoteDir)
err = os.MkdirAll(localDir, os.ModePerm)
if err != nil {
return fmt.Errorf("❌ failed to create local directory structure: %v", err)
}
// Move the build file to the local directory structure
fmt.Printf("📦 Copying build file to temp location: %s...\n", filepath.Base(buildPath))
destinationFile := filepath.Join(localDir, filepath.Base(buildPath))
err = copyFile(buildPath, destinationFile)
if err != nil {
return fmt.Errorf("❌ failed to copy file to local directory structure: %v", err)
}
// Upload the entire local directory structure to the remote directory
fmt.Printf("🚀 Uploading %s to SourceForge (%s)...\n", filepath.Base(buildPath), remoteDir)
scpCmd := exec.Command("scp", "-i", config.SFKeyPath, "-r", tmpDir+"/.", fmt.Sprintf("%s@%s:%s", config.SFUser, config.SFHost, "/"))
scpCmd.Stdout = os.Stdout
scpCmd.Stderr = os.Stderr
if err := scpCmd.Run(); err != nil {
return fmt.Errorf("❌ upload failed: %v", err)
}
fmt.Println("✅ Upload completed successfully")
return nil
}
// Helper function to generate a random hash
func generateRandomHash(length int) (string, error) {
bytes := make([]byte, length)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
// Helper function to copy a file from src to dst
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
if err != nil {
return err
}
return destFile.Sync() // Ensure all writes to the file are flushed
}
// Download the APPINDEX file from SourceForge
func DownloadAPPINDEX(config *Config, remoteDir string) error {
fmt.Println("📥 Downloading APPINDEX from SourceForge...")
// Construct the correct path without double slashes
remoteAPPINDEXPath := filepath.Join(remoteDir, "APPINDEX")
// Run the SCP command to download the APPINDEX file
cmd := exec.Command("scp", "-i", config.SFKeyPath, fmt.Sprintf("%s@%s:%s", config.SFUser, config.SFHost, remoteAPPINDEXPath), "./APPINDEX")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
// Check if the error is due to the file not existing
if strings.Contains(err.Error(), "No such file or directory") {
fmt.Println(" APPINDEX file not found - will create new one")
return nil // Continue without failing if the APPINDEX is missing
}
return fmt.Errorf("❌ failed to download APPINDEX: %v", err) // Fail for other types of errors
}
fmt.Println("✅ APPINDEX downloaded successfully")
return nil
}
// Upload the updated APPINDEX file to SourceForge
func UploadAPPINDEX(config *Config, remoteDir string) error {
fmt.Println("📤 Uploading updated APPINDEX to SourceForge...")
cmd := exec.Command("scp", "-i", config.SFKeyPath, "./APPINDEX", fmt.Sprintf("%s@%s:%s", config.SFUser, config.SFHost, remoteDir))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("❌ failed to upload APPINDEX: %v", err)
}
fmt.Println("✅ APPINDEX uploaded successfully")
return nil
}
// GetDirectorySize calculates the total size of all files in a directory
func GetDirectorySize(path string) (int64, error) {
var size int64
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
size += info.Size()
}
return nil
})
return size, err
}
// GetFileSize returns the size of a file in bytes
func GetFileSize(filePath string) (int64, error) {
fileInfo, err := os.Stat(filePath)
if err != nil {
return 0, err
}
return fileInfo.Size(), nil
}
// BytesToHumanReadable converts bytes to a human-readable format (e.g., MiB, GiB)
func BytesToHumanReadable(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.2f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
// CalculateCompressionEfficiency calculates the compression ratio and efficiency percentage
func CalculateCompressionEfficiency(uncompressed, compressed int64) (float64, float64) {
if uncompressed == 0 {
return 0, 0
}
compressionRatio := float64(uncompressed) / float64(compressed)
efficiency := 100 * (1 - float64(compressed)/float64(uncompressed))
return compressionRatio, efficiency
}