Files
fn_registry/functions/infra/http_download_file.go
T
egutierrez 9c0d24d3ef feat: funciones Go — core (cron, join_by_key, validate_struct), datascience (pivot, diff_entities), infra (http, cache, cron_ticker)
Nuevas funciones Go con tests en tres dominios:
- core: parse_cron_expr, next_cron_time, join_by_key, validate_struct_fields + tipo CronSchedule
- datascience: pivot (tabla dinámica), diff_entities (comparación de entidades)
- infra: http_get_json, http_post_json, http_download_file, cache_to_sqlite, cron_ticker

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:11:12 +02:00

72 lines
1.8 KiB
Go

package infra
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
)
// HttpDownloadFile descarga url en destPath en streaming con io.Copy.
// Crea directorios intermedios con os.MkdirAll. Usa archivo temporal + rename
// para garantizar atomicidad (no deja archivo corrupto si falla a mitad).
// Retorna los bytes escritos.
func HttpDownloadFile(url, destPath string, headers map[string]string, timeout time.Duration) (int64, error) {
client := &http.Client{Timeout: timeout}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return 0, fmt.Errorf("http_download_file: build request: %w", err)
}
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := client.Do(req)
if err != nil {
return 0, fmt.Errorf("http_download_file: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
shortURL := url
if len(shortURL) > 100 {
shortURL = shortURL[:100]
}
return 0, fmt.Errorf("http_download_file: HTTP %d at %q", resp.StatusCode, shortURL)
}
dir := filepath.Dir(destPath)
if err := os.MkdirAll(dir, 0o755); err != nil {
return 0, fmt.Errorf("http_download_file: create dirs: %w", err)
}
// Archivo temporal en el mismo directorio para que rename sea atomico
tmp, err := os.CreateTemp(dir, ".download-*")
if err != nil {
return 0, fmt.Errorf("http_download_file: create temp file: %w", err)
}
tmpPath := tmp.Name()
defer func() {
tmp.Close()
os.Remove(tmpPath) // no-op si rename tuvo exito
}()
n, err := io.Copy(tmp, resp.Body)
if err != nil {
return 0, fmt.Errorf("http_download_file: write: %w", err)
}
if err := tmp.Close(); err != nil {
return 0, fmt.Errorf("http_download_file: close temp: %w", err)
}
if err := os.Rename(tmpPath, destPath); err != nil {
return 0, fmt.Errorf("http_download_file: rename: %w", err)
}
return n, nil
}