// download.go fetches a remote file (presigned MinIO URL or any HTTP URL) and // saves it to a local path. Uses stdlib only — no external HTTP client needed. package runner import ( "context" "fmt" "io" "net/http" "os" "path/filepath" ) // DownloadFile fetches the resource at rawURL and writes it to destPath, // creating parent directories as needed. Returns the number of bytes written. func DownloadFile(ctx context.Context, rawURL, destPath string) (int64, error) { if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil { return 0, fmt.Errorf("mkdir: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil) if err != nil { return 0, fmt.Errorf("new request: %w", err) } resp, err := http.DefaultClient.Do(req) if err != nil { return 0, fmt.Errorf("GET: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return 0, fmt.Errorf("server returned %d", resp.StatusCode) } f, err := os.Create(destPath) if err != nil { return 0, fmt.Errorf("create file: %w", err) } defer f.Close() n, err := io.Copy(f, resp.Body) if err != nil { return 0, fmt.Errorf("write: %w", err) } return n, nil } // UploadFile PUTs a local file to a presigned MinIO/S3 URL. // MinIO presigned PUT expects the raw bytes in the request body with // Content-Type application/octet-stream. func UploadFile(ctx context.Context, rawURL, filePath string) (int64, error) { f, err := os.Open(filePath) if err != nil { return 0, fmt.Errorf("open: %w", err) } defer f.Close() stat, err := f.Stat() if err != nil { return 0, fmt.Errorf("stat: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPut, rawURL, f) if err != nil { return 0, fmt.Errorf("new request: %w", err) } req.ContentLength = stat.Size() req.Header.Set("Content-Type", "application/octet-stream") resp, err := http.DefaultClient.Do(req) if err != nil { return 0, fmt.Errorf("PUT: %w", err) } defer resp.Body.Close() // MinIO returns 200 on successful PUT of presigned objects if resp.StatusCode >= 300 { return 0, fmt.Errorf("upload server returned %d", resp.StatusCode) } return stat.Size(), nil }