2
0
Files
gitcaddy-runner/internal/pkg/artifact/upload_helper.go
logikonline 63967eb6fa
Some checks failed
CI / build-and-test (push) Has been cancelled
Release / build (arm64, darwin) (push) Has been cancelled
Release / build (amd64, windows) (push) Has been cancelled
Release / build (arm64, linux) (push) Has been cancelled
Release / build (amd64, darwin) (push) Has been cancelled
Release / build (amd64, linux) (push) Has been cancelled
Release / release (push) Has been cancelled
style(ui): add package docs and mark unused parameters
Adds package-level documentation comments across cmd and internal packages. Marks unused function parameters with underscore prefix to satisfy linter requirements. Replaces if-else chains with switch statements for better readability. Explicitly ignores os.Setenv return value where error handling is not needed.
2026-01-19 01:16:47 -05:00

147 lines
3.9 KiB
Go

// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
// Package artifact provides utilities for handling artifact uploads.
package artifact
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"time"
log "github.com/sirupsen/logrus"
)
// UploadHelper handles reliable file uploads with retry logic
type UploadHelper struct {
MaxRetries int
RetryDelay time.Duration
ChunkSize int64
ConnectTimeout time.Duration
MaxTimeout time.Duration
}
// NewUploadHelper creates a new upload helper with sensible defaults
func NewUploadHelper() *UploadHelper {
return &UploadHelper{
MaxRetries: 5,
RetryDelay: 10 * time.Second,
ChunkSize: 10 * 1024 * 1024, // 10MB
ConnectTimeout: 120 * time.Second,
MaxTimeout: 3600 * time.Second,
}
}
// UploadWithRetry uploads a file with automatic retry on failure
func (u *UploadHelper) UploadWithRetry(url, token, filepath string) error {
client := &http.Client{
Timeout: u.MaxTimeout,
Transport: &http.Transport{
MaxIdleConns: 10,
MaxIdleConnsPerHost: 5,
IdleConnTimeout: 90 * time.Second,
DisableKeepAlives: false, // Keep connections alive
ForceAttemptHTTP2: false, // Use HTTP/1.1 for large uploads
},
}
var lastErr error
for attempt := 0; attempt < u.MaxRetries; attempt++ {
if attempt > 0 {
delay := u.RetryDelay * time.Duration(attempt)
log.Infof("Upload attempt %d/%d, waiting %v before retry...", attempt+1, u.MaxRetries, delay)
time.Sleep(delay)
}
// Pre-resolve DNS / warm connection
if err := u.prewarmConnection(url); err != nil {
lastErr = fmt.Errorf("connection prewarm failed: %w", err)
log.Warnf("Prewarm failed: %v", err)
continue
}
// Attempt upload
if err := u.doUpload(client, url, token, filepath); err != nil {
lastErr = err
log.Warnf("Upload attempt %d failed: %v", attempt+1, err)
continue
}
log.Infof("Upload succeeded on attempt %d", attempt+1)
return nil // Success
}
return fmt.Errorf("upload failed after %d attempts: %w", u.MaxRetries, lastErr)
}
// prewarmConnection establishes a connection to help with DNS and TCP setup
func (u *UploadHelper) prewarmConnection(url string) error {
req, err := http.NewRequest("HEAD", url, nil)
if err != nil {
return err
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
_ = resp.Body.Close()
return nil
}
// doUpload performs the actual file upload
func (u *UploadHelper) doUpload(client *http.Client, url, token, filepath string) error {
file, err := os.Open(filepath)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer func() { _ = file.Close() }()
stat, err := file.Stat()
if err != nil {
return fmt.Errorf("failed to stat file: %w", err)
}
log.Infof("Uploading %s (%d bytes) to %s", filepath, stat.Size(), url)
// Create multipart form
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("attachment", stat.Name())
if err != nil {
return fmt.Errorf("failed to create form file: %w", err)
}
if _, err := io.Copy(part, file); err != nil {
return fmt.Errorf("failed to copy file to form: %w", err)
}
_ = writer.Close()
req, err := http.NewRequest("POST", url, body)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Connection", "keep-alive")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("upload request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
respBody, _ := io.ReadAll(resp.Body)
return fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(respBody))
}
log.Infof("Upload completed successfully, status: %d", resp.StatusCode)
return nil
}