File indexing completed on 2024-03-24 15:14:41

0001 /* GCompris - DownloadManager.cpp
0002  *
0003  * SPDX-FileCopyrightText: 2014 Holger Kaelberer <holger.k@elberer.de>
0004  *
0005  * Authors:
0006  *   Holger Kaelberer <holger.k@elberer.de>
0007  *
0008  *   SPDX-License-Identifier: GPL-3.0-or-later
0009  */
0010 
0011 #include "DownloadManager.h"
0012 #include "ApplicationSettings.h"
0013 #include "ApplicationInfo.h"
0014 
0015 #include <QFile>
0016 #include <QDir>
0017 #include <QResource>
0018 #include <QStandardPaths>
0019 #include <QMutexLocker>
0020 #include <QNetworkConfiguration>
0021 #include <QDirIterator>
0022 #include <QCoreApplication>
0023 
0024 const QString DownloadManager::contentsFilename = QLatin1String("Contents");
0025 DownloadManager *DownloadManager::_instance = nullptr;
0026 
0027 const QString DownloadManager::localFolderForData = QLatin1String("data3/");
0028 /* Public interface: */
0029 
0030 DownloadManager::DownloadManager() :
0031     accessManager(this), serverUrl(ApplicationSettings::getInstance()->downloadServerUrl())
0032 {
0033     // Manually add the "ISRG Root X1" certificate to download on older android
0034     // Check https://bugs.kde.org/show_bug.cgi?id=447572 for more info
0035 #if defined(Q_OS_ANDROID)
0036     // Starting Qt5.15, we can directly use addCaCertificate (https://doc.qt.io/qt-5/qsslconfiguration.html#addCaCertificate)
0037     auto sslConfig = QSslConfiguration::defaultConfiguration();
0038     QList<QSslCertificate> certificates = sslConfig.caCertificates();
0039     // Certificate downloaded from https://letsencrypt.org/certificates/#root-certificates
0040     QFile file(":/gcompris/src/core/resource/isrgrootx1.pem");
0041     QIODevice::OpenMode openMode = QIODevice::ReadOnly | QIODevice::Text;
0042     if (!file.open(openMode)) {
0043         qDebug() << "Error opening " << file;
0044     }
0045     else {
0046         certificates << QSslCertificate::fromData(file.readAll(), QSsl::Pem);
0047         sslConfig.setCaCertificates(certificates);
0048         QSslConfiguration::setDefaultConfiguration(sslConfig);
0049     }
0050 #endif
0051 }
0052 
0053 DownloadManager::~DownloadManager()
0054 {
0055     shutdown();
0056     _instance = nullptr;
0057 }
0058 
0059 void DownloadManager::shutdown()
0060 {
0061     qDebug() << "DownloadManager: shutting down," << activeJobs.size() << "active jobs";
0062     abortDownloads();
0063 }
0064 
0065 // It is not recommended to create a singleton of Qml Singleton registered
0066 // object but we could not found a better way to let us access DownloadManager
0067 // on the C++ side. All our test shows that it works.
0068 // Using the singleton after the QmlEngine has been destroyed is forbidden!
0069 DownloadManager *DownloadManager::getInstance()
0070 {
0071     if (_instance == nullptr)
0072         _instance = new DownloadManager;
0073     return _instance;
0074 }
0075 
0076 QObject *DownloadManager::downloadManagerProvider(QQmlEngine *engine,
0077                                                   QJSEngine *scriptEngine)
0078 {
0079     Q_UNUSED(engine)
0080     Q_UNUSED(scriptEngine)
0081 
0082     return getInstance();
0083 }
0084 
0085 bool DownloadManager::downloadIsRunning() const
0086 {
0087     return !activeJobs.empty();
0088 }
0089 
0090 void DownloadManager::abortDownloads()
0091 {
0092     if (downloadIsRunning()) {
0093         QMutexLocker locker(&jobsMutex);
0094         QMutableListIterator<DownloadJob *> iter(activeJobs);
0095         while (iter.hasNext()) {
0096             DownloadJob *job = iter.next();
0097             if (!job->downloadFinished && job->reply != nullptr) {
0098                 disconnect(job->reply, SIGNAL(finished()), this, SLOT(finishDownload()));
0099                 disconnect(job->reply, SIGNAL(error(QNetworkReply::NetworkError)),
0100                            this, SLOT(handleError(QNetworkReply::NetworkError)));
0101                 if (job->reply->isRunning()) {
0102                     qDebug() << "Aborting download job:" << job->url << job->resourceType;
0103                     job->reply->abort();
0104 
0105                     // remove new Contents file before removing the new rcc
0106                     QFileInfo fi(job->file.fileName());
0107                     QDir dir = fi.dir(); // Get the directory of the file
0108                     // Get the filepath of new Contents file
0109                     QString newContentsFilePath = tempFilenameForFilename(dir.filePath(contentsFilename));
0110                     // remove new Contents file if it exists
0111                     if (QFile::exists(newContentsFilePath) && !QFile::remove(newContentsFilePath))
0112                         qWarning() << "Error while removing new Contents file" << newContentsFilePath;
0113 
0114                     // close and remove new rcc file
0115                     job->file.close();
0116                     job->file.remove();
0117                 }
0118                 job->reply->deleteLater();
0119             }
0120             delete job;
0121             iter.remove();
0122         }
0123         locker.unlock();
0124         Q_EMIT error(QNetworkReply::OperationCanceledError, QObject::tr("Download cancelled by user"));
0125     }
0126 }
0127 
0128 void DownloadManager::initializeAssets()
0129 {
0130     const QStringList files = {
0131         localFolderForData + QLatin1String("Contents"),
0132         localFolderForData + QLatin1String("voices-" COMPRESSED_AUDIO "/Contents"),
0133         localFolderForData + QLatin1String("backgroundMusic/Contents"),
0134         localFolderForData + QLatin1String("words/Contents")
0135     };
0136     for (const QString &contentsFolder: files) {
0137         QFileInfo fi(getAbsoluteResourcePath(contentsFolder));
0138         if (fi.exists()) {
0139             DownloadJob job(GCompris::ResourceType::NONE);
0140             job.file.setFileName(fi.filePath());
0141             parseContents(&job);
0142         }
0143         else {
0144             qDebug() << fi.filePath() << "does not exist, cannot parse Contents for " << contentsFolder;
0145         }
0146     }
0147 
0148     connect(this, &DownloadManager::allDownloadsFinished, this, &DownloadManager::finishAllDownloads);
0149 }
0150 
0151 QString DownloadManager::getVoicesResourceForLocale(const QString &locale) const
0152 {
0153     const QString localeName = ApplicationInfo::getInstance()->getVoicesLocale(locale);
0154     qDebug() << "Get voices for " << locale << ", " << localeName;
0155     return getResourcePath(GCompris::ResourceType::VOICES, { { "locale", localeName } });
0156 }
0157 
0158 QString DownloadManager::getBackgroundMusicResources() const
0159 {
0160     return getResourcePath(GCompris::ResourceType::BACKGROUND_MUSIC, {});
0161 }
0162 
0163 inline QString DownloadManager::getAbsoluteResourcePath(const QString &path) const
0164 {
0165     for (const QString &base: getSystemResourcePaths()) {
0166         if (QFile::exists(base + '/' + path))
0167             return QString(base + '/' + path);
0168     }
0169     return QString();
0170 }
0171 
0172 // @FIXME should support a variable subpath length like data2/full.rcc"
0173 inline QString DownloadManager::getRelativeResourcePath(const QString &path) const
0174 {
0175 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
0176     QStringList parts = path.split('/', Qt::SkipEmptyParts);
0177 #else
0178     QStringList parts = path.split('/', QString::SkipEmptyParts);
0179 #endif
0180     if (parts.size() < 3)
0181         return QString();
0182     return QString(parts[parts.size() - 3] + '/' + parts[parts.size() - 2]
0183                    + '/' + parts[parts.size() - 1]);
0184 }
0185 
0186 bool DownloadManager::haveLocalResource(const QString &path) const
0187 {
0188     return (!getAbsoluteResourcePath(path).isEmpty());
0189 }
0190 
0191 QString DownloadManager::getLocalSubFolderForData(const GCompris::ResourceType &rt) const
0192 {
0193     switch (rt) {
0194     case GCompris::ResourceType::WORDSET:
0195         return localFolderForData + QLatin1String("words/");
0196         break;
0197     case GCompris::ResourceType::BACKGROUND_MUSIC:
0198         return localFolderForData + QLatin1String("backgroundMusic/");
0199         break;
0200     case GCompris::ResourceType::VOICES:
0201         return localFolderForData + QLatin1String("voices-" COMPRESSED_AUDIO "/");
0202         break;
0203     case GCompris::ResourceType::FULL:
0204         return localFolderForData;
0205         break;
0206     }
0207     return QString();
0208 }
0209 
0210 QString DownloadManager::getResourcePath(/*const GCompris::ResourceType &rt*/ int rt, const QVariantMap &data) const
0211 {
0212     // qDebug() << "getResourcePath" << GCompris::ResourceType(rt) << resourceTypeToLocalFileName << data["locale"].toString();
0213     GCompris::ResourceType resource = GCompris::ResourceType(rt);
0214     switch (resource) {
0215     case GCompris::ResourceType::WORDSET:
0216         if (resourceTypeToLocalFileName.contains("words")) {
0217             return getLocalSubFolderForData(resource) + resourceTypeToLocalFileName["words"];
0218         }
0219         break;
0220     case GCompris::ResourceType::BACKGROUND_MUSIC:
0221         if (resourceTypeToLocalFileName.contains("backgroundMusic-" COMPRESSED_AUDIO)) {
0222             return getLocalSubFolderForData(resource) + resourceTypeToLocalFileName["backgroundMusic-" COMPRESSED_AUDIO];
0223         }
0224         break;
0225     case GCompris::ResourceType::VOICES:
0226         if (resourceTypeToLocalFileName.contains(data["locale"].toString())) {
0227             return getLocalSubFolderForData(resource) + resourceTypeToLocalFileName[data["locale"].toString()];
0228         }
0229         break;
0230     case GCompris::ResourceType::FULL:
0231         if (resourceTypeToLocalFileName.contains("full-" COMPRESSED_AUDIO)) {
0232             return getLocalSubFolderForData(resource) + resourceTypeToLocalFileName["full-" COMPRESSED_AUDIO];
0233         }
0234         break;
0235     }
0236     return QString();
0237 }
0238 
0239 bool DownloadManager::updateResource(/*const GCompris::ResourceType &*/ int rt, const QVariantMap &extra)
0240 {
0241     if (checkDownloadRestriction())
0242         return downloadResource(rt, extra); // check for updates and register
0243 
0244     QString resourcePath = getResourcePath(rt, extra);
0245     QString absPath = getAbsoluteResourcePath(resourcePath);
0246     // automatic download prohibited -> register if available
0247     if (!resourcePath.isEmpty())
0248         return registerResourceAbsolute(absPath);
0249 
0250     qDebug() << "No such local resource and download prohibited: "
0251              << GCompris::ResourceType(rt) << extra;
0252     return false;
0253 }
0254 
0255 bool DownloadManager::downloadResource(int rt, const QVariantMap &extra)
0256 {
0257     DownloadJob *job = nullptr;
0258     GCompris::ResourceType resourceType = GCompris::ResourceType(rt);
0259     {
0260         QMutexLocker locker(&jobsMutex);
0261         if (getJobByType_locked(resourceType, extra) != nullptr) {
0262             qDebug() << "Download of" << resourceType << "is already running, skipping second attempt.";
0263             return false;
0264         }
0265         job = new DownloadJob(resourceType, extra);
0266         activeJobs.append(job);
0267     }
0268 
0269     qDebug() << "downloadResource" << resourceType << extra;
0270     if (!download(job)) {
0271         QMutexLocker locker(&jobsMutex);
0272         activeJobs.removeOne(job);
0273         return false;
0274     }
0275     return true;
0276 }
0277 
0278 /* Private: */
0279 
0280 inline QString DownloadManager::tempFilenameForFilename(const QString &filename) const
0281 {
0282     return QString(filename).append("_");
0283 }
0284 
0285 inline QString DownloadManager::filenameForTempFilename(const QString &tempFilename) const
0286 {
0287     if (tempFilename.endsWith(QLatin1String("_")))
0288         return tempFilename.left(tempFilename.length() - 1);
0289     return tempFilename;
0290 }
0291 
0292 bool DownloadManager::download(DownloadJob *job)
0293 {
0294     QNetworkRequest request;
0295 
0296     // First download Contents file for verification if not yet done:
0297     if (!job->contents.contains(job->url.fileName()) /*|| job->url.fileName().contains("Contents")*/) {
0298         QUrl contentsUrl;
0299         if (!job->url.isEmpty()) {
0300             int len = job->url.fileName().length();
0301             contentsUrl = QUrl(job->url.toString().remove(job->url.toString().length() - len, len)
0302                                + contentsFilename);
0303         }
0304         else {
0305             switch (job->resourceType) {
0306             case GCompris::ResourceType::BACKGROUND_MUSIC:
0307                 contentsUrl = QUrl(serverUrl.toString() + '/' + localFolderForData + "backgroundMusic/" + contentsFilename);
0308                 break;
0309             case GCompris::ResourceType::VOICES:
0310                 contentsUrl = QUrl(serverUrl.toString() + '/' + localFolderForData + "voices-" COMPRESSED_AUDIO + '/' + contentsFilename);
0311                 break;
0312             case GCompris::ResourceType::WORDSET:
0313                 contentsUrl = QUrl(serverUrl.toString() + '/' + localFolderForData + "words/" + contentsFilename);
0314                 break;
0315             default:
0316                 break;
0317             }
0318         }
0319         if (!job->knownContentsUrls.contains(contentsUrl)) {
0320             // Note: need to track already tried Contents files or we can end
0321             // up in an infinite loop if corresponding Contents file does not
0322             // exist upstream
0323             job->knownContentsUrls.append(contentsUrl);
0324             // qDebug() << "Postponing rcc download, first fetching Contents" << contentsUrl;
0325             job->queue.prepend(job->url);
0326             job->url = contentsUrl;
0327         }
0328     }
0329 
0330     QFileInfo fi(getFilenameForUrl(job->url));
0331     // make sure target path exists:
0332     QDir dir;
0333     if (!dir.exists(fi.path()) && !dir.mkpath(fi.path())) {
0334         // qDebug() << "Could not create resource path " << fi.path();
0335         Q_EMIT error(QNetworkReply::ProtocolUnknownError, QObject::tr("Could not create resource path"));
0336         return false;
0337     }
0338 
0339     job->file.setFileName(tempFilenameForFilename(fi.filePath()));
0340     if (!job->file.open(QIODevice::WriteOnly)) {
0341         Q_EMIT error(QNetworkReply::ProtocolUnknownError,
0342                      QObject::tr("Could not open target file %1").arg(job->file.fileName()));
0343         return false;
0344     }
0345 
0346     // start download:
0347     request.setUrl(job->url);
0348     qDebug() << "Now downloading" << job->url << "to" << fi.filePath() << "...";
0349     QNetworkReply *reply = accessManager.get(request);
0350     job->reply = reply;
0351     connect(reply, SIGNAL(finished()), this, SLOT(finishDownload()));
0352     connect(reply, &QNetworkReply::readyRead, this, &DownloadManager::downloadReadyRead);
0353     connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
0354             this, SLOT(handleError(QNetworkReply::NetworkError)));
0355     if (job->url.fileName() != contentsFilename) {
0356         connect(reply, &QNetworkReply::downloadProgress,
0357                 this, &DownloadManager::downloadInProgress);
0358         Q_EMIT downloadStarted(job->url.toString().remove(0, serverUrl.toString().length()));
0359     }
0360 
0361     return true;
0362 }
0363 
0364 inline DownloadManager::DownloadJob *DownloadManager::getJobByType_locked(GCompris::ResourceType rt, const QVariantMap &data) const
0365 {
0366     for (auto activeJob: activeJobs)
0367         if (activeJob->resourceType == rt && activeJob->extraInfos == data) // || activeJob->queue.indexOf(url) != -1)
0368             return activeJob;
0369     return nullptr;
0370 }
0371 
0372 inline DownloadManager::DownloadJob *DownloadManager::getJobByReply(QNetworkReply *r)
0373 {
0374     QMutexLocker locker(&jobsMutex);
0375     for (auto activeJob: qAsConst(activeJobs))
0376         if (activeJob->reply == r)
0377             return activeJob;
0378     return nullptr; // should never happen!
0379 }
0380 
0381 void DownloadManager::downloadReadyRead()
0382 {
0383     QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
0384     DownloadJob *job = getJobByReply(reply);
0385     job->file.write(reply->readAll());
0386 }
0387 
0388 inline QString DownloadManager::getFilenameForUrl(const QUrl &url) const
0389 {
0390     QString relPart = url.toString().remove(0, serverUrl.toString().length());
0391     return QString(getSystemDownloadPath() + relPart);
0392 }
0393 
0394 inline QUrl DownloadManager::getUrlForFilename(const QString &filename) const
0395 {
0396     return QUrl(serverUrl.toString() + '/' + getRelativeResourcePath(filename));
0397 }
0398 
0399 inline QString DownloadManager::getSystemDownloadPath() const
0400 {
0401     return ApplicationSettings::getInstance()->cachePath();
0402 }
0403 
0404 inline const QStringList DownloadManager::getSystemResourcePaths() const
0405 {
0406 
0407     QStringList results({
0408         QCoreApplication::applicationDirPath() + '/' + QString(GCOMPRIS_DATA_FOLDER) + "/rcc/",
0409             getSystemDownloadPath(),
0410             QStandardPaths::writableLocation(QStandardPaths::CacheLocation),
0411 #if defined(Q_OS_ANDROID)
0412             "assets:",
0413 #endif
0414 #if defined(UBUNTUTOUCH)
0415             QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + '/' + GCOMPRIS_APPLICATION_NAME
0416 #else
0417             QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + '/' + GCOMPRIS_APPLICATION_NAME
0418 #endif
0419     });
0420 
0421     // Append standard application directories (like /usr/share/KDE/gcompris-qt)
0422     results += QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);
0423 
0424     return results;
0425 }
0426 
0427 bool DownloadManager::checkDownloadRestriction() const
0428 {
0429 #if 0
0430     // note: Something like the following can be used once bearer mgmt
0431     // has been implemented for android (cf. Qt bug #30394)
0432     QNetworkConfiguration::BearerType conn = networkConfiguration.bearerType();
0433     qDebug() << "Bearer type: "<<  conn << ": "<< networkConfiguration.bearerTypeName();
0434     if (!ApplicationSettings::getInstance()->isMobileNetworkDownloadsEnabled() &&
0435         conn != QNetworkConfiguration::BearerEthernet &&
0436         conn != QNetworkConfiguration::QNetworkConfiguration::BearerWLAN)
0437         return false;
0438     return true;
0439 #endif
0440     return ApplicationSettings::getInstance()->isAutomaticDownloadsEnabled() && ApplicationInfo::getInstance()->isDownloadAllowed();
0441 }
0442 
0443 void DownloadManager::handleError(QNetworkReply::NetworkError code)
0444 {
0445     Q_UNUSED(code);
0446     QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
0447     qDebug() << reply->errorString() << " " << reply->error();
0448     Q_EMIT error(reply->error(), reply->errorString());
0449 }
0450 
0451 bool DownloadManager::parseContents(DownloadJob *job)
0452 {
0453     if (job->file.isOpen())
0454         job->file.close();
0455 
0456     if (!job->file.open(QIODevice::ReadOnly | QIODevice::Text)) {
0457         qWarning() << "Could not open file " << job->file.fileName();
0458         return false;
0459     }
0460 
0461     /*
0462      * We expect the line-syntax, that md5sum and colleagues creates:
0463      * <MD5SUM>  <FILENAME>
0464      * 53f0a3eb206b3028500ca039615c5f84  voices-en.rcc
0465      */
0466     QTextStream in(&job->file);
0467     while (!in.atEnd()) {
0468         QString line = in.readLine();
0469 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
0470         QStringList parts = line.split(' ', Qt::SkipEmptyParts);
0471 #else
0472         QStringList parts = line.split(' ', QString::SkipEmptyParts);
0473 #endif
0474         if (parts.size() != 2) {
0475             qWarning() << "Invalid format of Contents file!";
0476             return false;
0477         }
0478         job->contents[parts[1]] = parts[0];
0479         if (parts[1].startsWith(QLatin1String("voices"))) {
0480             QString locale = parts[1];
0481             locale.remove(0, locale.indexOf('-') + 1); // Remove "voices-" prefix
0482             locale = locale.left(locale.indexOf('-')); // Remove the date and ".rcc" suffix
0483             // Retrieve locale from filename
0484             resourceTypeToLocalFileName[locale] = parts[1];
0485             qDebug() << "Contents: " << locale << " -> " << parts[1];
0486         }
0487         else if (parts[1].startsWith(QLatin1String("words"))) {
0488             QString type = parts[1];
0489             type = type.left(type.indexOf('-'));
0490             resourceTypeToLocalFileName[type] = parts[1];
0491             qDebug() << "Contents: " << type << " -> " << parts[1];
0492         }
0493         else {
0494             QString type = parts[1];
0495             type = type.section('-', 0, 1); // keep backgroundMusic-$CA or full-$CA
0496             resourceTypeToLocalFileName[type] = parts[1];
0497             qDebug() << "Contents: " << type << " -> " << parts[1];
0498         }
0499         // qDebug() << "Contents: " << parts[1] << " -> " << parts[0];
0500     }
0501     job->file.close();
0502     return true;
0503 }
0504 
0505 bool DownloadManager::checksumMatches(DownloadJob *job, const QString &filename) const
0506 {
0507     Q_ASSERT(!job->contents.empty());
0508 
0509     if (!QFile::exists(filename))
0510         return false;
0511 
0512     QString basename = QFileInfo(filename).fileName();
0513     if (!job->contents.contains(basename))
0514         return false;
0515 
0516     QFile file(filename);
0517     file.open(QIODevice::ReadOnly);
0518     QCryptographicHash fileHasher(hashMethod);
0519     if (!fileHasher.addData(&file)) {
0520         qWarning() << "Could not read file for hashing: " << filename;
0521         return false;
0522     }
0523     file.close();
0524     QByteArray fileHash = fileHasher.result().toHex();
0525     // qDebug() << "Checking file-hash ~ contents-hash: " << fileHash << " ~ " << job->contents[basename];
0526 
0527     return (fileHash == job->contents[basename]);
0528 }
0529 
0530 void DownloadManager::unregisterResource_locked(const QString &filename)
0531 {
0532     if (!QResource::unregisterResource(filename))
0533         qDebug() << "Error unregistering resource file" << filename;
0534     else {
0535         qDebug() << "Successfully unregistered resource file" << filename;
0536         registeredResources.removeOne(filename);
0537     }
0538 }
0539 
0540 inline bool DownloadManager::isRegistered(const QString &filename) const
0541 {
0542     return (registeredResources.indexOf(filename) != -1);
0543 }
0544 
0545 /*
0546  * Registers an rcc file given by absolute path
0547  */
0548 bool DownloadManager::registerResourceAbsolute(const QString &filename)
0549 {
0550     QMutexLocker locker(&rcMutex);
0551     if (isRegistered(filename))
0552         unregisterResource_locked(filename);
0553 
0554     if (!QResource::registerResource(filename)) {
0555         qDebug() << "Error registering resource file" << filename;
0556         return false;
0557     }
0558 
0559     qDebug() << "Successfully registered resource"
0560              << filename;
0561     registeredResources.append(filename);
0562 
0563     locker.unlock(); /* note: we unlock before emitting to prevent
0564                       * potential deadlocks */
0565 
0566     const QString relativeFilename = getRelativeResourcePath(filename);
0567     Q_EMIT resourceRegistered(relativeFilename);
0568 
0569     QString v = getVoicesResourceForLocale(
0570         ApplicationSettings::getInstance()->locale());
0571     QString musicPath = getBackgroundMusicResources();
0572 
0573     if (v == relativeFilename)
0574         Q_EMIT voicesRegistered();
0575     else if (musicPath == relativeFilename)
0576         Q_EMIT backgroundMusicRegistered();
0577     return true;
0578 }
0579 
0580 /*
0581  * Registers an rcc file given by a relative resource path
0582  */
0583 bool DownloadManager::registerResource(const QString &filename)
0584 {
0585     QString absPath = getAbsoluteResourcePath(filename);
0586     if (!absPath.isEmpty()) {
0587         return registerResourceAbsolute(getAbsoluteResourcePath(filename));
0588     }
0589     else {
0590         qDebug() << "Error while registering" << filename;
0591         return false;
0592     }
0593 }
0594 
0595 bool DownloadManager::isDataRegistered(const QString &data) const
0596 {
0597     QString res = QString(":/gcompris/data/%1").arg(data);
0598     return !QDir(res).entryList().empty();
0599 }
0600 
0601 bool DownloadManager::areVoicesRegistered(const QString &locale) const
0602 {
0603     QString resource = QString("voices-" COMPRESSED_AUDIO "/%1").arg(ApplicationInfo::getInstance()->getVoicesLocale(locale));
0604     return isDataRegistered(resource);
0605 }
0606 
0607 void DownloadManager::downloadInProgress(qint64 bytesReceived, qint64 bytesTotal)
0608 {
0609     QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
0610     DownloadJob *job = nullptr;
0611     // don't call getJobByReply to not cause deadlock with mutex
0612     for (auto activeJob: qAsConst(activeJobs)) {
0613         if (activeJob->reply == reply) {
0614             job = activeJob;
0615             break;
0616         }
0617     }
0618     if (!job) {
0619         return;
0620     }
0621     job->bytesReceived = bytesReceived;
0622     job->bytesTotal = bytesTotal;
0623     qint64 allJobsBytesReceived = 0;
0624     qint64 allJobsBytesTotal = 0;
0625     for (auto activeJob: qAsConst(activeJobs)) {
0626         allJobsBytesReceived += activeJob->bytesReceived;
0627         allJobsBytesTotal += activeJob->bytesTotal;
0628     }
0629     Q_EMIT downloadProgress(allJobsBytesReceived, allJobsBytesTotal);
0630 }
0631 
0632 void DownloadManager::finishAllDownloads(int code)
0633 {
0634     QList<QString> registeredFiles = resourceTypeToLocalFileName.values();
0635     // Remove all previous rcc for this kind of download
0636     for (auto *job: activeJobs) {
0637         QString subfolder = getLocalSubFolderForData(job->resourceType);
0638         if (subfolder.isEmpty()) {
0639             continue;
0640         }
0641         QDirIterator it(getSystemDownloadPath() + '/' + subfolder);
0642         while (it.hasNext()) {
0643             QString filename = it.next();
0644             QFileInfo fi = it.fileInfo();
0645             if (fi.isFile() && (filename.endsWith(QLatin1String(".rcc")))) {
0646                 if (!registeredFiles.contains(fi.fileName())) {
0647                     if (!QFile::remove(filename)) {
0648                         qDebug() << "Unable to remove" << filename;
0649                     }
0650                 }
0651             }
0652         }
0653         delete job;
0654     }
0655     activeJobs.clear();
0656 }
0657 
0658 void DownloadManager::finishDownload()
0659 {
0660     QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
0661     DownloadFinishedCode code = Success;
0662     DownloadJob *job = getJobByReply(reply);
0663     QString targetFilename = getFilenameForUrl(job->url);
0664     bool allFinished = false;
0665     if (job->file.isOpen()) {
0666         job->file.flush(); // note: important, or checksums might be wrong!
0667         job->file.close();
0668     }
0669 
0670     if (reply->error() != 0 && job->file.exists()) {
0671         job->file.remove();
0672     }
0673     else if (job->url.fileName() != contentsFilename) {
0674         // remove old rcc file and rename the new file
0675         // remove old Contents file, rename the new Contents file
0676 
0677         // get corresponding content filename
0678         QFileInfo fi(targetFilename);
0679         QDir dir = fi.dir(); // Get the directory of the file
0680         QString contentsFilePath = dir.filePath(contentsFilename); // Get the filepath of Contents file
0681         QString newContentsFilePath = tempFilenameForFilename(contentsFilePath);
0682 
0683         if (QFile::exists(targetFilename)) {
0684             QFile::remove(targetFilename);
0685         }
0686         if (!job->file.rename(targetFilename)) {
0687             qWarning() << "Could not rename temporary file to" << targetFilename;
0688             if (!QFile::remove(newContentsFilePath))
0689                 qWarning() << "Could not remove new Contents file" << newContentsFilePath;
0690         }
0691         else {
0692             // new rcc placed at the required path, now place new contents file
0693             if (QFile::exists(contentsFilePath) && !QFile::remove(contentsFilePath))
0694                 qWarning() << "Could not remove old Contents file" << contentsFilePath;
0695             else if (!QFile::rename(newContentsFilePath, contentsFilePath))
0696                 qWarning() << "Could not rename new Contents file to " << contentsFilePath;
0697         }
0698     }
0699 
0700     if (job->url.fileName() == contentsFilename) {
0701         // Contents
0702         if (reply->error() != 0) {
0703             qWarning() << "Error downloading Contents from" << job->url
0704                        << ":" << reply->error() << ":" << reply->errorString();
0705             // note: errorHandler() emit's error!
0706             goto outError;
0707         }
0708         // qDebug() << "Download of Contents finished successfully: " << job->url;
0709         if (!parseContents(job)) {
0710             qWarning() << "Invalid format of Contents file" << job->url;
0711             Q_EMIT error(QNetworkReply::UnknownContentError, QObject::tr("Invalid format of Contents file"));
0712             goto outError;
0713         }
0714     }
0715     else {
0716         QUrl redirect = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
0717         // RCC file
0718         if (reply->error() != 0) {
0719             qWarning() << "Error downloading RCC file from " << job->url
0720                        << ":" << reply->error() << ":" << reply->errorString();
0721             // note: errorHandler() emit's error!
0722             code = Error;
0723             // register already existing files (if not yet done):
0724             if (QFile::exists(targetFilename) && !isRegistered(targetFilename))
0725                 registerResourceAbsolute(targetFilename);
0726         }
0727         // In case the file does not exist on the server, it is redirected to
0728         // an error page and this error page is downloaded but on our case
0729         // this is an error as we don't have the expected rcc.
0730         else if (!redirect.isEmpty()) {
0731             qWarning() << QString("The url %1 does not exist.").arg(job->url.toString());
0732             Q_EMIT error(QNetworkReply::UnknownContentError,
0733                          QObject::tr("The url %1 does not exist.")
0734                              .arg(job->url.toString()));
0735             code = Error;
0736             if (QFile::exists(targetFilename)) {
0737                 QFile::remove(targetFilename);
0738             }
0739         }
0740         else {
0741             qDebug() << "Download of RCC file finished successfully: " << job->url;
0742             if (!checksumMatches(job, targetFilename)) {
0743                 qWarning() << "Checksum of downloaded file does not match: "
0744                            << targetFilename;
0745                 Q_EMIT error(QNetworkReply::UnknownContentError,
0746                              QObject::tr("Checksum of downloaded file does not match: %1")
0747                                  .arg(targetFilename));
0748                 code = Error;
0749             }
0750             else
0751                 registerResourceAbsolute(targetFilename);
0752         }
0753     }
0754 
0755     // try next:
0756     while (!job->queue.isEmpty()) {
0757         job->url = job->queue.takeFirst();
0758         if (job->url.isEmpty() && job->resourceType != GCompris::ResourceType::NONE) {
0759             QString nextDownload = getResourcePath(job->resourceType, job->extraInfos);
0760             if (!nextDownload.isEmpty()) {
0761                 job->url = QUrl(serverUrl.toString() + '/' + nextDownload);
0762             }
0763             else {
0764                 if (job->resourceType == GCompris::ResourceType::VOICES) {
0765                     const QString localeName = job->extraInfos["locale"].toString();
0766                     const QLocale locale(localeName);
0767                     Q_EMIT error(QNetworkReply::UnknownContentError,
0768                                  QObject::tr("No voices found for %1.")
0769                                      .arg(locale.nativeLanguageName()));
0770                 }
0771                 else {
0772                     Q_EMIT error(QNetworkReply::UnknownContentError,
0773                                  QObject::tr("No data found for %1.")
0774                                      .arg(GCompris::ResourceType(job->resourceType)));
0775                 }
0776                 code = Error;
0777                 continue;
0778             }
0779         }
0780         QString relPath = getRelativeResourcePath(getFilenameForUrl(job->url));
0781         // check in each resource-path for an up2date rcc file:
0782         for (const QString &base: getSystemResourcePaths()) {
0783             QString filename = base + '/' + relPath;
0784             if (QFile::exists(filename)
0785                 && checksumMatches(job, filename)) {
0786                 // file is up2date, register! necessary:
0787                 qDebug() << "Local resource is up-to-date:"
0788                          << QFileInfo(filename).fileName();
0789                 if (!isRegistered(filename)) // no update and already registered -> noop
0790                     registerResourceAbsolute(filename);
0791                 code = NoChange;
0792                 break;
0793             }
0794         }
0795         if (code != NoChange) {
0796             // nothing is up2date locally -> download it
0797             if (download(job))
0798                 goto outNext;
0799         }
0800         else {
0801             // data has not changed, remove corresponding Contents file
0802             // it is usually removed when rcc is downloaded
0803             // in this case, as data has not changed, rcc won't be downloaded
0804             QFileInfo fi(job->file.fileName());
0805             QDir dir = fi.dir(); // Get the directory of the file
0806             // Get the filepath of new Contents file
0807             QString newContentsFilePath = tempFilenameForFilename(dir.filePath(contentsFilename));
0808 
0809             if (!QFile::remove(newContentsFilePath)) {
0810                 qWarning() << "Could not remove new Contents file" << newContentsFilePath;
0811             }
0812         }
0813     }
0814 
0815     // none left, DownloadJob finished
0816     job->downloadFinished = true;
0817     job->downloadResult = code;
0818     if (job->file.isOpen())
0819         job->file.close();
0820     Q_EMIT downloadFinished(code);
0821     reply->deleteLater();
0822 
0823     allFinished = std::all_of(activeJobs.constBegin(), activeJobs.constEnd(),
0824                               [](const DownloadJob *job) { return job->downloadFinished; });
0825     if (allFinished) {
0826         QMutexLocker locker(&jobsMutex);
0827         DownloadFinishedCode allCode = Success;
0828         std::for_each(activeJobs.constBegin(), activeJobs.constEnd(),
0829                       [&allCode](const DownloadJob *job) {
0830                           if (job->downloadResult == Error)
0831                               allCode = Error;
0832                       });
0833         Q_EMIT allDownloadsFinished(allCode);
0834     }
0835     return;
0836 
0837 outError:
0838     if (job->url.fileName() == contentsFilename) {
0839         // if we could not download the contents file register local existing
0840         // files for outstanding jobs:
0841         QUrl nUrl;
0842         while (!job->queue.isEmpty()) {
0843             nUrl = job->queue.takeFirst();
0844             QString relPath = getResourcePath(job->resourceType, job->extraInfos);
0845             for (const QString &base: getSystemResourcePaths()) {
0846                 QString filename = base + '/' + relPath;
0847                 if (QFile::exists(filename))
0848                     registerResourceAbsolute(filename);
0849             }
0850         }
0851     }
0852     else {
0853         // restore old Contents file
0854         QFileInfo fi(job->file.fileName());
0855         QDir dir = fi.dir(); // Get the directory of the file
0856         // Get the filepath of new Contents file
0857         QString newContentsFilePath = tempFilenameForFilename(dir.filePath(contentsFilename));
0858 
0859         if (!QFile::remove(newContentsFilePath)) {
0860             qWarning() << "Could not remove new Contents file" << newContentsFilePath;
0861         }
0862     }
0863 
0864     if (job->file.isOpen())
0865         job->file.close();
0866 
0867     {
0868         QMutexLocker locker(&jobsMutex);
0869         activeJobs.removeOne(job);
0870     }
0871 
0872     reply->deleteLater();
0873     delete job;
0874     return;
0875 
0876 outNext:
0877     // next sub-job started
0878     reply->deleteLater();
0879     return;
0880 }
0881 
0882 #if 0
0883 // vvv might be helpful later with other use-cases:
0884 void DownloadManager::registerLocalResources()
0885 {
0886     QStringList filenames = getLocalResources();
0887     if (filenames.empty()) {
0888         qDebug() << "No local resources found";
0889         return;
0890     }
0891 
0892     QList<QString>::const_iterator iter;
0893     for (iter = filenames.constBegin(); iter != filenames.constEnd(); iter++)
0894         registerResource(*iter);
0895 }
0896 
0897 bool DownloadManager::checkForUpdates()
0898 {
0899     QStringList filenames = getLocalResources();
0900     if (filenames.empty()) {
0901         qDebug() << "No local resources found";
0902         return true;
0903     }
0904 
0905     if (!checkDownloadRestriction()) {
0906         qDebug() << "Can't download with current network connection (" <<
0907                 networkConfiguration.bearerTypeName() << ")!";
0908         return false;
0909     }
0910 
0911     QList<QString>::const_iterator iter;
0912     DownloadJob *job = new DownloadJob();
0913     for (iter = filenames.constBegin(); iter != filenames.constEnd(); iter++) {
0914         QUrl url = getUrlForFilename(*iter);
0915         qDebug() << "Scheduling resource for update: " << url;
0916         job->queue.append(url);
0917     }
0918     job->url = job->queue.takeFirst();
0919 
0920     {
0921         QMutexLocker locker(&jobsMutex);
0922         activeJobs.append(job);
0923     }
0924 
0925     if (!download(job)) {
0926         QMutexLocker locker(&jobsMutex);
0927         activeJobs.removeOne(job);
0928         return false;
0929     }
0930     return true;
0931 
0932 }
0933 
0934 QStringList DownloadManager::getLocalResources()
0935 {
0936     QStringList result;
0937 
0938     for (const QString &path : getSystemResourcePaths()) {
0939         QDir dir(path);
0940         if (!dir.exists(path) && !dir.mkpath(path)) {
0941             qWarning() << "Could not create resource path " << path;
0942             continue;
0943         }
0944 
0945         QDirIterator it(dir, QDirIterator::Subdirectories);
0946         while (it.hasNext()) {
0947             QString filename = it.next();
0948             QFileInfo fi = it.fileInfo();
0949             if (fi.isFile() &&
0950                     (filename.endsWith(QLatin1String(".rcc"))))
0951                 result.append(filename);
0952         }
0953     }
0954     return result;
0955 }
0956 #endif
0957 
0958 #include "moc_DownloadManager.cpp"