309 lines
8.9 KiB
Go
309 lines
8.9 KiB
Go
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
|
||
}
|