]> git.mxchange.org Git - simgear.git/blob - simgear/package/Install.cxx
9b71d9f0f020a922e0589809f1e7f9485ba775ba
[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         m_owner->m_revision = m_owner->package()->revision();
170         m_owner->writeRevisionFile();
171         m_owner->m_download.reset(); // so isDownloading reports false
172
173         m_owner->installResult(Delegate::STATUS_SUCCESS);
174     }
175
176     virtual void onFail()
177     {
178         if (responseCode() == -1) {
179             doFailure(Delegate::USER_CANCELLED);
180         } else {
181             doFailure(Delegate::FAIL_DOWNLOAD);
182         }
183     }
184
185 private:
186
187     void extractCurrentFile(unzFile zip, char* buffer, size_t bufferSize)
188     {
189         unz_file_info fileInfo;
190         unzGetCurrentFileInfo(zip, &fileInfo,
191             buffer, bufferSize,
192             NULL, 0,  /* extra field */
193             NULL, 0 /* comment field */);
194
195         std::string name(buffer);
196     // no absolute paths, no 'up' traversals
197     // we could also look for suspicious file extensions here (forbid .dll, .exe, .so)
198         if ((name[0] == '/') || (name.find("../") != std::string::npos) || (name.find("..\\") != std::string::npos)) {
199             throw sg_format_exception("Bad zip path", name);
200         }
201
202         if (fileInfo.uncompressed_size == 0) {
203             // assume it's a directory for now
204             // since we create parent directories when extracting
205             // a path, we're done here
206             return;
207         }
208
209         int result = unzOpenCurrentFile(zip);
210         if (result != UNZ_OK) {
211             throw sg_io_exception("opening current zip file failed", sg_location(name));
212         }
213
214         std::ofstream outFile;
215         bool eof = false;
216         SGPath path(m_extractPath);
217         path.append(name);
218
219     // create enclosing directory heirarchy as required
220         Dir parentDir(path.dir());
221         if (!parentDir.exists()) {
222             bool ok = parentDir.create(0755);
223             if (!ok) {
224                 throw sg_io_exception("failed to create directory heirarchy for extraction", path.c_str());
225             }
226         }
227
228         outFile.open(path.c_str(), std::ios::binary | std::ios::trunc | std::ios::out);
229         if (outFile.fail()) {
230             throw sg_io_exception("failed to open output file for writing", path.c_str());
231         }
232
233         while (!eof) {
234             int bytes = unzReadCurrentFile(zip, buffer, bufferSize);
235             if (bytes < 0) {
236                 throw sg_io_exception("unzip failure reading curent archive", sg_location(name));
237             } else if (bytes == 0) {
238                 eof = true;
239             } else {
240                 outFile.write(buffer, bytes);
241             }
242         }
243
244         outFile.close();
245         unzCloseCurrentFile(zip);
246     }
247
248     bool extractUnzip()
249     {
250         bool result = true;
251         zlib_filefunc_def memoryAccessFuncs;
252         fill_memory_filefunc(&memoryAccessFuncs);
253
254         char bufferName[128];
255         snprintf(bufferName, 128, "%p+%lx", m_buffer.data(), m_buffer.size());
256         unzFile zip = unzOpen2(bufferName, &memoryAccessFuncs);
257
258         const size_t BUFFER_SIZE = 32 * 1024;
259         void* buf = malloc(BUFFER_SIZE);
260
261         try {
262             int result = unzGoToFirstFile(zip);
263             if (result != UNZ_OK) {
264                 throw sg_exception("failed to go to first file in archive");
265             }
266
267             while (true) {
268                 extractCurrentFile(zip, (char*) buf, BUFFER_SIZE);
269                 result = unzGoToNextFile(zip);
270                 if (result == UNZ_END_OF_LIST_OF_FILE) {
271                     break;
272                 } else if (result != UNZ_OK) {
273                     throw sg_io_exception("failed to go to next file in the archive");
274                 }
275             }
276         } catch (sg_exception& ) {
277             result = false;
278         }
279
280         free(buf);
281         unzClose(zip);
282         return result;
283     }
284
285     void doFailure(Delegate::StatusCode aReason)
286     {
287         Dir dir(m_extractPath);
288         if (dir.exists()) {
289             dir.remove(true /* recursive */);
290         }
291
292         // TODO - try other mirrors
293         m_owner->m_download.reset(); // ensure we get cleaned up
294         m_owner->installResult(aReason);
295     }
296
297     InstallRef m_owner;
298     string_list m_urls;
299     SG_MD5_CTX m_md5;
300     std::string m_buffer;
301     SGPath m_extractPath;
302     size_t m_downloaded;
303 };
304
305 ////////////////////////////////////////////////////////////////////
306 Install::Install(PackageRef aPkg, const SGPath& aPath) :
307     m_package(aPkg),
308     m_path(aPath),
309     m_status(Delegate::STATUS_IN_PROGRESS)
310 {
311     parseRevision();
312     m_package->catalog()->root()->registerInstall(this);
313 }
314
315 Install::~Install()
316 {
317 }
318
319 InstallRef Install::createFromPath(const SGPath& aPath, CatalogRef aCat)
320 {
321     std::string path = aPath.file();
322     PackageRef pkg = aCat->getPackageByPath(path);
323     if (!pkg)
324         throw sg_exception("no package with path:" + path);
325
326     return new Install(pkg, aPath);
327 }
328
329 void Install::parseRevision()
330 {
331     SGPath revisionFile = m_path;
332     revisionFile.append(".revision");
333     if (!revisionFile.exists()) {
334         m_revision = 0;
335         return;
336     }
337
338     std::ifstream f(revisionFile.c_str(), std::ios::in);
339     f >> m_revision;
340 }
341
342 void Install::writeRevisionFile()
343 {
344     SGPath revisionFile = m_path;
345     revisionFile.append(".revision");
346     std::ofstream f(revisionFile.c_str(), std::ios::out | std::ios::trunc);
347     f << m_revision << std::endl;
348 }
349
350 bool Install::hasUpdate() const
351 {
352     return m_package->revision() > m_revision;
353 }
354
355 void Install::startUpdate()
356 {
357     if (m_download) {
358         return; // already active
359     }
360
361     m_download = new PackageArchiveDownloader(this);
362     m_package->catalog()->root()->makeHTTPRequest(m_download);
363     m_package->catalog()->root()->startInstall(this);
364 }
365
366 bool Install::uninstall()
367 {
368     Dir d(m_path);
369     if (!d.remove(true)) {
370         SG_LOG(SG_GENERAL, SG_ALERT, "package uninstall failed: couldn't remove path " << m_path);
371         return false;
372     }
373
374     m_package->catalog()->root()->unregisterInstall(this);
375     return true;
376 }
377
378 bool Install::isDownloading() const
379 {
380     return (m_download.valid());
381 }
382
383 bool Install::isQueued() const
384 {
385     return m_package->catalog()->root()->isInstallQueued(const_cast<Install*>(this));
386 }
387
388 int Install::downloadedPercent() const
389 {
390     if (!m_download.valid()) {
391         return -1;
392     }
393
394     PackageArchiveDownloader* dl = static_cast<PackageArchiveDownloader*>(m_download.get());
395     return dl->percentDownloaded();
396 }
397
398 size_t Install::downloadedBytes() const
399 {
400     if (!m_download.valid()) {
401         return -1;
402     }
403
404     PackageArchiveDownloader* dl = static_cast<PackageArchiveDownloader*>(m_download.get());
405     return dl->downloadedBytes();
406
407 }
408
409 void Install::cancelDownload()
410 {
411     if (m_download.valid()) {
412         m_package->catalog()->root()->cancelHTTPRequest(m_download, "User cancelled download");
413     }
414
415     if (m_revision == 0) {
416         SG_LOG(SG_GENERAL, SG_INFO, "cancel install of package, will unregister");
417         m_package->catalog()->root()->unregisterInstall(this);
418     }
419
420     m_package->catalog()->root()->cancelDownload(this);
421 }
422
423 struct PathAppender
424 {
425     PathAppender(const SGPath& p) : m_path(p) {}
426
427     SGPath operator()(const std::string& s) const
428     {
429         SGPath p(m_path);
430         p.append(s);
431         return p;
432     }
433
434     SGPath m_path;
435 };
436
437 PathList Install::thumbnailPaths() const
438 {
439     const string_list& thumbs(m_package->thumbnails());
440     PathList result;
441     if (thumbs.empty())
442         return result;
443
444     std::transform(thumbs.begin(), thumbs.end(),
445                    std::back_inserter(result),
446                    PathAppender(m_path));
447     return result;
448 }
449
450 SGPath Install::primarySetPath() const
451 {
452     SGPath setPath(m_path);
453     std::string ps(m_package->id());
454     setPath.append(ps + "-set.xml");
455     return setPath;
456 }
457
458 //------------------------------------------------------------------------------
459 Install* Install::done(const Callback& cb)
460 {
461   if( m_status == Delegate::STATUS_SUCCESS )
462     cb(this);
463   else
464     _cb_done.push_back(cb);
465
466   return this;
467 }
468
469 //------------------------------------------------------------------------------
470 Install* Install::fail(const Callback& cb)
471 {
472   if(    m_status != Delegate::STATUS_SUCCESS
473       && m_status != Delegate::STATUS_IN_PROGRESS )
474     cb(this);
475   else
476     _cb_fail.push_back(cb);
477
478   return this;
479 }
480
481 //------------------------------------------------------------------------------
482 Install* Install::always(const Callback& cb)
483 {
484   if( m_status != Delegate::STATUS_IN_PROGRESS )
485     cb(this);
486   else
487     _cb_always.push_back(cb);
488
489   return this;
490 }
491
492 //------------------------------------------------------------------------------
493 Install* Install::progress(const ProgressCallback& cb)
494 {
495   _cb_progress.push_back(cb);
496   return this;
497 }
498
499 //------------------------------------------------------------------------------
500 void Install::installResult(Delegate::StatusCode aReason)
501 {
502     m_package->catalog()->root()->finishInstall(this, aReason);
503     if (aReason == Delegate::STATUS_SUCCESS) {
504         _cb_done(this);
505     } else {
506         _cb_fail(this);
507     }
508
509     _cb_always(this);
510 }
511
512 //------------------------------------------------------------------------------
513 void Install::installProgress(unsigned int aBytes, unsigned int aTotal)
514 {
515   m_package->catalog()->root()->installProgress(this, aBytes, aTotal);
516   _cb_progress(this, aBytes, aTotal);
517 }
518
519
520 } // of namespace pkg
521
522 } // of namespace simgear