Skip to content

Commit e485736

Browse files
committed
feat: add progress callback support
1 parent 42ae9a1 commit e485736

2 files changed

Lines changed: 98 additions & 16 deletions

File tree

src/init.lua

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ ffi.cdef [[
1414
} curl_slist;
1515

1616
typedef size_t (*curl_write_callback)(char *ptr, size_t size, size_t nmemb, void *userdata);
17+
typedef int (*curl_xferinfo_callback)(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow);
1718

1819
/* CURLoption values (CINIT macro: type*10000 + num) */
1920
/* OBJECTPOINT = 10000, FUNCTIONPOINT = 20000, LONG = 0, OFF_T = 30000 */
@@ -52,22 +53,24 @@ lib.curl_global_init(3) -- CURL_GLOBAL_ALL
5253

5354
-- CURLoption constants
5455
local OPT = {
55-
URL = 10002,
56-
WRITEFUNCTION = 20011,
57-
WRITEDATA = 10001,
58-
HTTPHEADER = 10023,
59-
POSTFIELDS = 10015,
60-
CUSTOMREQUEST = 10036,
61-
FOLLOWLOCATION = 52,
62-
TIMEOUT = 13,
63-
VERBOSE = 41,
64-
USERAGENT = 10018,
65-
USERNAME = 10173,
66-
PASSWORD = 10174,
67-
SSL_VERIFYPEER = 64,
68-
SSL_VERIFYHOST = 81,
69-
CAINFO = 10065,
70-
CAINFO_BLOB = 40309
56+
URL = 10002,
57+
WRITEFUNCTION = 20011,
58+
WRITEDATA = 10001,
59+
HTTPHEADER = 10023,
60+
POSTFIELDS = 10015,
61+
CUSTOMREQUEST = 10036,
62+
FOLLOWLOCATION = 52,
63+
TIMEOUT = 13,
64+
VERBOSE = 41,
65+
USERAGENT = 10018,
66+
USERNAME = 10173,
67+
PASSWORD = 10174,
68+
SSL_VERIFYPEER = 64,
69+
SSL_VERIFYHOST = 81,
70+
CAINFO = 10065,
71+
CAINFO_BLOB = 40309,
72+
NOPROGRESS = 43,
73+
XFERINFOFUNCTION = 20219
7174
}
7275

7376
-- on non-Windows, point curl at a CA bundle (bundled cacert.pem next to the lib, or system fallback)
@@ -113,6 +116,8 @@ local INFO = {
113116
EFFECTIVE_URL = 0x100001
114117
}
115118

119+
--- @alias CurlProgressCallback fun(dltotal: number, dlnow: number, ultotal: number, ulnow: number): boolean|number|nil
120+
116121
--- @class CurlResponse
117122
--- @field status number HTTP status code
118123
--- @field body string Response body
@@ -132,6 +137,7 @@ local INFO = {
132137
--- @field username string|nil Username for auth
133138
--- @field password string|nil Password for auth
134139
--- @field verifySsl boolean|nil Verify SSL cert (default: true)
140+
--- @field progress CurlProgressCallback|nil Progress callback; return truthy to abort
135141

136142
-- pre-allocated output slots reused across requests
137143
local statusOut = ffi.new("long[1]")
@@ -179,6 +185,15 @@ local function request(opts)
179185
if opts.username then lib.curl_easy_setopt(handle, OPT.USERNAME, opts.username) end
180186
if opts.password then lib.curl_easy_setopt(handle, OPT.PASSWORD, opts.password) end
181187

188+
local progressCb
189+
if opts.progress then
190+
progressCb = ffi.cast("curl_xferinfo_callback", function(_, dltotal, dlnow, ultotal, ulnow)
191+
return opts.progress(tonumber(dltotal), tonumber(dlnow), tonumber(ultotal), tonumber(ulnow)) and 1 or 0
192+
end)
193+
setlong(OPT.NOPROGRESS, 0)
194+
lib.curl_easy_setopt(handle, OPT.XFERINFOFUNCTION, progressCb)
195+
end
196+
182197
-- headers
183198
local slist = nil
184199
if opts.headers then
@@ -274,6 +289,17 @@ function curl.download(url, path, opts)
274289
lib.curl_easy_setopt(handle, OPT.CAINFO_BLOB, defaultCainfoBlob)
275290
end
276291
lib.curl_easy_setopt(handle, OPT.WRITEDATA, f)
292+
293+
local progressCb
294+
if opts and opts.progress then
295+
progressCb = ffi.cast("curl_xferinfo_callback", function(_, dltotal, dlnow, ultotal, ulnow)
296+
return opts.progress(tonumber(dltotal), tonumber(dlnow), tonumber(ultotal), tonumber(ulnow)) and 1 or 0
297+
end)
298+
299+
setlong(OPT.NOPROGRESS, 0)
300+
lib.curl_easy_setopt(handle, OPT.XFERINFOFUNCTION, progressCb)
301+
end
302+
277303
local code = lib.curl_easy_perform(handle)
278304
ffi.C.fclose(f)
279305

tests/curl.test.lua

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,59 @@ test.it("download follows redirects and writes a non-empty tar.gz", function()
6868
-- gzip magic bytes: 0x1f 0x8b
6969
test.equal(magic, "\31\139")
7070
end)
71+
72+
test.it("request calls progress callback", function()
73+
local called = 0
74+
local res, err = curl.request({
75+
url = "https://httpbin.org/get",
76+
progress = function(dltotal, dlnow, ultotal, ulnow)
77+
called = called + 1
78+
end,
79+
})
80+
test.falsy(err)
81+
test.equal(res.status, 200)
82+
test.truthy(called > 0)
83+
end)
84+
85+
test.it("request progress callback can abort", function()
86+
local count = 0
87+
local res, err = curl.request({
88+
url = "https://httpbin.org/get",
89+
progress = function(dltotal, dlnow, ultotal, ulnow)
90+
count = count + 1
91+
return true -- abort immediately
92+
end,
93+
})
94+
test.truthy(err)
95+
test.falsy(res)
96+
test.equal(count, 1)
97+
end)
98+
99+
test.it("download calls progress callback", function()
100+
local path = os.tmpname()
101+
local called = 0
102+
local ok, err = curl.download("https://httpbin.org/get", path, {
103+
progress = function(dltotal, dlnow, ultotal, ulnow)
104+
called = called + 1
105+
end,
106+
})
107+
os.remove(path)
108+
test.falsy(err)
109+
test.truthy(ok)
110+
test.truthy(called > 0)
111+
end)
112+
113+
test.it("download progress callback can abort", function()
114+
local path = os.tmpname()
115+
local count = 0
116+
local ok, err = curl.download("https://httpbin.org/get", path, {
117+
progress = function(dltotal, dlnow, ultotal, ulnow)
118+
count = count + 1
119+
return true -- abort immediately
120+
end,
121+
})
122+
os.remove(path)
123+
test.truthy(err)
124+
test.falsy(ok)
125+
test.equal(count, 1)
126+
end)

0 commit comments

Comments
 (0)