package spitfire import ( "archive/tar" "crypto/rand" "compress/gzip" "encoding/json" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "encoding/hex" ) // 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) { file, err := os.Open("sourceforge_config.json") // Assuming a JSON config file if err != nil { return nil, err } defer file.Close() config := &Config{} if err := json.NewDecoder(file).Decode(config); err != nil { return nil, err } 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 { // Create the destination file f, err := os.Create(dstFile) if err != nil { return fmt.Errorf("could not create file %s: %v", dstFile, err) } defer f.Close() // Create a new gzip writer gw := gzip.NewWriter(f) defer gw.Close() // Create a new tar writer with PAX format for large file support tw := tar.NewWriter(gw) defer tw.Close() // Walk through the source directory and add files to the tar archive 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 } header.Name = 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) } return nil } // Upload the file to SourceForge, ensuring the local directory structure is created and uploaded func Upload(config *Config, buildPath, remoteDir string) error { // Generate a random hash for the temp directory name 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 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 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 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 file %s to %s on SourceForge...\n", 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 return scpCmd.Run() } // 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 on the server. A new one will be created.") 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 return cmd.Run() } // 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 }