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"