|
root / src / util / http.reef
http.reef Reef 278 lines 8.1 KB
  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
274
275
276
277
/******************************************************************************
               __               ____                __
              / /   ___  ____ _/ __/_____________ _/ /__
             / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
            / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
           /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/

   (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 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