/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: http.reef Authors: Chris Tusa License: Description: HTTP download utilities using native Reef net.http Requires Reef 0.1.16+ for progress bar support ******************************************************************************/ module util.http import net.http import net.extras import io.file import core.str import core.result as res import sys.process import util.color export // Download a file from URL to destination path fn download(url: string, dest: string): bool // Download with verbose progress output fn download_verbose(url: string, dest: string): bool // Fetch URL content as text fn fetch_text(url: string): string // Fetch URL content with timeout fn fetch_text_timeout(url: string, timeout_ms: int): string // Check if a URL is reachable (HEAD request) fn check_url(url: string): bool // Get response status code for a URL fn get_status(url: string): int // Configure proxy usage (call before downloads) proc enable_proxy(use_proxy: bool) end export // Module-level proxy setting mut proxy_enabled = false // Default timeout in milliseconds (30 seconds) fn default_timeout(): int return 30000 end default_timeout // Internal: download with progress bar (Style C: solid block) // Returns true on success, false on failure fn download_with_progress(url: string, dest: string): bool // Configure progress bar - Style C: solid block character extras.progress_bar_set_char("█") extras.progress_bar_set_width(30) extras.progress_bar_init(url) // Download with progress callback let cb = fn(p: http.HttpDownloadProgress): bool => extras.progress_bar(p) return res.is_ok(http.http_download_file_callback(url, dest, cb)) end download_with_progress // Check if URL will redirect (3xx status) - native HTTP download crashes on redirects fn url_will_redirect(url: string): bool let status = get_status(url) return status >= 300 and status < 400 end url_will_redirect // Download a file from URL to destination with progress bar // Uses native Reef http_download_file_callback() (requires Reef 0.1.16+) // Falls back to curl if native download fails or URL redirects fn download(url: string, dest: string): bool let filename = extract_filename(url) print(" " + filename + " ") // Check for redirects first - native HTTP crashes on 302s if not url_will_redirect(url) // Try native Reef download with progress bar if download_with_progress(url, dest) println("") // Newline after progress bar completes return true end if println("") // Newline after failed progress end if // Fallback to curl (handles redirects with -L flag) print(" (using curl) ... ") if download_with_curl(url, dest) println("done") return true end if println("failed") return false end download // Download with verbose progress output // Uses native Reef http_download_file_callback() with progress bar (requires Reef 0.1.16+) fn download_verbose(url: string, dest: string): bool let filename = extract_filename(url) println(" " + url) print(" " + filename + " ") // Check for redirects first - native HTTP crashes on 302s if not url_will_redirect(url) // Try native Reef download with progress bar if download_with_progress(url, dest) println("") // Newline after progress bar completes color.print_success("Download complete: " + filename) return true end if println("") // Newline after failed progress end if // Fallback to curl (handles redirects with -L flag) print(" (using curl) ... ") if download_with_curl(url, dest) println("done") color.print_success("Download complete: " + filename) return true end if println("failed") color.print_error("Download failed: " + url) return false end download_verbose // Fetch URL content as text fn fetch_text(url: string): string return fetch_text_timeout(url, default_timeout()) end fetch_text // Fetch URL content with timeout // Uses http_get_auto() which handles redirects and is safe for text/binary fn fetch_text_timeout(url: string, timeout_ms: int): string // Create request with timeout let req = http.http_request_new("GET", url) http.http_request_set_header(req, "User-Agent", "Coral/1.0") let resp_r = http.http_send_timeout(req, timeout_ms) // Check for errors (transport failure is now Err; HttpResponse no longer has an error field) if res.is_err(resp_r) return "" end if let resp = res.unwrap_ok(resp_r) // Check for success if not http.http_response_is_ok(resp) return "" end if return http.http_response_body(resp) end fetch_text_timeout // Check if a URL is reachable fn check_url(url: string): bool let req = http.http_request_new("HEAD", url) http.http_request_set_header(req, "User-Agent", "Coral/1.0") let resp_r = http.http_send_timeout(req, 10000) if res.is_err(resp_r) return false end if let resp = res.unwrap_ok(resp_r) // Accept success codes and redirects as "reachable" return resp.status_code >= 200 and resp.status_code < 400 end check_url // Get response status code for a URL fn get_status(url: string): int let req = http.http_request_new("HEAD", url) http.http_request_set_header(req, "User-Agent", "Coral/1.0") let resp_r = http.http_send_timeout(req, 10000) if res.is_err(resp_r) return 0 end if let resp = res.unwrap_ok(resp_r) return resp.status_code end get_status // Extract filename from URL fn extract_filename(url: string): string // Find last slash let last_slash = str.last_index_of_char(url, '/') if last_slash < 0 return url end if let filename = str.substring(url, last_slash + 1, str.length(url) - last_slash - 1) // Remove query string if present let query_pos = str.index_of_char(filename, '?') if query_pos > 0 return str.substring(filename, 0, query_pos) end if return filename end extract_filename // Helper: convert int to string fn int_to_str(n: int): string if n == 0 return "0" end if mut negative = false mut value = n if n < 0 negative = true value = 0 - n end if mut result = "" while value > 0 let digit = value % 10 result = str.concat(str.substring("0123456789", digit, 1), result) value = value / 10 end while if negative result = str.concat("-", result) end if return result end int_to_str proc enable_proxy(use_proxy: bool) proxy_enabled = use_proxy end enable_proxy // Download using curl command // Handles HTTP redirects, chunked encoding, and SSL/TLS properly fn download_with_curl(url: string, dest: string): bool // Use curl with: // -L: follow redirects (required for GitHub, etc.) // -f: fail silently on HTTP errors (returns non-zero exit code) // -s: silent mode (no progress meter) // -S: show errors even in silent mode // -o: output file mut proxy_flag = " --noproxy \"*\"" if proxy_enabled proxy_flag = "" end if let cmd = "curl -L -f -s -S" + proxy_flag + " -o \"" + dest + "\" \"" + url + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to spawn curl") return false end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Download failed: " + url) return false end if return true end download_with_curl end module