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 }