Hooked up progress reporting into the AddonDialog, non-blocking downloads are working now
[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 <memory>
22 #include <physfs.h>
23 #include <stdexcept>
24
25 #include "util/log.hpp"
26 #include "version.h"
27
28 namespace {
29
30 size_t my_curl_string_append(void* ptr, size_t size, size_t nmemb, void* userdata)
31 {
32   std::string& s = *static_cast<std::string*>(userdata);
33   std::string buf(static_cast<char*>(ptr), size * nmemb);
34   s += buf;
35   log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
36   return size * nmemb;
37 }
38
39 size_t my_curl_physfs_write(void* ptr, size_t size, size_t nmemb, void* userdata)
40 {
41   PHYSFS_file* f = static_cast<PHYSFS_file*>(userdata);
42   PHYSFS_sint64 written = PHYSFS_write(f, ptr, size, nmemb);
43   log_debug << "read " << size * nmemb << " bytes of data..." << std::endl;
44   return size * written;
45 }
46
47 } // namespace
48
49 class Transfer
50 {
51 private:
52   Downloader& m_downloader;
53   TransferId m_id;
54
55   std::string m_url;
56   CURL* m_handle;
57   std::array<char, CURL_ERROR_SIZE> m_error_buffer;
58
59   TransferStatusPtr m_status;
60
61 public:
62   Transfer(Downloader& downloader, TransferId id, const std::string& url) :
63     m_downloader(downloader),
64     m_id(id),
65     m_url(url),
66     m_handle(),
67     m_error_buffer(),
68     m_status(new TransferStatus(id))
69   {
70     m_handle = curl_easy_init();
71     if (!m_handle)
72     {
73       throw std::runtime_error("curl_easy_init() failed");
74     }
75     else
76     {
77       curl_easy_setopt(m_handle, CURLOPT_URL, url.c_str());
78       curl_easy_setopt(m_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
79
80       curl_easy_setopt(m_handle, CURLOPT_WRITEDATA, this);
81       curl_easy_setopt(m_handle, CURLOPT_WRITEFUNCTION, &Transfer::on_data_wrap);
82
83       curl_easy_setopt(m_handle, CURLOPT_ERRORBUFFER, m_error_buffer.data());
84       curl_easy_setopt(m_handle, CURLOPT_NOSIGNAL, 1);
85       curl_easy_setopt(m_handle, CURLOPT_FAILONERROR, 1);
86       curl_easy_setopt(m_handle, CURLOPT_FOLLOWLOCATION, 1);
87
88       curl_easy_setopt(m_handle, CURLOPT_NOPROGRESS, 0);
89       curl_easy_setopt(m_handle, CURLOPT_XFERINFODATA, this);
90       curl_easy_setopt(m_handle, CURLOPT_XFERINFOFUNCTION, &Transfer::on_progress_wrap);
91     }
92   }
93
94   ~Transfer()
95   {
96     curl_easy_cleanup(m_handle);
97   }
98
99   TransferStatusPtr get_status() const
100   {
101     return m_status;
102   }
103
104   TransferId get_id() const
105   {
106     return m_id;
107   }
108
109   CURL* get_curl_handle() const
110   {
111     return m_handle;
112   }
113
114   std::string get_url() const
115   {
116     return m_url;
117   }
118
119   size_t on_data(void* ptr, size_t size, size_t nmemb)
120   {
121     return size * nmemb;
122   }
123
124   void on_progress(curl_off_t dltotal, curl_off_t dlnow,
125                    curl_off_t ultotal, curl_off_t ulnow)
126   {
127     log_info << "progress: " << dlnow << "/" << dltotal << std::endl;
128     m_status->dltotal = dltotal;
129     m_status->dlnow = dlnow;
130
131     m_status->ultotal = ultotal;
132     m_status->ulnow = ulnow;
133   }
134
135 private:
136   static size_t on_data_wrap(char* ptr, size_t size, size_t nmemb, void* userdata)
137   {
138     return static_cast<Transfer*>(userdata)->on_data(ptr, size, nmemb);
139   }
140
141   static void on_progress_wrap(void* userdata,
142                                curl_off_t dltotal, curl_off_t dlnow,
143                                curl_off_t ultotal, curl_off_t ulnow)
144   {
145     return static_cast<Transfer*>(userdata)->on_progress(dltotal, dlnow, ultotal, ulnow);
146   }
147
148 private:
149   Transfer(const Transfer&) = delete;
150   Transfer& operator=(const Transfer&) = delete;
151 };
152
153 Downloader::Downloader() :
154   m_multi_handle(),
155   m_transfers(),
156   m_next_transfer_id(1)
157 {
158   curl_global_init(CURL_GLOBAL_ALL);
159   m_multi_handle = curl_multi_init();
160   if (!m_multi_handle)
161   {
162     throw std::runtime_error("curl_multi_init() failed");
163   }
164 }
165
166 Downloader::~Downloader()
167 {
168   for(auto& transfer : m_transfers)
169   {
170     curl_multi_remove_handle(m_multi_handle, transfer->get_curl_handle());
171   }
172   m_transfers.clear();
173
174   curl_multi_cleanup(m_multi_handle);
175   curl_global_cleanup();
176 }
177
178 void
179 Downloader::download(const std::string& url,
180                      size_t (*write_func)(void* ptr, size_t size, size_t nmemb, void* userdata),
181                      void* userdata)
182 {
183   log_info << "Downloading " << url << std::endl;
184
185   char error_buffer[CURL_ERROR_SIZE+1];
186
187   CURL* curl_handle = curl_easy_init();
188   curl_easy_setopt(curl_handle, CURLOPT_URL, url.c_str());
189   curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
190   curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_func);
191   curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, userdata);
192   curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
193   curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
194   curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
195   curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
196   curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
197   CURLcode result = curl_easy_perform(curl_handle);
198   curl_easy_cleanup(curl_handle);
199
200   if (result != CURLE_OK)
201   {
202     std::string why = error_buffer[0] ? error_buffer : "unhandled error";
203     throw std::runtime_error(url + ": download failed: " + why);
204   }
205 }
206
207 std::string
208 Downloader::download(const std::string& url)
209 {
210   std::string result;
211   download(url, my_curl_string_append, &result);
212   return result;
213 }
214
215 void
216 Downloader::download(const std::string& url, const std::string& filename)
217 {
218   std::unique_ptr<PHYSFS_file, int(*)(PHYSFS_File*)> fout(PHYSFS_openWrite(filename.c_str()),
219                                                           PHYSFS_close);
220   download(url, my_curl_physfs_write, fout.get());
221 }
222
223 void
224 Downloader::abort(TransferId id)
225 {
226   auto it = std::find_if(m_transfers.begin(), m_transfers.end(),
227                          [&id](const std::unique_ptr<Transfer>& rhs)
228                          {
229                            return id == rhs->get_id();
230                          });
231   if (it == m_transfers.end())
232   {
233     log_warning << "transfer not found: " << id << std::endl;
234   }
235   else
236   {
237     curl_multi_remove_handle(m_multi_handle, (*it)->get_curl_handle());
238     m_transfers.erase(it);
239   }
240 }
241
242 void
243 Downloader::update()
244 {
245   // read data from the network
246   CURLMcode ret;
247   int running_handles;
248   while((ret = curl_multi_perform(m_multi_handle, &running_handles)) == CURLM_CALL_MULTI_PERFORM)
249   {
250     log_debug << "updating" << std::endl;
251   }
252
253   // check if any downloads got finished
254   int msgs_in_queue;
255   CURLMsg* msg;
256   while ((msg = curl_multi_info_read(m_multi_handle, &msgs_in_queue)))
257   {
258     switch(msg->msg)
259     {
260       case CURLMSG_DONE:
261         curl_multi_remove_handle(m_multi_handle, msg->easy_handle);
262         log_info << "DOWNLOAD DONE" << std::endl;
263         //FIXME: finish_transfer(msg->easy_handle);
264         break;
265
266       default:
267         log_warning << "unhandled cURL message: " << msg->msg << std::endl;
268         break;
269     }
270   }
271 }
272
273 TransferStatusPtr
274 Downloader::request_download(const std::string& url, const std::string& filename)
275 {
276   std::unique_ptr<Transfer> transfer(new Transfer(*this, m_next_transfer_id++, url));
277   curl_multi_add_handle(m_multi_handle, transfer->get_curl_handle());
278   m_transfers.push_back(std::move(transfer));
279   return m_transfers.back()->get_status();
280 }
281
282 /* EOF */