redc 0.1.1.dev1__cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
redc/codes.py ADDED
@@ -0,0 +1,151 @@
1
+ status_descriptions = {
2
+ # Informational (1xx)
3
+ 100: "Continue",
4
+ 101: "Switching Protocols",
5
+ 102: "Processing",
6
+ 103: "Early Hints",
7
+ # Success (2xx)
8
+ 200: "OK",
9
+ 201: "Created",
10
+ 202: "Accepted",
11
+ 203: "Non-Authoritative Information",
12
+ 204: "No Content",
13
+ 205: "Reset Content",
14
+ 206: "Partial Content",
15
+ 207: "Multi-Status",
16
+ 208: "Already Reported",
17
+ 226: "IM Used",
18
+ # Redirection (3xx)
19
+ 300: "Multiple Choices",
20
+ 301: "Moved Permanently",
21
+ 302: "Found",
22
+ 303: "See Other",
23
+ 304: "Not Modified",
24
+ 305: "Use Proxy",
25
+ 307: "Temporary Redirect",
26
+ 308: "Permanent Redirect",
27
+ # Client Errors (4xx)
28
+ 400: "Bad Request",
29
+ 401: "Unauthorized",
30
+ 402: "Payment Required",
31
+ 403: "Forbidden",
32
+ 404: "Not Found",
33
+ 405: "Method Not Allowed",
34
+ 406: "Not Acceptable",
35
+ 407: "Proxy Authentication Required",
36
+ 408: "Request Timeout",
37
+ 409: "Conflict",
38
+ 410: "Gone",
39
+ 411: "Length Required",
40
+ 412: "Precondition Failed",
41
+ 413: "Payload Too Large",
42
+ 414: "URI Too Long",
43
+ 415: "Unsupported Media Type",
44
+ 416: "Range Not Satisfiable",
45
+ 417: "Expectation Failed",
46
+ 418: "I'm a Teapot",
47
+ 421: "Misdirected Request",
48
+ 422: "Unprocessable Entity",
49
+ 423: "Locked",
50
+ 424: "Failed Dependency",
51
+ 425: "Too Early",
52
+ 426: "Upgrade Required",
53
+ 428: "Precondition Required",
54
+ 429: "Too Many Requests",
55
+ 431: "Request Header Fields Too Large",
56
+ 451: "Unavailable For Legal Reasons",
57
+ # Server Errors (5xx)
58
+ 500: "Internal Server Error",
59
+ 501: "Not Implemented",
60
+ 502: "Bad Gateway",
61
+ 503: "Service Unavailable",
62
+ 504: "Gateway Timeout",
63
+ 505: "HTTP Version Not Supported",
64
+ 506: "Variant Also Negotiates",
65
+ 507: "Insufficient Storage",
66
+ 508: "Loop Detected",
67
+ 510: "Not Extended",
68
+ 511: "Network Authentication Required",
69
+ }
70
+
71
+
72
+ class HTTPStatus:
73
+ # Informational (1xx)
74
+ CONTINUE = 100
75
+ SWITCHING_PROTOCOLS = 101
76
+ PROCESSING = 102
77
+ EARLY_HINTS = 103
78
+
79
+ # Success (2xx)
80
+ OK = 200
81
+ CREATED = 201
82
+ ACCEPTED = 202
83
+ NON_AUTHORITATIVE_INFORMATION = 203
84
+ NO_CONTENT = 204
85
+ RESET_CONTENT = 205
86
+ PARTIAL_CONTENT = 206
87
+ MULTI_STATUS = 207
88
+ ALREADY_REPORTED = 208
89
+ IM_USED = 226
90
+
91
+ # Redirection (3xx)
92
+ MULTIPLE_CHOICES = 300
93
+ MOVED_PERMANENTLY = 301
94
+ FOUND = 302
95
+ SEE_OTHER = 303
96
+ NOT_MODIFIED = 304
97
+ USE_PROXY = 305
98
+ TEMPORARY_REDIRECT = 307
99
+ PERMANENT_REDIRECT = 308
100
+
101
+ # Client Errors (4xx)
102
+ BAD_REQUEST = 400
103
+ UNAUTHORIZED = 401
104
+ PAYMENT_REQUIRED = 402
105
+ FORBIDDEN = 403
106
+ NOT_FOUND = 404
107
+ METHOD_NOT_ALLOWED = 405
108
+ NOT_ACCEPTABLE = 406
109
+ PROXY_AUTHENTICATION_REQUIRED = 407
110
+ REQUEST_TIMEOUT = 408
111
+ CONFLICT = 409
112
+ GONE = 410
113
+ LENGTH_REQUIRED = 411
114
+ PRECONDITION_FAILED = 412
115
+ PAYLOAD_TOO_LARGE = 413
116
+ URI_TOO_LONG = 414
117
+ UNSUPPORTED_MEDIA_TYPE = 415
118
+ RANGE_NOT_SATISFIABLE = 416
119
+ EXPECTATION_FAILED = 417
120
+ IM_A_TEAPOT = 418
121
+ MISDIRECTED_REQUEST = 421
122
+ UNPROCESSABLE_ENTITY = 422
123
+ LOCKED = 423
124
+ FAILED_DEPENDENCY = 424
125
+ TOO_EARLY = 425
126
+ UPGRADE_REQUIRED = 426
127
+ PRECONDITION_REQUIRED = 428
128
+ TOO_MANY_REQUESTS = 429
129
+ REQUEST_HEADER_FIELDS_TOO_LARGE = 431
130
+ UNAVAILABLE_FOR_LEGAL_REASONS = 451
131
+
132
+ # Server Errors (5xx)
133
+ INTERNAL_SERVER_ERROR = 500
134
+ NOT_IMPLEMENTED = 501
135
+ BAD_GATEWAY = 502
136
+ SERVICE_UNAVAILABLE = 503
137
+ GATEWAY_TIMEOUT = 504
138
+ HTTP_VERSION_NOT_SUPPORTED = 505
139
+ VARIANT_ALSO_NEGOTIATES = 506
140
+ INSUFFICIENT_STORAGE = 507
141
+ LOOP_DETECTED = 508
142
+ NOT_EXTENDED = 510
143
+ NETWORK_AUTHENTICATION_REQUIRED = 511
144
+
145
+ @staticmethod
146
+ def get_description(status_code: int) -> str:
147
+ """
148
+ Get the description of a status code
149
+ """
150
+
151
+ return status_descriptions.get(status_code, "")
@@ -0,0 +1,22 @@
1
+ from ..codes import HTTPStatus
2
+
3
+
4
+ class HTTPError(Exception):
5
+ """Exception raised for HTTP and CURL-related errors"""
6
+
7
+ def __init__(self, status_code: int, curl_error_code: int, curl_error_message: str):
8
+ self.status_code = status_code
9
+ self.curl_error_code = curl_error_code
10
+ self.curl_error_message = curl_error_message
11
+ self.is_curl_error = status_code == -1
12
+
13
+ if self.is_curl_error:
14
+ super().__init__(f"CURL {self.curl_error_code}: {self.curl_error_message}")
15
+ else:
16
+ short_description = HTTPStatus.get_description(self.status_code)
17
+
18
+ super().__init__(
19
+ self.status_code
20
+ if not short_description
21
+ else f"{self.status_code} - {short_description}"
22
+ )
redc/ext/redc.cpp ADDED
@@ -0,0 +1,362 @@
1
+ #include "redc.h"
2
+ #include "utils/curl_utils.h"
3
+ #include <iostream>
4
+ #include <stdexcept>
5
+
6
+ RedC::RedC(const long &buffer) {
7
+ {
8
+ acq_gil gil;
9
+ loop_ = nb::module_::import_("asyncio").attr("get_event_loop")();
10
+ call_soon_threadsafe_ = loop_.attr("call_soon_threadsafe");
11
+ }
12
+
13
+ static CurlGlobalInit g;
14
+
15
+ buffer_size_ = buffer;
16
+ multi_handle_ = curl_multi_init();
17
+
18
+ if (!multi_handle_) {
19
+ throw std::runtime_error("Failed to create CURL multi handle");
20
+ }
21
+
22
+ try {
23
+ running_ = true;
24
+ worker_thread_ = std::thread(&RedC::worker_loop, this);
25
+ } catch (...) {
26
+ curl_multi_cleanup(multi_handle_);
27
+ throw;
28
+ }
29
+ }
30
+
31
+ RedC::~RedC() {
32
+ this->close();
33
+ }
34
+
35
+ bool RedC::is_running() {
36
+ return running_;
37
+ }
38
+
39
+ void RedC::close() {
40
+ if (running_) {
41
+ running_ = false;
42
+
43
+ if (worker_thread_.joinable()) {
44
+ curl_multi_wakeup(multi_handle_);
45
+ worker_thread_.join();
46
+ }
47
+
48
+ cleanup();
49
+
50
+ curl_multi_cleanup(multi_handle_);
51
+ }
52
+ }
53
+
54
+ py_object RedC::request(const char *method, const char *url, const char *raw_data, const py_object &data,
55
+ const py_object &files, const py_object &headers, const long &timeout_ms,
56
+ const long &connect_timeout_ms, const bool &allow_redirect, const char *proxy_url,
57
+ const bool &verify, const char *ca_cert_path, const py_object &stream_callback,
58
+ const py_object &progress_callback, const bool &verbose) {
59
+ CHECK_RUNNING();
60
+
61
+ if (isNullOrEmpty(method) || isNullOrEmpty(url)) {
62
+ throw std::invalid_argument("method or url must be non-empty");
63
+ }
64
+
65
+ CURL *easy = curl_easy_init();
66
+ if (!easy) {
67
+ throw std::runtime_error("Failed to create CURL easy handle");
68
+ }
69
+
70
+ bool is_nobody = (strcmp(method, "HEAD") == 0 || strcmp(method, "OPTIONS") == 0);
71
+
72
+ try {
73
+ curl_easy_setopt(easy, CURLOPT_BUFFERSIZE, buffer_size_);
74
+ curl_easy_setopt(easy, CURLOPT_URL, url);
75
+ curl_easy_setopt(easy, CURLOPT_CUSTOMREQUEST, method);
76
+ curl_easy_setopt(easy, CURLOPT_NOSIGNAL, 1L);
77
+
78
+ curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, timeout_ms);
79
+
80
+ curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &RedC::header_callback);
81
+
82
+ if (verbose) {
83
+ curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
84
+ }
85
+
86
+ if (connect_timeout_ms > 0) {
87
+ curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, connect_timeout_ms);
88
+ }
89
+
90
+ if (is_nobody) {
91
+ curl_easy_setopt(easy, CURLOPT_NOBODY, 1L);
92
+ } else {
93
+ curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &RedC::write_callback);
94
+ }
95
+
96
+ if (allow_redirect) {
97
+ curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L);
98
+ curl_easy_setopt(easy, CURLOPT_MAXREDIRS, 30L);
99
+ }
100
+
101
+ if (!isNullOrEmpty(proxy_url)) {
102
+ curl_easy_setopt(easy, CURLOPT_PROXY, proxy_url);
103
+ }
104
+
105
+ if (!verify) {
106
+ curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0);
107
+ curl_easy_setopt(easy, CURLOPT_SSL_VERIFYHOST, 0);
108
+ } else if (!isNullOrEmpty(ca_cert_path)) {
109
+ curl_easy_setopt(easy, CURLOPT_CAINFO, ca_cert_path);
110
+ }
111
+
112
+ CurlMime curl_mime_;
113
+ if (!isNullOrEmpty(raw_data)) {
114
+ curl_easy_setopt(easy, CURLOPT_POSTFIELDS, raw_data);
115
+ curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)strlen(raw_data));
116
+ } else if (!data.is_none() || !files.is_none()) {
117
+ curl_mime_.mime = curl_mime_init(easy);
118
+
119
+ if (!data.is_none()) {
120
+ dict dict_obj;
121
+ try {
122
+ dict_obj = nb::cast<dict>(data);
123
+ } catch (...) {
124
+ throw std::runtime_error("Expected \"data\" to be a dictionary of strings");
125
+ }
126
+
127
+ for (auto const &it : dict_obj) {
128
+ curl_mimepart *part = curl_mime_addpart(curl_mime_.mime);
129
+ curl_mime_name(part, nb::str(it.first).c_str());
130
+ curl_mime_data(part, nb::str(it.second).c_str(), CURL_ZERO_TERMINATED);
131
+ }
132
+ }
133
+
134
+ if (!files.is_none()) {
135
+ dict dict_obj;
136
+ try {
137
+ dict_obj = nb::cast<dict>(files);
138
+ } catch (...) {
139
+ throw std::runtime_error("Expected \"files\" to be a dictionary of strings");
140
+ }
141
+
142
+ for (auto const &it : dict_obj) {
143
+ curl_mimepart *part = curl_mime_addpart(curl_mime_.mime);
144
+ curl_mime_name(part, nb::str(it.first).c_str());
145
+ curl_mime_filedata(part, nb::str(it.second).c_str());
146
+ }
147
+ }
148
+
149
+ curl_easy_setopt(easy, CURLOPT_MIMEPOST, curl_mime_.mime);
150
+ }
151
+
152
+ CurlSlist slist_headers;
153
+ if (!headers.is_none()) {
154
+ for (auto const &it : headers) {
155
+ slist_headers.slist = curl_slist_append(slist_headers.slist, nb::str(it).c_str());
156
+ }
157
+ curl_easy_setopt(easy, CURLOPT_HTTPHEADER, slist_headers.slist);
158
+ }
159
+
160
+ py_object future;
161
+ {
162
+ acq_gil gil;
163
+ future = loop_.attr("create_future")();
164
+ }
165
+
166
+ {
167
+ std::unique_lock<std::mutex> lock(mutex_);
168
+ auto &d = transfers_[easy];
169
+ lock.unlock();
170
+
171
+ d.future = future;
172
+ d.request_headers = std::move(slist_headers);
173
+ d.curl_mime_ = std::move(curl_mime_);
174
+
175
+ curl_easy_setopt(easy, CURLOPT_HEADERDATA, &d);
176
+ if (!is_nobody) {
177
+ curl_easy_setopt(easy, CURLOPT_WRITEDATA, &d);
178
+
179
+ if (!stream_callback.is_none()) {
180
+ d.stream_callback = stream_callback;
181
+ d.has_stream_callback = true;
182
+ }
183
+
184
+ if (!progress_callback.is_none()) {
185
+ d.progress_callback = progress_callback;
186
+ d.has_progress_callback = true;
187
+
188
+ curl_easy_setopt(easy, CURLOPT_XFERINFODATA, &d);
189
+ curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 0L);
190
+ curl_easy_setopt(easy, CURLOPT_XFERINFOFUNCTION, &RedC::progress_callback);
191
+ } else {
192
+ curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 1L);
193
+ }
194
+ }
195
+ }
196
+
197
+ queue_.enqueue(easy);
198
+
199
+ curl_multi_wakeup(multi_handle_); // thread-safe
200
+ return future;
201
+ } catch (...) {
202
+ curl_easy_cleanup(easy);
203
+ throw;
204
+ }
205
+ }
206
+
207
+ void RedC::worker_loop() {
208
+ while (running_) {
209
+ CURL *e;
210
+ if (queue_.try_dequeue(e)) {
211
+ CURLMcode res = curl_multi_add_handle(multi_handle_, e);
212
+ if (res != CURLM_OK) {
213
+ std::unique_lock<std::mutex> lock(mutex_);
214
+ auto it = transfers_.find(e);
215
+ if (it != transfers_.end()) {
216
+ Data data = std::move(it->second);
217
+ transfers_.erase(it);
218
+ lock.unlock();
219
+ {
220
+ acq_gil gil;
221
+ call_soon_threadsafe_(nb::cpp_function([data = std::move(data), res]() {
222
+ data.future.attr("set_result")(nb::make_tuple(-1, NULL, NULL, (int)res, curl_multi_strerror(res)));
223
+ }));
224
+ }
225
+ }
226
+ curl_easy_cleanup(e);
227
+ }
228
+ } else {
229
+ int numfds;
230
+ curl_multi_poll(multi_handle_, nullptr, 0, 30000, &numfds);
231
+ }
232
+
233
+ if (!running_) {
234
+ return;
235
+ }
236
+
237
+ curl_multi_perform(multi_handle_, &still_running_);
238
+
239
+ CURLMsg *msg;
240
+ int msgs_left;
241
+ while ((msg = curl_multi_info_read(multi_handle_, &msgs_left))) {
242
+ if (msg->msg == CURLMSG_DONE) {
243
+ std::unique_lock<std::mutex> lock(mutex_);
244
+ auto it = transfers_.find(msg->easy_handle);
245
+ if (it != transfers_.end()) {
246
+ Data data = std::move(it->second);
247
+ transfers_.erase(it);
248
+ lock.unlock();
249
+
250
+ {
251
+ acq_gil gil;
252
+
253
+ CURLcode res = msg->data.result;
254
+
255
+ /*
256
+ * Result is allways Tuple:
257
+
258
+ * 0: HTTP response status code.
259
+ * If the value is -1, it indicates a cURL error occurred
260
+ *
261
+ * 1: Response headers as bytes; can be null
262
+ *
263
+ * 2: The actual response data as bytes; can be null
264
+ *
265
+ * 3: cURL return code. This indicates the result code of the cURL operation.
266
+ * See: https://curl.se/libcurl/c/libcurl-errors.html
267
+ *
268
+ * 4: cURL error message string; can be null
269
+ */
270
+ py_object result;
271
+ if (res == CURLE_OK) {
272
+ short status_code = 0;
273
+ curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &status_code);
274
+ result = nb::make_tuple(status_code, py_bytes(data.headers.data(), data.headers.size()),
275
+ py_bytes(data.response.data(), data.response.size()), (int)res, NULL);
276
+ } else {
277
+ result = nb::make_tuple(-1, NULL, NULL, (int)res, curl_easy_strerror(res));
278
+ }
279
+
280
+ call_soon_threadsafe_(nb::cpp_function([data = std::move(data), result = std::move(result)]() {
281
+ data.future.attr("set_result")(std::move(result));
282
+ }));
283
+ }
284
+
285
+ curl_multi_remove_handle(multi_handle_, msg->easy_handle);
286
+ curl_easy_cleanup(msg->easy_handle);
287
+ }
288
+ }
289
+ }
290
+ }
291
+ }
292
+
293
+ void RedC::cleanup() {
294
+ std::unique_lock<std::mutex> lock(mutex_);
295
+ for (auto &[easy, data] : transfers_) {
296
+ {
297
+ acq_gil gil;
298
+ call_soon_threadsafe_(data.future.attr("cancel"));
299
+ }
300
+
301
+ curl_multi_remove_handle(multi_handle_, easy);
302
+ curl_easy_cleanup(easy);
303
+ }
304
+ transfers_.clear();
305
+ }
306
+
307
+ void RedC::CHECK_RUNNING() {
308
+ if (!running_) {
309
+ throw std::runtime_error("RedC can't be used after being closed");
310
+ }
311
+ }
312
+
313
+ size_t RedC::header_callback(char *buffer, size_t size, size_t nitems, Data *clientp) {
314
+ size_t total_size = size * nitems;
315
+ clientp->headers.insert(clientp->headers.end(), buffer, buffer + total_size);
316
+
317
+ return total_size;
318
+ }
319
+
320
+ size_t RedC::progress_callback(Data *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal,
321
+ curl_off_t ulnow) {
322
+ if (clientp->has_progress_callback) {
323
+ try {
324
+ acq_gil
325
+ gil; //TODO: this sometimes hangs on exit, which lead to other functions to block such as curl_multi_perform and worker_loop never exit
326
+ clientp->progress_callback(dltotal, dlnow, ultotal, ulnow);
327
+ } catch (const std::exception &e) {
328
+ std::cerr << "Error in progress_callback: " << e.what() << std::endl;
329
+ }
330
+ }
331
+
332
+ return 0;
333
+ }
334
+
335
+ size_t RedC::write_callback(char *data, size_t size, size_t nmemb, Data *clientp) {
336
+ size_t total_size = size * nmemb;
337
+
338
+ if (clientp->has_stream_callback) {
339
+ try {
340
+ acq_gil gil;
341
+ clientp->stream_callback(py_bytes(data, total_size), total_size);
342
+ } catch (const std::exception &e) {
343
+ std::cerr << "Error in stream_callback: " << e.what() << std::endl;
344
+ }
345
+ } else {
346
+ clientp->response.insert(clientp->response.end(), data, data + total_size);
347
+ }
348
+
349
+ return total_size;
350
+ }
351
+
352
+ NB_MODULE(redc_ext, m) {
353
+ nb::class_<RedC>(m, "RedC")
354
+ .def(nb::init<const long &>())
355
+ .def("is_running", &RedC::is_running)
356
+ .def("request", &RedC::request, arg("method"), arg("url"), arg("raw_data") = "", arg("data") = nb::none(),
357
+ arg("files") = nb::none(), arg("headers") = nb::none(), arg("timeout_ms") = 60 * 1000,
358
+ arg("connect_timeout_ms") = 0, arg("allow_redirect") = true, arg("proxy_url") = "", arg("verify") = true,
359
+ arg("ca_cert_path") = "", arg("stream_callback") = nb::none(), arg("progress_callback") = nb::none(),
360
+ arg("verbose") = false)
361
+ .def("close", &RedC::close);
362
+ }
redc/ext/redc.h ADDED
@@ -0,0 +1,91 @@
1
+ #ifndef REDC_H
2
+ #define REDC_H
3
+
4
+ #include <atomic>
5
+ #include <cstring>
6
+ #include <map>
7
+ #include <mutex>
8
+ #include <thread>
9
+ #include <vector>
10
+
11
+ #include <curl/curl.h>
12
+
13
+ #include <nanobind/nanobind.h>
14
+ #include <nanobind/stl/string.h>
15
+ #include <nanobind/stl/tuple.h>
16
+
17
+ #include "utils/concurrentqueue.h"
18
+ #include "utils/curl_utils.h"
19
+
20
+ namespace nb = nanobind;
21
+ using namespace nb::literals;
22
+
23
+ using acq_gil = nb::gil_scoped_acquire;
24
+ using rel_gil = nb::gil_scoped_release;
25
+
26
+ using py_object = nb::object;
27
+ using py_bytes = nb::bytes;
28
+ using arg = nb::arg;
29
+ using dict = nb::dict;
30
+
31
+ bool isNullOrEmpty(const char *str) {
32
+ return !str || !*str;
33
+ }
34
+
35
+ struct Data {
36
+ py_object future;
37
+ py_object loop;
38
+ py_object stream_callback{nb::none()};
39
+ py_object progress_callback{nb::none()};
40
+
41
+ bool has_stream_callback{false};
42
+ bool has_progress_callback{false};
43
+
44
+ std::vector<char> headers;
45
+ CurlSlist request_headers;
46
+ CurlMime curl_mime_;
47
+
48
+ std::vector<char> response;
49
+ };
50
+
51
+ class RedC {
52
+ public:
53
+ RedC(const long &buffer = 16384);
54
+ ~RedC();
55
+
56
+ bool is_running();
57
+ void close();
58
+
59
+ py_object request(const char *method, const char *url, const char *raw_data = "", const py_object &data = nb::none(),
60
+ const py_object &files = nb::none(), const py_object &headers = nb::none(),
61
+ const long &timeout_ms = 60 * 1000, const long &connect_timeout_ms = 0,
62
+ const bool &allow_redirect = true, const char *proxy_url = "", const bool &verify = true,
63
+ const char *ca_cert_path = "", const py_object &stream_callback = nb::none(),
64
+ const py_object &progress_callback = nb::none(), const bool &verbose = false);
65
+
66
+ private:
67
+ int still_running_{0};
68
+ long buffer_size_;
69
+ py_object loop_;
70
+ py_object call_soon_threadsafe_;
71
+
72
+ CURLM *multi_handle_;
73
+
74
+ std::map<CURL *, Data> transfers_;
75
+ std::mutex mutex_;
76
+ std::thread worker_thread_;
77
+ std::atomic<bool> running_{false};
78
+
79
+ moodycamel::ConcurrentQueue<CURL *> queue_;
80
+
81
+ void worker_loop();
82
+ void cleanup();
83
+ void CHECK_RUNNING();
84
+
85
+ static size_t header_callback(char *buffer, size_t size, size_t nitems, Data *clientp);
86
+ static size_t progress_callback(Data *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal,
87
+ curl_off_t ulnow);
88
+ static size_t write_callback(char *data, size_t size, size_t nmemb, Data *clientp);
89
+ };
90
+
91
+ #endif // REDC_H