1 | // SuperTux |
2 | // Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de> |
3 | // 2014 Ingo Ruhnke <grumbel@gmail.com> |
4 | // |
5 | // This program is free software: you can redistribute it and/or modify |
6 | // it under the terms of the GNU General Public License as published by |
7 | // the Free Software Foundation, either version 3 of the License, or |
8 | // (at your option) any later version. |
9 | // |
10 | // This program is distributed in the hope that it will be useful, |
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | // GNU General Public License for more details. |
14 | // |
15 | // You should have received a copy of the GNU General Public License |
16 | // along with this program. If not, see <http://www.gnu.org/licenses/>. |
17 | |
18 | #include "addon/downloader.hpp" |
19 | |
20 | #include <algorithm> |
21 | #include <array> |
22 | #include <assert.h> |
23 | #include <memory> |
24 | #include <physfs.h> |
25 | #include <sstream> |
26 | #include <stdexcept> |
27 | #include <version.h> |
28 | |
29 | #include "util/log.hpp" |
30 | |
31 | namespace { |
32 | |
33 | size_t my_curl_string_append(void* ptr, size_t size, size_t nmemb, void* userdata) |
34 | { |
35 | std::string& s = *static_cast<std::string*>(userdata); |
36 | std::string buf(static_cast<char*>(ptr), size * nmemb); |
37 | s += buf; |
38 | log_debug << "read " << size * nmemb << " bytes of data..." << std::endl; |
39 | return size * nmemb; |
40 | } |
41 | |
42 | size_t my_curl_physfs_write(void* ptr, size_t size, size_t nmemb, void* userdata) |
43 | { |
44 | PHYSFS_file* f = static_cast<PHYSFS_file*>(userdata); |
45 | PHYSFS_sint64 written = PHYSFS_writeBytes(f, ptr, size * nmemb); |
46 | log_debug << "read " << size * nmemb << " bytes of data..." << std::endl; |
47 | if (written < 0) |
48 | { |
49 | return 0; |
50 | } |
51 | else |
52 | { |
53 | return static_cast<size_t>(written); |
54 | } |
55 | } |
56 | |
57 | } // namespace |
58 | |
59 | void |
60 | TransferStatus::abort() |
61 | { |
62 | m_downloader.abort(id); |
63 | } |
64 | |
65 | void |
66 | TransferStatus::update() |
67 | { |
68 | m_downloader.update(); |
69 | } |
70 | |
71 | class Transfer final |
72 | { |
73 | private: |
74 | Downloader& m_downloader; |
75 | TransferId m_id; |
76 | |
77 | std::string m_url; |
78 | CURL* m_handle; |
79 | std::array<char, CURL_ERROR_SIZE> m_error_buffer; |
80 | |
81 | TransferStatusPtr m_status; |
82 | std::unique_ptr<PHYSFS_file, int(*)(PHYSFS_File*)> m_fout; |
83 | |
84 | public: |
85 | Transfer(Downloader& downloader, TransferId id, |
86 | const std::string& url, |
87 | const std::string& outfile) : |
88 | m_downloader(downloader), |
89 | m_id(id), |
90 | m_url(url), |
91 | m_handle(), |
92 | m_error_buffer({{'\0'}}), |
93 | m_status(new TransferStatus(m_downloader, id)), |
94 | m_fout(PHYSFS_openWrite(outfile.c_str()), PHYSFS_close) |
95 | { |
96 | if (!m_fout) |
97 | { |
98 | std::ostringstream out; |
99 | out << "PHYSFS_openRead() failed: " << PHYSFS_getLastErrorCode(); |
100 | throw std::runtime_error(out.str()); |
101 | } |
102 | |
103 | m_handle = curl_easy_init(); |
104 | if (!m_handle) |
105 | { |
106 | throw std::runtime_error("curl_easy_init() failed" ); |
107 | } |
108 | else |
109 | { |
110 | curl_easy_setopt(m_handle, CURLOPT_URL, url.c_str()); |
111 | curl_easy_setopt(m_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL" ); |
112 | |
113 | curl_easy_setopt(m_handle, CURLOPT_WRITEDATA, this); |
114 | curl_easy_setopt(m_handle, CURLOPT_WRITEFUNCTION, &Transfer::on_data_wrap); |
115 | |
116 | curl_easy_setopt(m_handle, CURLOPT_ERRORBUFFER, m_error_buffer.data()); |
117 | curl_easy_setopt(m_handle, CURLOPT_NOSIGNAL, 1); |
118 | curl_easy_setopt(m_handle, CURLOPT_FAILONERROR, 1); |
119 | curl_easy_setopt(m_handle, CURLOPT_FOLLOWLOCATION, 1); |
120 | |
121 | curl_easy_setopt(m_handle, CURLOPT_NOPROGRESS, 0); |
122 | curl_easy_setopt(m_handle, CURLOPT_PROGRESSDATA, this); |
123 | curl_easy_setopt(m_handle, CURLOPT_PROGRESSFUNCTION, &Transfer::on_progress_wrap); |
124 | } |
125 | } |
126 | |
127 | ~Transfer() |
128 | { |
129 | curl_easy_cleanup(m_handle); |
130 | } |
131 | |
132 | TransferStatusPtr get_status() const |
133 | { |
134 | return m_status; |
135 | } |
136 | |
137 | const char* get_error_buffer() const |
138 | { |
139 | return m_error_buffer.data(); |
140 | } |
141 | |
142 | TransferId get_id() const |
143 | { |
144 | return m_id; |
145 | } |
146 | |
147 | CURL* get_curl_handle() const |
148 | { |
149 | return m_handle; |
150 | } |
151 | |
152 | std::string get_url() const |
153 | { |
154 | return m_url; |
155 | } |
156 | |
157 | size_t on_data(void* ptr, size_t size, size_t nmemb) |
158 | { |
159 | PHYSFS_writeBytes(m_fout.get(), ptr, size * nmemb); |
160 | return size * nmemb; |
161 | } |
162 | |
163 | int on_progress(double dltotal, double dlnow, |
164 | double ultotal, double ulnow) |
165 | { |
166 | m_status->dltotal = static_cast<int>(dltotal); |
167 | m_status->dlnow = static_cast<int>(dlnow); |
168 | |
169 | m_status->ultotal = static_cast<int>(ultotal); |
170 | m_status->ulnow = static_cast<int>(ulnow); |
171 | |
172 | return 0; |
173 | } |
174 | |
175 | private: |
176 | static size_t on_data_wrap(char* ptr, size_t size, size_t nmemb, void* userdata) |
177 | { |
178 | return static_cast<Transfer*>(userdata)->on_data(ptr, size, nmemb); |
179 | } |
180 | |
181 | static int on_progress_wrap(void* userdata, |
182 | double dltotal, double dlnow, |
183 | double ultotal, double ulnow) |
184 | { |
185 | return static_cast<Transfer*>(userdata)->on_progress(dltotal, dlnow, ultotal, ulnow); |
186 | } |
187 | |
188 | private: |
189 | Transfer(const Transfer&) = delete; |
190 | Transfer& operator=(const Transfer&) = delete; |
191 | }; |
192 | |
193 | Downloader::Downloader() : |
194 | m_multi_handle(), |
195 | m_transfers(), |
196 | m_next_transfer_id(1) |
197 | { |
198 | curl_global_init(CURL_GLOBAL_ALL); |
199 | m_multi_handle = curl_multi_init(); |
200 | if (!m_multi_handle) |
201 | { |
202 | throw std::runtime_error("curl_multi_init() failed" ); |
203 | } |
204 | } |
205 | |
206 | Downloader::~Downloader() |
207 | { |
208 | for (auto& transfer : m_transfers) |
209 | { |
210 | curl_multi_remove_handle(m_multi_handle, transfer->get_curl_handle()); |
211 | } |
212 | m_transfers.clear(); |
213 | |
214 | curl_multi_cleanup(m_multi_handle); |
215 | curl_global_cleanup(); |
216 | } |
217 | |
218 | void |
219 | Downloader::download(const std::string& url, |
220 | size_t (*write_func)(void* ptr, size_t size, size_t nmemb, void* userdata), |
221 | void* userdata) |
222 | { |
223 | log_info << "Downloading " << url << std::endl; |
224 | |
225 | char error_buffer[CURL_ERROR_SIZE+1]; |
226 | |
227 | CURL* curl_handle = curl_easy_init(); |
228 | curl_easy_setopt(curl_handle, CURLOPT_URL, url.c_str()); |
229 | curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL" ); |
230 | curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_func); |
231 | curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, userdata); |
232 | curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer); |
233 | curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1); |
234 | curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1); |
235 | curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1); |
236 | curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1); |
237 | CURLcode result = curl_easy_perform(curl_handle); |
238 | curl_easy_cleanup(curl_handle); |
239 | |
240 | if (result != CURLE_OK) |
241 | { |
242 | std::string why = error_buffer[0] ? error_buffer : "unhandled error" ; |
243 | throw std::runtime_error(url + ": download failed: " + why); |
244 | } |
245 | } |
246 | |
247 | std::string |
248 | Downloader::download(const std::string& url) |
249 | { |
250 | std::string result; |
251 | download(url, my_curl_string_append, &result); |
252 | return result; |
253 | } |
254 | |
255 | void |
256 | Downloader::download(const std::string& url, const std::string& filename) |
257 | { |
258 | log_info << "download: " << url << " to " << filename << std::endl; |
259 | std::unique_ptr<PHYSFS_file, int(*)(PHYSFS_File*)> fout(PHYSFS_openWrite(filename.c_str()), |
260 | PHYSFS_close); |
261 | download(url, my_curl_physfs_write, fout.get()); |
262 | } |
263 | |
264 | void |
265 | Downloader::abort(TransferId id) |
266 | { |
267 | auto it = std::find_if(m_transfers.begin(), m_transfers.end(), |
268 | [&id](const std::unique_ptr<Transfer>& rhs) |
269 | { |
270 | return id == rhs->get_id(); |
271 | }); |
272 | if (it == m_transfers.end()) |
273 | { |
274 | log_warning << "transfer not found: " << id << std::endl; |
275 | } |
276 | else |
277 | { |
278 | TransferStatusPtr status = (*it)->get_status(); |
279 | |
280 | curl_multi_remove_handle(m_multi_handle, (*it)->get_curl_handle()); |
281 | m_transfers.erase(it); |
282 | |
283 | for (auto& callback : status->callbacks) |
284 | { |
285 | try |
286 | { |
287 | callback(false); |
288 | } |
289 | catch(const std::exception& err) |
290 | { |
291 | log_warning << "Illegal exception in Downloader: " << err.what() << std::endl; |
292 | } |
293 | } |
294 | } |
295 | } |
296 | |
297 | void |
298 | Downloader::update() |
299 | { |
300 | // read data from the network |
301 | CURLMcode ret; |
302 | int running_handles; |
303 | while ((ret = curl_multi_perform(m_multi_handle, &running_handles)) == CURLM_CALL_MULTI_PERFORM) |
304 | { |
305 | log_debug << "updating" << std::endl; |
306 | } |
307 | |
308 | // check if any downloads got finished |
309 | int msgs_in_queue; |
310 | CURLMsg* msg; |
311 | while ((msg = curl_multi_info_read(m_multi_handle, &msgs_in_queue))) |
312 | { |
313 | switch (msg->msg) |
314 | { |
315 | case CURLMSG_DONE: |
316 | { |
317 | CURLcode resultfromcurl = msg->data.result; |
318 | log_info << "Download completed with " << resultfromcurl << std::endl; |
319 | curl_multi_remove_handle(m_multi_handle, msg->easy_handle); |
320 | |
321 | auto it = std::find_if(m_transfers.begin(), m_transfers.end(), |
322 | [&msg](const std::unique_ptr<Transfer>& rhs) { |
323 | return rhs->get_curl_handle() == msg->easy_handle; |
324 | }); |
325 | assert(it != m_transfers.end()); |
326 | TransferStatusPtr status = (*it)->get_status(); |
327 | status->error_msg = (*it)->get_error_buffer(); |
328 | m_transfers.erase(it); |
329 | |
330 | if (resultfromcurl == CURLE_OK) |
331 | { |
332 | bool success = true; |
333 | for (auto& callback : status->callbacks) |
334 | { |
335 | try |
336 | { |
337 | callback(success); |
338 | } |
339 | catch(const std::exception& err) |
340 | { |
341 | success = false; |
342 | log_warning << "Exception in Downloader: " << err.what() << std::endl; |
343 | status->error_msg = err.what(); |
344 | } |
345 | } |
346 | } |
347 | else |
348 | { |
349 | log_warning << "Error: " << curl_easy_strerror(resultfromcurl) << std::endl; |
350 | for (auto& callback : status->callbacks) |
351 | { |
352 | try |
353 | { |
354 | callback(false); |
355 | } |
356 | catch(const std::exception& err) |
357 | { |
358 | log_warning << "Illegal exception in Downloader: " << err.what() << std::endl; |
359 | } |
360 | } |
361 | } |
362 | } |
363 | break; |
364 | |
365 | default: |
366 | log_warning << "unhandled cURL message: " << msg->msg << std::endl; |
367 | break; |
368 | } |
369 | } |
370 | } |
371 | |
372 | TransferStatusPtr |
373 | Downloader::request_download(const std::string& url, const std::string& outfile) |
374 | { |
375 | log_info << "request_download: " << url << std::endl; |
376 | auto transfer = std::make_unique<Transfer>(*this, m_next_transfer_id++, url, outfile); |
377 | curl_multi_add_handle(m_multi_handle, transfer->get_curl_handle()); |
378 | m_transfers.push_back(std::move(transfer)); |
379 | return m_transfers.back()->get_status(); |
380 | } |
381 | |
382 | /* EOF */ |
383 | |