b923813f5be15a2840549dc49e1e04f8ac2c207d
[supertux.git] / src / addon / downloader.cpp
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 <assert.h>
22 #include <memory>
23 #include <physfs.h>
24 #include <sstream>
25 #include <stdexcept>
26
27 #include "util/log.hpp"
28 #include "version.h"
29
30 namespace {
31
32 size_t my_curl_string_append(void* ptr, size_t size, size_t nmemb, void* userdata)
33 {
34   std::string& s = *static_cast<std::string*>(userdata);
35   std::string buf(static_cast<char*>(ptr), size * nmemb);
36   s += buf;
37   log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
38   return size * nmemb;
39 }
40
41 size_t my_curl_physfs_write(void* ptr, size_t size, size_t nmemb, void* userdata)
42 {
43   PHYSFS_file* f = static_cast<PHYSFS_file*>(userdata);
44   PHYSFS_sint64 written = PHYSFS_write(f, ptr, size, nmemb);
45   log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
46   return size * written;
47 }
48
49 } // namespace
50
51 void
52 TransferStatus::update()
53 {
54   m_downloader.update();
55 }
56
57 class Transfer
58 {
59 private:
60   Downloader& m_downloader;
61   TransferId m_id;
62
63   std::string m_url;
64   CURL* m_handle;
65   std::array<char, CURL_ERROR_SIZE> m_error_buffer;
66
67   TransferStatusPtr m_status;
68   std::unique_ptr<PHYSFS_file, int(*)(PHYSFS_File*)> m_fout;
69
70 public:
71   Transfer(Downloader& downloader, TransferId id,
72            const std::string& url,
73            const std::string& outfile) :
74     m_downloader(downloader),
75     m_id(id),
76     m_url(url),
77     m_handle(),
78     m_error_buffer({'\0'}),
79     m_status(new TransferStatus(downloader, id)),
80     m_fout(PHYSFS_openWrite(outfile.c_str()), PHYSFS_close)
81   {
82     if (!m_fout)
83     {
84       std::ostringstream out;
85       out << "PHYSFS_openRead() failed: " << PHYSFS_getLastError();
86       throw std::runtime_error(out.str());
87     }
88
89     m_handle = curl_easy_init();
90     if (!m_handle)
91     {
92       throw std::runtime_error("curl_easy_init() failed");
93     }
94     else
95     {
96       curl_easy_setopt(m_handle, CURLOPT_URL, url.c_str());
97       curl_easy_setopt(m_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
98
99       curl_easy_setopt(m_handle, CURLOPT_WRITEDATA, this);
100       curl_easy_setopt(m_handle, CURLOPT_WRITEFUNCTION, &Transfer::on_data_wrap);
101
102       curl_easy_setopt(m_handle, CURLOPT_ERRORBUFFER, m_error_buffer.data());
103       curl_easy_setopt(m_handle, CURLOPT_NOSIGNAL, 1);
104       curl_easy_setopt(m_handle, CURLOPT_FAILONERROR, 1);
105       curl_easy_setopt(m_handle, CURLOPT_FOLLOWLOCATION, 1);
106
107       curl_easy_setopt(m_handle, CURLOPT_NOPROGRESS, 0);
108       curl_easy_setopt(m_handle, CURLOPT_XFERINFODATA, this);
109       curl_easy_setopt(m_handle, CURLOPT_XFERINFOFUNCTION, &Transfer::on_progress_wrap);
110     }
111   }
112
113   ~Transfer()
114   {
115     curl_easy_cleanup(m_handle);
116   }
117
118   TransferStatusPtr get_status() const
119   {
120     return m_status;
121   }
122
123   const char* get_error_buffer() const
124   {
125     return m_error_buffer.data();
126   }
127
128   TransferId get_id() const
129   {
130     return m_id;
131   }
132
133   CURL* get_curl_handle() const
134   {
135     return m_handle;
136   }
137
138   std::string get_url() const
139   {
140     return m_url;
141   }
142
143   size_t on_data(void* ptr, size_t size, size_t nmemb)
144   {
145     PHYSFS_write(m_fout.get(), ptr, size, nmemb);
146     return size * nmemb;
147   }
148
149   void on_progress(curl_off_t dltotal, curl_off_t dlnow,
150                    curl_off_t ultotal, curl_off_t ulnow)
151   {
152     m_status->dltotal = dltotal;
153     m_status->dlnow = dlnow;
154
155     m_status->ultotal = ultotal;
156     m_status->ulnow = ulnow;
157   }
158
159 private:
160   static size_t on_data_wrap(char* ptr, size_t size, size_t nmemb, void* userdata)
161   {
162     return static_cast<Transfer*>(userdata)->on_data(ptr, size, nmemb);
163   }
164
165   static void on_progress_wrap(void* userdata,
166                                curl_off_t dltotal, curl_off_t dlnow,
167                                curl_off_t ultotal, curl_off_t ulnow)
168   {
169     return static_cast<Transfer*>(userdata)->on_progress(dltotal, dlnow, ultotal, ulnow);
170   }
171
172 private:
173   Transfer(const Transfer&) = delete;
174   Transfer& operator=(const Transfer&) = delete;
175 };
176
177 Downloader::Downloader() :
178   m_multi_handle(),
179   m_transfers(),
180   m_next_transfer_id(1)
181 {
182   curl_global_init(CURL_GLOBAL_ALL);
183   m_multi_handle = curl_multi_init();
184   if (!m_multi_handle)
185   {
186     throw std::runtime_error("curl_multi_init() failed");
187   }
188 }
189
190 Downloader::~Downloader()
191 {
192   for(auto& transfer : m_transfers)
193   {
194     curl_multi_remove_handle(m_multi_handle, transfer->get_curl_handle());
195   }
196   m_transfers.clear();
197
198   curl_multi_cleanup(m_multi_handle);
199   curl_global_cleanup();
200 }
201
202 void
203 Downloader::download(const std::string& url,
204                      size_t (*write_func)(void* ptr, size_t size, size_t nmemb, void* userdata),
205                      void* userdata)
206 {
207   log_info << "Downloading " << url << std::endl;
208
209   char error_buffer[CURL_ERROR_SIZE+1];
210
211   CURL* curl_handle = curl_easy_init();
212   curl_easy_setopt(curl_handle, CURLOPT_URL, url.c_str());
213   curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
214   curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_func);
215   curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, userdata);
216   curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
217   curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
218   curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
219   curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
220   curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
221   CURLcode result = curl_easy_perform(curl_handle);
222   curl_easy_cleanup(curl_handle);
223
224   if (result != CURLE_OK)
225   {
226     std::string why = error_buffer[0] ? error_buffer : "unhandled error";
227     throw std::runtime_error(url + ": download failed: " + why);
228   }
229 }
230
231 std::string
232 Downloader::download(const std::string& url)
233 {
234   std::string result;
235   download(url, my_curl_string_append, &result);
236   return result;
237 }
238
239 void
240 Downloader::download(const std::string& url, const std::string& filename)
241 {
242   std::unique_ptr<PHYSFS_file, int(*)(PHYSFS_File*)> fout(PHYSFS_openWrite(filename.c_str()),
243                                                           PHYSFS_close);
244   download(url, my_curl_physfs_write, fout.get());
245 }
246
247 void
248 Downloader::abort(TransferId id)
249 {
250   auto it = std::find_if(m_transfers.begin(), m_transfers.end(),
251                          [&id](const std::unique_ptr<Transfer>& rhs)
252                          {
253                            return id == rhs->get_id();
254                          });
255   if (it == m_transfers.end())
256   {
257     log_warning << "transfer not found: " << id << std::endl;
258   }
259   else
260   {
261     curl_multi_remove_handle(m_multi_handle, (*it)->get_curl_handle());
262     m_transfers.erase(it);
263   }
264 }
265
266 void
267 Downloader::update()
268 {
269   // read data from the network
270   CURLMcode ret;
271   int running_handles;
272   while((ret = curl_multi_perform(m_multi_handle, &running_handles)) == CURLM_CALL_MULTI_PERFORM)
273   {
274     log_debug << "updating" << std::endl;
275   }
276
277   // check if any downloads got finished
278   int msgs_in_queue;
279   CURLMsg* msg;
280   while ((msg = curl_multi_info_read(m_multi_handle, &msgs_in_queue)))
281   {
282     switch(msg->msg)
283     {
284       case CURLMSG_DONE:
285         {
286           log_info << "Download completed with " << msg->data.result << std::endl;
287           curl_multi_remove_handle(m_multi_handle, msg->easy_handle);
288
289           auto it = std::find_if(m_transfers.begin(), m_transfers.end(),
290                                  [&msg](const std::unique_ptr<Transfer>& rhs) {
291                                    return rhs->get_curl_handle() == msg->easy_handle;
292                                  });
293           assert(it != m_transfers.end());
294           TransferStatusPtr status = (*it)->get_status();
295           status->error_msg = (*it)->get_error_buffer();
296           m_transfers.erase(it);
297
298           if (msg->data.result == CURLE_OK)
299           {
300             bool success = true;
301             for(auto& callback : status->callbacks)
302             {
303               try
304               {
305                 callback(success);
306               }
307               catch(const std::exception& err)
308               {
309                 success = false;
310                 log_warning << "Exception in Downloader: " << err.what() << std::endl;
311                 status->error_msg = err.what();
312               }
313             }
314           }
315           else
316           {
317             log_warning << "Error: " << curl_easy_strerror(msg->data.result) << std::endl;
318             for(auto& callback : status->callbacks)
319             {
320               try
321               {
322                 callback(false);
323               }
324               catch(const std::exception& err)
325               {
326                 log_warning << "Illegal exception in Downloader: " << err.what() << std::endl;
327               }
328             }
329           }
330         }
331         break;
332
333       default:
334         log_warning << "unhandled cURL message: " << msg->msg << std::endl;
335         break;
336     }
337   }
338 }
339
340 TransferStatusPtr
341 Downloader::request_download(const std::string& url, const std::string& outfile)
342 {
343   std::unique_ptr<Transfer> transfer(new Transfer(*this, m_next_transfer_id++, url, outfile));
344   curl_multi_add_handle(m_multi_handle, transfer->get_curl_handle());
345   m_transfers.push_back(std::move(transfer));
346   return m_transfers.back()->get_status();
347 }
348
349 /* EOF */