1 // Copyright (C) 2013 James Turner - zakalawe@mac.com
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.
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.
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.
18 #include <simgear/package/Install.hxx>
20 #include <boost/foreach.hpp>
23 #include <simgear/package/unzip.h>
24 #include <simgear/package/md5.h>
25 #include <simgear/package/untar.hxx>
27 #include <simgear/structure/exception.hxx>
28 #include <simgear/props/props_io.hxx>
29 #include <simgear/package/Catalog.hxx>
30 #include <simgear/package/Package.hxx>
31 #include <simgear/package/Root.hxx>
32 #include <simgear/io/HTTPRequest.hxx>
33 #include <simgear/io/HTTPClient.hxx>
34 #include <simgear/misc/sg_dir.hxx>
35 #include <simgear/misc/strutils.hxx>
38 void fill_memory_filefunc (zlib_filefunc_def*);
45 class Install::PackageArchiveDownloader : public HTTP::Request
48 PackageArchiveDownloader(InstallRef aOwner) :
49 HTTP::Request("" /* dummy URL */),
53 m_urls = m_owner->package()->downloadUrls();
55 throw sg_exception("no package download URLs");
58 // TODO randomise order of m_urls
60 m_extractPath = aOwner->path().dir();
61 m_extractPath.append("_extract_" + aOwner->package()->md5());
63 // clean up any existing files
66 d.remove(true /* recursive */);
70 ~PackageArchiveDownloader()
72 // always clean up our extraction dir: if we successfully downloaded
73 // and installed it will be an empty dir, if we failed it might contain
74 // (some) of the package files.
77 d.remove(true /* recursive */);
81 size_t downloadedBytes() const
86 int percentDownloaded() const
88 if (responseLength() <= 0) {
92 return (m_downloaded * 100) / responseLength();
95 virtual std::string url() const
97 return m_urls.front();
100 virtual void responseHeadersComplete()
102 Request::responseHeadersComplete();
104 Dir d(m_extractPath);
107 memset(&m_md5, 0, sizeof(SG_MD5_CTX));
111 virtual void gotBodyData(const char* s, int n)
113 m_buffer += std::string(s, n);
114 SG_MD5Update(&m_md5, (unsigned char*) s, n);
116 m_downloaded = m_buffer.size();
117 m_owner->installProgress(m_buffer.size(), responseLength());
120 virtual void onDone()
122 if (responseCode() != 200) {
123 SG_LOG(SG_GENERAL, SG_ALERT, "download failure:" << responseCode() <<
125 Delegate::StatusCode code = Delegate::FAIL_DOWNLOAD;
126 if (responseCode() == 404) {
127 code = Delegate::FAIL_NOT_FOUND;
134 unsigned char digest[MD5_DIGEST_LENGTH];
135 SG_MD5Final(digest, &m_md5);
136 std::string const hex_md5 =
137 strutils::encodeHex(digest, MD5_DIGEST_LENGTH);
139 if (hex_md5 != m_owner->package()->md5()) {
140 SG_LOG(SG_GENERAL, SG_ALERT, "md5 verification failed:\n"
141 << "\t" << hex_md5 << "\n\t"
142 << m_owner->package()->md5() << "\n\t"
143 << "downloading from:" << url());
144 doFailure(Delegate::FAIL_CHECKSUM);
149 SG_LOG(SG_GENERAL, SG_WARN, "archive extraction failed");
150 doFailure(Delegate::FAIL_EXTRACT);
154 if (m_owner->path().exists()) {
155 Dir destDir(m_owner->path());
156 destDir.remove(true /* recursive */);
159 // build a path like /path/to/packages/org.some.catalog/Aircraft/extract_xxxx/MyAircraftDir
160 SGPath extractedPath = m_extractPath;
161 extractedPath.append(m_owner->package()->dirName());
163 // rename it to path/to/packages/org.some.catalog/Aircraft/MyAircraftDir
164 bool ok = extractedPath.rename(m_owner->path());
166 doFailure(Delegate::FAIL_FILESYSTEM);
170 // extract_xxxx directory is now empty, so remove it
171 if (m_extractPath.exists()) {
172 simgear::Dir(m_extractPath).remove();
175 m_owner->m_revision = m_owner->package()->revision();
176 m_owner->writeRevisionFile();
177 m_owner->m_download.reset(); // so isDownloading reports false
179 m_owner->installResult(Delegate::STATUS_SUCCESS);
182 virtual void onFail()
184 if (responseCode() == -1) {
185 doFailure(Delegate::USER_CANCELLED);
187 doFailure(Delegate::FAIL_DOWNLOAD);
193 void extractCurrentFile(unzFile zip, char* buffer, size_t bufferSize)
195 unz_file_info fileInfo;
196 unzGetCurrentFileInfo(zip, &fileInfo,
198 NULL, 0, /* extra field */
199 NULL, 0 /* comment field */);
201 std::string name(buffer);
202 // no absolute paths, no 'up' traversals
203 // we could also look for suspicious file extensions here (forbid .dll, .exe, .so)
204 if ((name[0] == '/') || (name.find("../") != std::string::npos) || (name.find("..\\") != std::string::npos)) {
205 throw sg_format_exception("Bad zip path", name);
208 if (fileInfo.uncompressed_size == 0) {
209 // assume it's a directory for now
210 // since we create parent directories when extracting
211 // a path, we're done here
215 int result = unzOpenCurrentFile(zip);
216 if (result != UNZ_OK) {
217 throw sg_io_exception("opening current zip file failed", sg_location(name));
220 std::ofstream outFile;
222 SGPath path(m_extractPath);
225 // create enclosing directory heirarchy as required
226 Dir parentDir(path.dir());
227 if (!parentDir.exists()) {
228 bool ok = parentDir.create(0755);
230 throw sg_io_exception("failed to create directory heirarchy for extraction", path.c_str());
234 outFile.open(path.c_str(), std::ios::binary | std::ios::trunc | std::ios::out);
235 if (outFile.fail()) {
236 throw sg_io_exception("failed to open output file for writing", path.c_str());
240 int bytes = unzReadCurrentFile(zip, buffer, bufferSize);
242 throw sg_io_exception("unzip failure reading curent archive", sg_location(name));
243 } else if (bytes == 0) {
246 outFile.write(buffer, bytes);
251 unzCloseCurrentFile(zip);
256 const std::string u(url());
257 const size_t ul(u.length());
258 if (u.rfind(".zip") == (ul - 4)) {
259 return extractUnzip();
262 if (u.rfind(".tar.gz") == (ul - 7)) {
266 SG_LOG(SG_IO, SG_WARN, "unsupported archive format:" << u);
273 zlib_filefunc_def memoryAccessFuncs;
274 fill_memory_filefunc(&memoryAccessFuncs);
276 char bufferName[128];
277 snprintf(bufferName, 128, "%p+%lx", m_buffer.data(), m_buffer.size());
278 unzFile zip = unzOpen2(bufferName, &memoryAccessFuncs);
280 const size_t BUFFER_SIZE = 32 * 1024;
281 void* buf = malloc(BUFFER_SIZE);
284 int result = unzGoToFirstFile(zip);
285 if (result != UNZ_OK) {
286 throw sg_exception("failed to go to first file in archive");
290 extractCurrentFile(zip, (char*) buf, BUFFER_SIZE);
291 result = unzGoToNextFile(zip);
292 if (result == UNZ_END_OF_LIST_OF_FILE) {
294 } else if (result != UNZ_OK) {
295 throw sg_io_exception("failed to go to next file in the archive");
298 } catch (sg_exception& ) {
309 TarExtractor tx(m_extractPath);
310 tx.extractBytes(m_buffer.data(), m_buffer.size());
311 return !tx.hasError() && tx.isAtEndOfArchive();
314 void doFailure(Delegate::StatusCode aReason)
316 Dir dir(m_extractPath);
318 dir.remove(true /* recursive */);
321 // TODO - try other mirrors
322 m_owner->m_download.reset(); // ensure we get cleaned up
323 m_owner->installResult(aReason);
329 std::string m_buffer;
330 SGPath m_extractPath;
334 ////////////////////////////////////////////////////////////////////
335 Install::Install(PackageRef aPkg, const SGPath& aPath) :
338 m_status(Delegate::STATUS_IN_PROGRESS)
341 m_package->catalog()->root()->registerInstall(this);
348 InstallRef Install::createFromPath(const SGPath& aPath, CatalogRef aCat)
350 std::string path = aPath.file();
351 PackageRef pkg = aCat->getPackageByPath(path);
353 throw sg_exception("no package with path:" + path);
355 return new Install(pkg, aPath);
358 void Install::parseRevision()
360 SGPath revisionFile = m_path;
361 revisionFile.append(".revision");
362 if (!revisionFile.exists()) {
367 std::ifstream f(revisionFile.c_str(), std::ios::in);
371 void Install::writeRevisionFile()
373 SGPath revisionFile = m_path;
374 revisionFile.append(".revision");
375 std::ofstream f(revisionFile.c_str(), std::ios::out | std::ios::trunc);
376 f << m_revision << std::endl;
379 bool Install::hasUpdate() const
381 return m_package->revision() > m_revision;
384 void Install::startUpdate()
387 return; // already active
390 m_download = new PackageArchiveDownloader(this);
391 m_package->catalog()->root()->makeHTTPRequest(m_download);
392 m_package->catalog()->root()->startInstall(this);
395 bool Install::uninstall()
398 if (!d.remove(true)) {
399 SG_LOG(SG_GENERAL, SG_ALERT, "package uninstall failed: couldn't remove path " << m_path);
403 m_package->catalog()->root()->unregisterInstall(this);
407 bool Install::isDownloading() const
409 return (m_download.valid());
412 bool Install::isQueued() const
414 return m_package->catalog()->root()->isInstallQueued(const_cast<Install*>(this));
417 int Install::downloadedPercent() const
419 if (!m_download.valid()) {
423 PackageArchiveDownloader* dl = static_cast<PackageArchiveDownloader*>(m_download.get());
424 return dl->percentDownloaded();
427 size_t Install::downloadedBytes() const
429 if (!m_download.valid()) {
433 PackageArchiveDownloader* dl = static_cast<PackageArchiveDownloader*>(m_download.get());
434 return dl->downloadedBytes();
438 void Install::cancelDownload()
440 if (m_download.valid()) {
441 m_package->catalog()->root()->cancelHTTPRequest(m_download, "User cancelled download");
444 if (m_revision == 0) {
445 SG_LOG(SG_GENERAL, SG_INFO, "cancel install of package, will unregister");
446 m_package->catalog()->root()->unregisterInstall(this);
449 m_package->catalog()->root()->cancelDownload(this);
454 PathAppender(const SGPath& p) : m_path(p) {}
456 SGPath operator()(const std::string& s) const
466 PathList Install::thumbnailPaths() const
468 const string_list& thumbs(m_package->thumbnails());
473 std::transform(thumbs.begin(), thumbs.end(),
474 std::back_inserter(result),
475 PathAppender(m_path));
479 SGPath Install::primarySetPath() const
481 SGPath setPath(m_path);
482 std::string ps(m_package->id());
483 setPath.append(ps + "-set.xml");
487 //------------------------------------------------------------------------------
488 Install* Install::done(const Callback& cb)
490 if( m_status == Delegate::STATUS_SUCCESS )
493 _cb_done.push_back(cb);
498 //------------------------------------------------------------------------------
499 Install* Install::fail(const Callback& cb)
501 if( m_status != Delegate::STATUS_SUCCESS
502 && m_status != Delegate::STATUS_IN_PROGRESS )
505 _cb_fail.push_back(cb);
510 //------------------------------------------------------------------------------
511 Install* Install::always(const Callback& cb)
513 if( m_status != Delegate::STATUS_IN_PROGRESS )
516 _cb_always.push_back(cb);
521 //------------------------------------------------------------------------------
522 Install* Install::progress(const ProgressCallback& cb)
524 _cb_progress.push_back(cb);
528 //------------------------------------------------------------------------------
529 void Install::installResult(Delegate::StatusCode aReason)
531 m_package->catalog()->root()->finishInstall(this, aReason);
532 if (aReason == Delegate::STATUS_SUCCESS) {
541 //------------------------------------------------------------------------------
542 void Install::installProgress(unsigned int aBytes, unsigned int aTotal)
544 m_package->catalog()->root()->installProgress(this, aBytes, aTotal);
545 _cb_progress(this, aBytes, aTotal);
549 } // of namespace pkg
551 } // of namespace simgear