53200cbc0d
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>
72 lines
1.8 KiB
Go
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
|
|
}
|