]> git.mxchange.org Git - simgear.git/blob - simgear/package/Install.cxx
Fix platform check for strerror_r on Mac
[simgear.git] / simgear / package / Install.cxx
1 // Copyright (C) 2013  James Turner - zakalawe@mac.com
2 //
3 // This library is free software; you can redistribute it and/or
4 // modify it under the terms of the GNU Library General Public
5 // License as published by the Free Software Foundation; either
6 // version 2 of the License, or (at your option) any later version.
7 //
8 // This library is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11 // Library General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with this program; if not, write to the Free Software
15 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16 //
17
18 #include <simgear/package/Install.hxx>
19
20 #include <boost/foreach.hpp>
21 #include <fstream>
22
23 #include <simgear/package/unzip.h>
24 #include <simgear/package/md5.h>
25
26 #include <simgear/structure/exception.hxx>
27 #include <simgear/props/props_io.hxx>
28 #include <simgear/package/Catalog.hxx>
29 #include <simgear/package/Package.hxx>
30 #include <simgear/package/Root.hxx>
31 #include <simgear/io/HTTPRequest.hxx>
32 #include <simgear/io/HTTPClient.hxx>
33 #include <simgear/misc/sg_dir.hxx>
34 #include <simgear/misc/strutils.hxx>
35
36 extern "C" {
37     void fill_memory_filefunc (zlib_filefunc_def*);
38 }
39
40 namespace simgear {
41
42 namespace pkg {
43
44 class Install::PackageArchiveDownloader : public HTTP::Request
45 {
46 public:
47     PackageArchiveDownloader(InstallRef aOwner) :
48         HTTP::Request("" /* dummy URL */),
49         m_owner(aOwner),
50         m_downloaded(0)
51     {
52         m_urls = m_owner->package()->downloadUrls();
53         if (m_urls.empty()) {
54             throw sg_exception("no package download URLs");
55         }
56
57         // TODO randomise order of m_urls
58
59         m_extractPath = aOwner->path().dir();
60         m_extractPath.append("_extract_" + aOwner->package()->md5());
61
62         // clean up any existing files
63         Dir d(m_extractPath);
64         if (d.exists()) {
65             d.remove(true /* recursive */);
66         }
67     }
68
69     ~PackageArchiveDownloader()
70     {
71         // always clean up our extraction dir: if we successfully downloaded
72         // and installed it will be an empty dir, if we failed it might contain
73         // (some) of the package files.
74         Dir d(m_extractPath);
75         if (d.exists()) {
76             d.remove(true /* recursive */);
77         }
78     }
79
80     size_t downloadedBytes() const
81     {
82         return m_downloaded;
83     }
84
85     int percentDownloaded() const
86     {
87         if (responseLength() <= 0) {
88             return 0;
89         }
90
91         return (m_downloaded * 100) / responseLength();
92     }
93 protected:
94     virtual std::string url() const
95     {
96         return m_urls.front();
97     }
98
99     virtual void responseHeadersComplete()
100     {
101         Request::responseHeadersComplete();
102
103         Dir d(m_extractPath);
104         d.create(0755);
105
106         memset(&m_md5, 0, sizeof(SG_MD5_CTX));
107         SG_MD5Init(&m_md5);
108     }
109
110     virtual void gotBodyData(const char* s, int n)
111     {
112         m_buffer += std::string(s, n);
113         SG_MD5Update(&m_md5, (unsigned char*) s, n);
114
115         m_downloaded = m_buffer.size();
116         m_owner->installProgress(m_buffer.size(), responseLength());
117     }
118
119     virtual void onDone()
120     {
121         if (responseCode() != 200) {
122             SG_LOG(SG_GENERAL, SG_ALERT, "download failure:" << responseCode() <<
123                    "\n\t" << url());
124             Delegate::StatusCode code = Delegate::FAIL_DOWNLOAD;
125             if (responseCode() == 404) {
126                 code = Delegate::FAIL_NOT_FOUND;
127             }
128
129             doFailure(code);
130             return;
131         }
132
133         unsigned char digest[MD5_DIGEST_LENGTH];
134         SG_MD5Final(digest, &m_md5);
135         std::string const hex_md5 =
136           strutils::encodeHex(digest, MD5_DIGEST_LENGTH);
137
138         if (hex_md5 != m_owner->package()->md5()) {
139             SG_LOG(SG_GENERAL, SG_ALERT, "md5 verification failed:\n"
140                 << "\t" << hex_md5 << "\n\t"
141                 << m_owner->package()->md5() << "\n\t"
142                 << "downloading from:" << url());
143             doFailure(Delegate::FAIL_CHECKSUM);
144             return;
145         }
146
147         if (!extractUnzip()) {
148             SG_LOG(SG_GENERAL, SG_WARN, "zip extraction failed");
149             doFailure(Delegate::FAIL_EXTRACT);
150             return;
151         }
152
153         if (m_owner->path().exists()) {
154             Dir destDir(m_owner->path());
155             destDir.remove(true /* recursive */);
156         }
157
158         // build a path like /path/to/packages/org.some.catalog/Aircraft/extract_xxxx/MyAircraftDir
159         SGPath extractedPath = m_extractPath;
160         extractedPath.append(m_owner->package()->dirName());
161
162         // rename it to path/to/packages/org.some.catalog/Aircraft/MyAircraftDir
163         bool ok = extractedPath.rename(m_owner->path());
164         if (!ok) {
165             doFailure(Delegate::FAIL_FILESYSTEM);
166             return;
167         }
168
169         // extract_xxxx directory is now empty, so remove it
170         m_extractPath.remove();
171
172         m_owner->m_revision = m_owner->package()->revision();
173         m_owner->writeRevisionFile();
174         m_owner->m_download.reset(); // so isDownloading reports false
175
176         m_owner->installResult(Delegate::STATUS_SUCCESS);
177     }
178
179     virtual void onFail()
180     {
181         if (responseCode() == -1) {
182             doFailure(Delegate::USER_CANCELLED);
183         } else {
184             doFailure(Delegate::FAIL_DOWNLOAD);
185         }
186     }
187
188 private:
189
190     void extractCurrentFile(unzFile zip, char* buffer, size_t bufferSize)
191     {
192         unz_file_info fileInfo;
193         unzGetCurrentFileInfo(zip, &fileInfo,
194             buffer, bufferSize,
195             NULL, 0,  /* extra field */
196             NULL, 0 /* comment field */);
197
198         std::string name(buffer);
199     // no absolute paths, no 'up' traversals
200     // we could also look for suspicious file extensions here (forbid .dll, .exe, .so)
201         if ((name[0] == '/') || (name.find("../") != std::string::npos) || (name.find("..\\") != std::string::npos)) {
202             throw sg_format_exception("Bad zip path", name);
203         }
204
205         if (fileInfo.uncompressed_size == 0) {
206             // assume it's a directory for now
207             // since we create parent directories when extracting
208             // a path, we're done here
209             return;
210         }
211
212         int result = unzOpenCurrentFile(zip);
213         if (result != UNZ_OK) {
214             throw sg_io_exception("opening current zip file failed", sg_location(name));
215         }
216
217         std::ofstream outFile;
218         bool eof = false;
219         SGPath path(m_extractPath);
220         path.append(name);
221
222     // create enclosing directory heirarchy as required
223         Dir parentDir(path.dir());
224         if (!parentDir.exists()) {
225             bool ok = parentDir.create(0755);
226             if (!ok) {
227                 throw sg_io_exception("failed to create directory heirarchy for extraction", path.c_str());
228             }
229         }
230
231         outFile.open(path.c_str(), std::ios::binary | std::ios::trunc | std::ios::out);
232         if (outFile.fail()) {
233             throw sg_io_exception("failed to open output file for writing", path.c_str());
234         }
235
236         while (!eof) {
237             int bytes = unzReadCurrentFile(zip, buffer, bufferSize);
238             if (bytes < 0) {
239                 throw sg_io_exception("unzip failure reading curent archive", sg_location(name));
240             } else if (bytes == 0) {
241                 eof = true;
242             } else {
243                 outFile.write(buffer, bytes);
244             }
245         }
246
247         outFile.close();
248         unzCloseCurrentFile(zip);
249     }
250
251     bool extractUnzip()
252     {
253         bool result = true;
254         zlib_filefunc_def memoryAccessFuncs;
255         fill_memory_filefunc(&memoryAccessFuncs);
256
257         char bufferName[128];
258         snprintf(bufferName, 128, "%p+%lx", m_buffer.data(), m_buffer.size());
259         unzFile zip = unzOpen2(bufferName, &memoryAccessFuncs);
260
261         const size_t BUFFER_SIZE = 32 * 1024;
262         void* buf = malloc(BUFFER_SIZE);
263
264         try {
265             int result = unzGoToFirstFile(zip);
266             if (result != UNZ_OK) {
267                 throw sg_exception("failed to go to first file in archive");
268             }
269
270             while (true) {
271                 extractCurrentFile(zip, (char*) buf, BUFFER_SIZE);
272                 result = unzGoToNextFile(zip);
273                 if (result == UNZ_END_OF_LIST_OF_FILE) {
274                     break;
275                 } else if (result != UNZ_OK) {
276                     throw sg_io_exception("failed to go to next file in the archive");
277                 }
278             }
279         } catch (sg_exception& ) {
280             result = false;
281         }
282
283         free(buf);
284         unzClose(zip);
285         return result;
286     }
287
288     void doFailure(Delegate::StatusCode aReason)
289     {
290         Dir dir(m_extractPath);
291         if (dir.exists()) {
292             dir.remove(true /* recursive */);
293         }
294
295         // TODO - try other mirrors
296         m_owner->m_download.reset(); // ensure we get cleaned up
297         m_owner->installResult(aReason);
298     }
299
300     InstallRef m_owner;
301     string_list m_urls;
302     SG_MD5_CTX m_md5;
303     std::string m_buffer;
304     SGPath m_extractPath;
305     size_t m_downloaded;
306 };
307
308 ////////////////////////////////////////////////////////////////////
309 Install::Install(PackageRef aPkg, const SGPath& aPath) :
310     m_package(aPkg),
311     m_path(aPath),
312     m_status(Delegate::STATUS_IN_PROGRESS)
313 {
314     parseRevision();
315     m_package->catalog()->root()->registerInstall(this);
316 }
317
318 Install::~Install()
319 {
320 }
321
322 InstallRef Install::createFromPath(const SGPath& aPath, CatalogRef aCat)
323 {
324     std::string path = aPath.file();
325     PackageRef pkg = aCat->getPackageByPath(path);
326     if (!pkg)
327         throw sg_exception("no package with path:" + path);
328
329     return new Install(pkg, aPath);
330 }
331
332 void Install::parseRevision()
333 {
334     SGPath revisionFile = m_path;
335     revisionFile.append(".revision");
336     if (!revisionFile.exists()) {
337         m_revision = 0;
338         return;
339     }
340
341     std::ifstream f(revisionFile.c_str(), std::ios::in);
342     f >> m_revision;
343 }
344
345 void Install::writeRevisionFile()
346 {
347     SGPath revisionFile = m_path;
348     revisionFile.append(".revision");
349     std::ofstream f(revisionFile.c_str(), std::ios::out | std::ios::trunc);
350     f << m_revision << std::endl;
351 }
352
353 bool Install::hasUpdate() const
354 {
355     return m_package->revision() > m_revision;
356 }
357
358 void Install::startUpdate()
359 {
360     if (m_download) {
361         return; // already active
362     }
363
364     m_download = new PackageArchiveDownloader(this);
365     m_package->catalog()->root()->makeHTTPRequest(m_download);
366     m_package->catalog()->root()->startInstall(this);
367 }
368
369 bool Install::uninstall()
370 {
371     Dir d(m_path);
372     if (!d.remove(true)) {
373         SG_LOG(SG_GENERAL, SG_ALERT, "package uninstall failed: couldn't remove path " << m_path);
374         return false;
375     }
376
377     m_package->catalog()->root()->unregisterInstall(this);
378     return true;
379 }
380
381 bool Install::isDownloading() const
382 {
383     return (m_download.valid());
384 }
385
386 bool Install::isQueued() const
387 {
388     return m_package->catalog()->root()->isInstallQueued(const_cast<Install*>(this));
389 }
390
391 int Install::downloadedPercent() const
392 {
393     if (!m_download.valid()) {
394         return -1;
395     }
396
397     PackageArchiveDownloader* dl = static_cast<PackageArchiveDownloader*>(m_download.get());
398     return dl->percentDownloaded();
399 }
400
401 size_t Install::downloadedBytes() const
402 {
403     if (!m_download.valid()) {
404         return -1;
405     }
406
407     PackageArchiveDownloader* dl = static_cast<PackageArchiveDownloader*>(m_download.get());
408     return dl->downloadedBytes();
409
410 }
411
412 void Install::cancelDownload()
413 {
414     if (m_download.valid()) {
415         m_package->catalog()->root()->cancelHTTPRequest(m_download, "User cancelled download");
416     }
417
418     if (m_revision == 0) {
419         SG_LOG(SG_GENERAL, SG_INFO, "cancel install of package, will unregister");
420         m_package->catalog()->root()->unregisterInstall(this);
421     }
422
423     m_package->catalog()->root()->cancelDownload(this);
424 }
425
426 struct PathAppender
427 {
428     PathAppender(const SGPath& p) : m_path(p) {}
429
430     SGPath operator()(const std::string& s) const
431     {
432         SGPath p(m_path);
433         p.append(s);
434         return p;
435     }
436
437     SGPath m_path;
438 };
439
440 PathList Install::thumbnailPaths() const
441 {
442     const string_list& thumbs(m_package->thumbnails());
443     PathList result;
444     if (thumbs.empty())
445         return result;
446
447     std::transform(thumbs.begin(), thumbs.end(),
448                    std::back_inserter(result),
449                    PathAppender(m_path));
450     return result;
451 }
452
453 SGPath Install::primarySetPath() const
454 {
455     SGPath setPath(m_path);
456     std::string ps(m_package->id());
457     setPath.append(ps + "-set.xml");
458     return setPath;
459 }
460
461 //------------------------------------------------------------------------------
462 Install* Install::done(const Callback& cb)
463 {
464   if( m_status == Delegate::STATUS_SUCCESS )
465     cb(this);
466   else
467     _cb_done.push_back(cb);
468
469   return this;
470 }
471
472 //------------------------------------------------------------------------------
473 Install* Install::fail(const Callback& cb)
474 {
475   if(    m_status != Delegate::STATUS_SUCCESS
476       && m_status != Delegate::STATUS_IN_PROGRESS )
477     cb(this);
478   else
479     _cb_fail.push_back(cb);
480
481   return this;
482 }
483
484 //------------------------------------------------------------------------------
485 Install* Install::always(const Callback& cb)
486 {
487   if( m_status != Delegate::STATUS_IN_PROGRESS )
488     cb(this);
489   else
490     _cb_always.push_back(cb);
491
492   return this;
493 }
494
495 //------------------------------------------------------------------------------
496 Install* Install::progress(const ProgressCallback& cb)
497 {
498   _cb_progress.push_back(cb);
499   return this;
500 }
501
502 //------------------------------------------------------------------------------
503 void Install::installResult(Delegate::StatusCode aReason)
504 {
505     m_package->catalog()->root()->finishInstall(this, aReason);
506     if (aReason == Delegate::STATUS_SUCCESS) {
507         _cb_done(this);
508     } else {
509         _cb_fail(this);
510     }
511
512     _cb_always(this);
513 }
514
515 //------------------------------------------------------------------------------
516 void Install::installProgress(unsigned int aBytes, unsigned int aTotal)
517 {
518   m_package->catalog()->root()->installProgress(this, aBytes, aTotal);
519   _cb_progress(this, aBytes, aTotal);
520 }
521
522
523 } // of namespace pkg
524
525 } // of namespace simgear