1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
|
/******************************************************************************
__ ____ __
/ / ___ ____ _/ __/_____________ _/ /__
/ / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
/ /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/
/_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
(C)opyright 2025, Leafscale, LLC - https://www.leafscale.com
Project: Zygaena
Filename: http.reef
Authors: Chris Tusa <chris.tusa@leafscale.com>
License: <see LICENSE file included with this source code>
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 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 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 = http.http_send_timeout(req, timeout_ms)
// Check for errors
if str.length(resp.error) > 0
return ""
end if
// 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 = http.http_send_timeout(req, 10000)
if str.length(resp.error) > 0
return false
end if
// 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 = http.http_send_timeout(req, 10000)
if str.length(resp.error) > 0
return 0
end if
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
|