Added file writing and then-callback to Downloader
[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 class Transfer
52 {
53 private:
54   Downloader& m_downloader;
55   TransferId m_id;
56
57   std::string m_url;
58   CURL* m_handle;
59   std::array<char, CURL_ERROR_SIZE> m_error_buffer;
60
61   TransferStatusPtr m_status;
62   std::unique_ptr<PHYSFS_file, int(*)(PHYSFS_File*)> m_fout;
63
64 public:
65   Transfer(Downloader& downloader, TransferId id,
66            const std::string& url,
67            const std::string& outfile) :
68     m_downloader(downloader),
69     m_id(id),
70     m_url(url),
71     m_handle(),
72     m_error_buffer(),
73     m_status(new TransferStatus(id)),
74     m_fout(PHYSFS_openWrite(outfile.c_str()), PHYSFS_close)
75   {
76     if (!m_fout)
77     {
78       std::ostringstream out;
79       out << "PHYSFS_openRead() failed: " << PHYSFS_getLastError();
80       throw std::runtime_error(out.str());
81     }
82
83     m_handle = curl_easy_init();
84     if (!m_handle)
85     {
86       throw std::runtime_error("curl_easy_init() failed");
87     }
88     else
89     {
90       curl_easy_setopt(m_handle, CURLOPT_URL, url.c_str());
91       curl_easy_setopt(m_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
92
93       curl_easy_setopt(m_handle, CURLOPT_WRITEDATA, this);
94       curl_easy_setopt(m_handle, CURLOPT_WRITEFUNCTION, &Transfer::on_data_wrap);
95
96       curl_easy_setopt(m_handle, CURLOPT_ERRORBUFFER, m_error_buffer.data());
97       curl_easy_setopt(m_handle, CURLOPT_NOSIGNAL, 1);
98       curl_easy_setopt(m_handle, CURLOPT_FAILONERROR, 1);
99       curl_easy_setopt(m_handle, CURLOPT_FOLLOWLOCATION, 1);
100
101       curl_easy_setopt(m_handle, CURLOPT_NOPROGRESS, 0);
102       curl_easy_setopt(m_handle, CURLOPT_XFERINFODATA, this);
103       curl_easy_setopt(m_handle, CURLOPT_XFERINFOFUNCTION, &Transfer::on_progress_wrap);
104     }
105   }
106
107   ~Transfer()
108   {
109     curl_easy_cleanup(m_handle);
110   }
111
112   TransferStatusPtr get_status() const
113   {
114     return m_status;
115   }
116
117   TransferId get_id() const
118   {
119     return m_id;
120   }
121
122   CURL* get_curl_handle() const
123   {
124     return m_handle;
125   }
126
127   std::string get_url() const
128   {
129     return m_url;
130   }
131
132   size_t on_data(void* ptr, size_t size, size_t nmemb)
133   {
134     PHYSFS_write(m_fout.get(), ptr, size, nmemb);
135     return size * nmemb;
136   }
137
138   void on_progress(curl_off_t dltotal, curl_off_t dlnow,
139                    curl_off_t ultotal, curl_off_t ulnow)
140   {
141     m_status->dltotal = dltotal;
142     m_status->dlnow = dlnow;
143
144     m_status->ultotal = ultotal;
145     m_status->ulnow = ulnow;
146   }
147
148 private:
149   static size_t on_data_wrap(char* ptr, size_t size, size_t nmemb, void* userdata)
150   {
151     return static_cast<Transfer*>(userdata)->on_data(ptr, size, nmemb);
152   }
153
154   static void on_progress_wrap(void* userdata,
155                                curl_off_t dltotal, curl_off_t dlnow,
156                                curl_off_t ultotal, curl_off_t ulnow)
157   {
158     return static_cast<Transfer*>(userdata)->on_progress(dltotal, dlnow, ultotal, ulnow);
159   }
160
161 private:
162   Transfer(const Transfer&) = delete;
163   Transfer& operator=(const Transfer&) = delete;
164 };
165
166 Downloader::Downloader() :
167   m_multi_handle(),
168   m_transfers(),
169   m_next_transfer_id(1)
170 {
171   curl_global_init(CURL_GLOBAL_ALL);
172   m_multi_handle = curl_multi_init();
173   if (!m_multi_handle)
174   {
175     throw std::runtime_error("curl_multi_init() failed");
176   }
177 }
178
179 Downloader::~Downloader()
180 {
181   for(auto& transfer : m_transfers)
182   {
183     curl_multi_remove_handle(m_multi_handle, transfer->get_curl_handle());
184   }
185   m_transfers.clear();
186
187   curl_multi_cleanup(m_multi_handle);
188   curl_global_cleanup();
189 }
190
191 void
192 Downloader::download(const std::string& url,
193                      size_t (*write_func)(void* ptr, size_t size, size_t nmemb, void* userdata),
194                      void* userdata)
195 {
196   log_info << "Downloading " << url << std::endl;
197
198   char error_buffer[CURL_ERROR_SIZE+1];
199
200   CURL* curl_handle = curl_easy_init();
201   curl_easy_setopt(curl_handle, CURLOPT_URL, url.c_str());
202   curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "SuperTux/" PACKAGE_VERSION " libcURL");
203   curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, write_func);
204   curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, userdata);
205   curl_easy_setopt(curl_handle, CURLOPT_ERRORBUFFER, error_buffer);
206   curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1);
207   curl_easy_setopt(curl_handle, CURLOPT_NOSIGNAL, 1);
208   curl_easy_setopt(curl_handle, CURLOPT_FAILONERROR, 1);
209   curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1);
210   CURLcode result = curl_easy_perform(curl_handle);
211   curl_easy_cleanup(curl_handle);
212
213   if (result != CURLE_OK)
214   {
215     std::string why = error_buffer[0] ? error_buffer : "unhandled error";
216     throw std::runtime_error(url + ": download failed: " + why);
217   }
218 }
219
220 std::string
221 Downloader::download(const std::string& url)
222 {
223   std::string result;
224   download(url, my_curl_string_append, &result);
225   return result;
226 }
227
228 void
229 Downloader::download(const std::string& url, const std::string& filename)
230 {
231   std::unique_ptr<PHYSFS_file, int(*)(PHYSFS_File*)> fout(PHYSFS_openWrite(filename.c_str()),
232                                                           PHYSFS_close);
233   download(url, my_curl_physfs_write, fout.get());
234 }
235
236 void
237 Downloader::abort(TransferId id)
238 {
239   auto it = std::find_if(m_transfers.begin(), m_transfers.end(),
240                          [&id](const std::unique_ptr<Transfer>& rhs)
241                          {
242                            return id == rhs->get_id();
243                          });
244   if (it == m_transfers.end())
245   {
246     log_warning << "transfer not found: " << id << std::endl;
247   }
248   else
249   {
250     curl_multi_remove_handle(m_multi_handle, (*it)->get_curl_handle());
251     m_transfers.erase(it);
252   }
253 }
254
255 void
256 Downloader::update()
257 {
258   // read data from the network
259   CURLMcode ret;
260   int running_handles;
261   while((ret = curl_multi_perform(m_multi_handle, &running_handles)) == CURLM_CALL_MULTI_PERFORM)
262   {
263     log_debug << "updating" << std::endl;
264   }
265
266   // check if any downloads got finished
267   int msgs_in_queue;
268   CURLMsg* msg;
269   while ((msg = curl_multi_info_read(m_multi_handle, &msgs_in_queue)))
270   {
271     switch(msg->msg)
272     {
273       case CURLMSG_DONE:
274         {
275           log_info << "Download completed" << std::endl;
276           curl_multi_remove_handle(m_multi_handle, msg->easy_handle);
277           auto it = std::find_if(m_transfers.begin(), m_transfers.end(),
278                                  [&msg](const std::unique_ptr<Transfer>& rhs) {
279                                    return rhs->get_curl_handle() == msg->easy_handle;
280                                  });
281           assert(it != m_transfers.end());
282           TransferStatusPtr status = (*it)->get_status();
283           m_transfers.erase(it);
284
285           status->status = TransferStatus::COMPLETED;
286           if (status->callback)
287           {
288             status->callback();
289           }
290         }
291         break;
292
293       default:
294         log_warning << "unhandled cURL message: " << msg->msg << std::endl;
295         break;
296     }
297   }
298 }
299
300 TransferStatusPtr
301 Downloader::request_download(const std::string& url, const std::string& outfile)
302 {
303   std::unique_ptr<Transfer> transfer(new Transfer(*this, m_next_transfer_id++, url, outfile));
304   curl_multi_add_handle(m_multi_handle, transfer->get_curl_handle());
305   m_transfers.push_back(std::move(transfer));
306   return m_transfers.back()->get_status();
307 }
308
309 /* EOF */