22 #include "DownloadManager.h"
23 #include "ApplicationSettings.h"
24 #include "ApplicationInfo.h"
29 #include <QStandardPaths>
30 #include <QMutexLocker>
31 #include <QNetworkConfiguration>
32 #include <QDirIterator>
34 const QString DownloadManager::contentsFilename = QString(
"Contents");
39 DownloadManager::DownloadManager()
43 QDir previousDataLocation = QStandardPaths::writableLocation(QStandardPaths::DataLocation) +
"/data";
44 if(previousDataLocation.exists()) {
45 qDebug() <<
"Remove previous directory data: " << previousDataLocation;
46 previousDataLocation.removeRecursively();
50 DownloadManager::~DownloadManager()
58 qDebug() <<
"DownloadManager: shutting down," << activeJobs.size() <<
"active jobs";
73 QObject *DownloadManager::systeminfoProvider(QQmlEngine *engine,
74 QJSEngine *scriptEngine)
77 Q_UNUSED(scriptEngine)
84 qmlRegisterSingletonType<DownloadManager>(
"GCompris", 1, 0,
91 return (activeJobs.size() > 0);
96 if (activeJobs.size() > 0) {
97 QMutexLocker locker(&jobsMutex);
98 QMutableListIterator<DownloadJob*> iter(activeJobs);
99 while (iter.hasNext()) {
100 DownloadJob *job = iter.next();
102 disconnect(job->reply, SIGNAL(finished()),
this, SLOT(downloadFinished()));
103 disconnect(job->reply, SIGNAL(
error(QNetworkReply::NetworkError)),
104 this, SLOT(handleError(QNetworkReply::NetworkError)));
105 if (job->reply->isRunning()) {
106 qDebug() <<
"Aborting download job:" << job->url;
116 emit
error(QNetworkReply::OperationCanceledError,
"Download cancelled by user");
122 return QString(
"data2/voices-" COMPRESSED_AUDIO
"/voices-%1.rcc")
123 .arg(ApplicationInfo::getInstance()->getVoicesLocale(locale));
126 inline QString DownloadManager::getAbsoluteResourcePath(
const QString& path)
const
128 foreach (
const QString &base, getSystemResourcePaths()) {
129 if (QFile::exists(base +
'/' + path))
130 return QString(base +
'/' + path);
136 inline QString DownloadManager::getRelativeResourcePath(
const QString& path)
const
138 QStringList parts = path.split(
'/', QString::SkipEmptyParts);
139 if (parts.size() < 3)
141 return QString(parts[parts.size()-3] +
'/' + parts[parts.size()-2]
142 +
'/' + parts[parts.size()-1]);
147 return (!getAbsoluteResourcePath(path).isEmpty());
152 if (checkDownloadRestriction())
155 QString absPath = getAbsoluteResourcePath(path);
157 if (!absPath.isEmpty())
158 return registerResource(absPath);
160 qDebug() <<
"No such local resource and download prohibited:"
169 qDebug() <<
"Downloading resource file" << path;
170 DownloadJob* job =
new DownloadJob(QUrl(serverUrl.toString() +
'/' + path));
173 QMutexLocker locker(&jobsMutex);
174 activeJobs.append(job);
177 if (!download(job)) {
178 QMutexLocker locker(&jobsMutex);
179 activeJobs.removeOne(job);
187 void DownloadManager::registerLocalResources()
189 QStringList filenames = getLocalResources();
190 if (filenames == QStringList()) {
191 qDebug() <<
"No local resources found";
195 QList<QString>::const_iterator iter;
196 for (iter = filenames.constBegin(); iter != filenames.constEnd(); iter++)
197 registerResource(*iter);
200 bool DownloadManager::checkForUpdates()
202 QStringList filenames = getLocalResources();
203 if (filenames == QStringList()) {
204 qDebug() <<
"No local resources found";
208 if (!checkDownloadRestriction()) {
209 qDebug() <<
"Can't download with current network connection (" <<
210 networkConfiguration.bearerTypeName() <<
")!";
214 QList<QString>::const_iterator iter;
215 DownloadJob *job =
new DownloadJob();
216 for (iter = filenames.constBegin(); iter != filenames.constEnd(); iter++) {
217 QUrl url = getUrlForFilename(*iter);
218 qDebug() <<
"Scheduling resource for update: " << url;
219 job->queue.append(url);
221 job->url = job->queue.takeFirst();
224 QMutexLocker locker(&jobsMutex);
225 activeJobs.append(job);
228 if (!download(job)) {
229 QMutexLocker locker(&jobsMutex);
230 activeJobs.removeOne(job);
240 inline QString DownloadManager::tempFilenameForFilename(
const QString &filename)
const
242 return QString(filename).append(
"_");
245 inline QString DownloadManager::filenameForTempFilename(
const QString &tempFilename)
const
247 if (tempFilename.endsWith(QLatin1String(
"_")))
248 return tempFilename.left(tempFilename.length() - 1);
252 bool DownloadManager::download(DownloadJob* job)
254 QNetworkRequest request;
257 if (!job->contents.contains(job->url.fileName())) {
258 int len = job->url.fileName().length();
259 QUrl contentsUrl = QUrl(job->url.toString().remove(job->url.toString().length() - len, len)
261 if (!job->knownContentsUrls.contains(contentsUrl)) {
265 job->knownContentsUrls.append(contentsUrl);
267 job->queue.prepend(job->url);
268 job->url = contentsUrl;
272 QFileInfo fi(getFilenameForUrl(job->url));
275 if (!dir.exists(fi.path()) && !dir.mkpath(fi.path())) {
276 qDebug() <<
"Could not create resource path " << fi.path();
277 emit
error(QNetworkReply::ProtocolUnknownError,
"Could not create resource path");
281 job->file.setFileName(tempFilenameForFilename(fi.filePath()));
282 if (!job->file.open(QIODevice::WriteOnly)) {
283 emit
error(QNetworkReply::ProtocolUnknownError,
284 QString(
"Could not open target file %1").arg(job->file.fileName()));
289 request.setUrl(job->url);
291 QNetworkReply *reply = accessManager.get(request);
293 connect(reply, SIGNAL(finished()),
this, SLOT(downloadFinished()));
294 connect(reply, SIGNAL(readyRead()),
this, SLOT(downloadReadyRead()));
295 connect(reply, SIGNAL(
error(QNetworkReply::NetworkError)),
296 this, SLOT(handleError(QNetworkReply::NetworkError)));
297 if (job->url.fileName() != contentsFilename) {
300 emit
downloadStarted(job->url.toString().remove(0, serverUrl.toString().length()));
306 inline DownloadManager::DownloadJob* DownloadManager::getJobByReply(QNetworkReply *r)
308 QMutexLocker locker(&jobsMutex);
309 for (
int i = 0; i < activeJobs.size(); i++)
310 if (activeJobs[i]->reply == r)
311 return activeJobs[i];
315 void DownloadManager::downloadReadyRead()
317 QNetworkReply *reply =
dynamic_cast<QNetworkReply*
>(sender());
318 DownloadJob *job = getJobByReply(reply);
319 job->file.write(reply->readAll());
322 inline QString DownloadManager::getFilenameForUrl(
const QUrl& url)
const
324 QString relPart = url.toString().remove(0, serverUrl.toString().length());
325 return QString(getSystemDownloadPath() + relPart);
328 inline QUrl DownloadManager::getUrlForFilename(
const QString& filename)
const
330 return QUrl(serverUrl.toString() +
'/' + getRelativeResourcePath(filename));
333 inline QString DownloadManager::getSystemDownloadPath()
const
335 return QStandardPaths::writableLocation(QStandardPaths::DataLocation);
338 inline QStringList DownloadManager::getSystemResourcePaths()
const
341 QStringList results({
342 getSystemDownloadPath(),
343 #if defined(Q_OS_ANDROID)
346 QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) +
347 '/' + GCOMPRIS_APPLICATION_NAME
351 foreach(QString path, QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation)) {
352 results.append(path +
'/' + GCOMPRIS_APPLICATION_NAME);
358 bool DownloadManager::checkDownloadRestriction()
const
363 QNetworkConfiguration::BearerType conn = networkConfiguration.bearerType();
364 qDebug() <<
"Bearer type: "<< conn <<
": "<< networkConfiguration.bearerTypeName();
365 if (!ApplicationSettings::getInstance()->isMobileNetworkDownloadsEnabled() &&
366 conn != QNetworkConfiguration::BearerEthernet &&
367 conn != QNetworkConfiguration::QNetworkConfiguration::BearerWLAN)
371 return ApplicationSettings::getInstance()->isAutomaticDownloadsEnabled() &&
372 ApplicationInfo::getInstance()->isDownloadAllowed();
375 void DownloadManager::handleError(QNetworkReply::NetworkError code)
378 QNetworkReply *reply =
dynamic_cast<QNetworkReply*
>(sender());
379 emit
error(reply->error(), reply->errorString());
382 bool DownloadManager::parseContents(DownloadJob *job)
384 if (job->file.isOpen())
387 if (!job->file.open(QIODevice::ReadOnly | QIODevice::Text)) {
388 qWarning() <<
"Could not open file " << job->file.fileName();
397 QTextStream in(&job->file);
398 while (!in.atEnd()) {
399 QString line = in.readLine();
400 QStringList parts = line.split(
' ', QString::SkipEmptyParts);
401 if (parts.size() != 2) {
402 qWarning() <<
"Invalid format of Contents file!";
405 job->contents[parts[1]] = parts[0];
412 bool DownloadManager::checksumMatches(DownloadJob *job,
const QString& filename)
const
414 Q_ASSERT(job->contents != (QMap<QString, QString>()));
416 if (!QFile::exists(filename))
419 QString basename = QFileInfo(filename).fileName();
420 if (!job->contents.contains(basename))
423 QFile file(filename);
424 file.open(QIODevice::ReadOnly);
425 QCryptographicHash fileHasher(hashMethod);
426 if (!fileHasher.addData(&file)) {
427 qWarning() <<
"Could not read file for hashing: " << filename;
431 QByteArray fileHash = fileHasher.result().toHex();
434 return (fileHash == job->contents[basename]);
437 void DownloadManager::unregisterResource_locked(
const QString& filename)
439 if (!QResource::unregisterResource(filename))
440 qDebug() <<
"Error unregistering resource file" << filename;
442 qDebug() <<
"Successfully unregistered resource file" << filename;
443 registeredResources.removeOne(filename);
447 inline bool DownloadManager::isRegistered(
const QString& filename)
const
449 return (registeredResources.indexOf(filename) != -1);
452 bool DownloadManager::registerResource(
const QString& filename)
454 QMutexLocker locker(&rcMutex);
455 if (isRegistered(filename))
456 unregisterResource_locked(filename);
458 if (!QResource::registerResource(filename)) {
459 qDebug() <<
"Error registering resource file" << filename;
462 qDebug() <<
"Successfully registered resource"
464 registeredResources.append(filename);
469 ApplicationSettings::getInstance()->locale());
470 if (v == getRelativeResourcePath(filename))
478 QString res = QString(
":/gcompris/data/%1").arg(data);
480 return !QDir(res).entryList().empty();
485 QString resource = QString(
"voices-" COMPRESSED_AUDIO
"/%1").
486 arg(ApplicationInfo::getInstance()->getVoicesLocale(ApplicationSettings::getInstance()->locale()));
491 void DownloadManager::downloadFinished()
493 QNetworkReply* reply =
dynamic_cast<QNetworkReply*
>(sender());
495 DownloadJob *job = getJobByReply(reply);
496 if (job->file.isOpen()) {
500 if (reply->error() && job->file.exists())
504 QString tFilename = filenameForTempFilename(job->file.fileName());
505 if (QFile::exists(tFilename))
506 QFile::remove(tFilename);
507 if (!job->file.rename(tFilename))
508 qWarning() <<
"Could not rename temporary file to" << tFilename;
511 QString targetFilename = getFilenameForUrl(job->url);
512 if (job->url.fileName() == contentsFilename) {
514 if (reply->error()) {
515 qWarning() <<
"Error downloading Contents from" << job->url
516 <<
":" << reply->error() <<
":" << reply->errorString();
522 if (!parseContents(job)) {
523 qWarning() <<
"Invalid format of Contents file" << job->url;
524 emit
error(QNetworkReply::UnknownContentError,
"Invalid format of Contents file");
530 if (reply->error()) {
531 qWarning() <<
"Error downloading RCC file from " << job->url
532 <<
":" << reply->error() <<
":" << reply->errorString();
536 if (QFile::exists(targetFilename))
537 registerResource(targetFilename);
539 qDebug() <<
"Download of RCC file finished successfully: " << job->url;
540 if (!checksumMatches(job, targetFilename)) {
541 qWarning() <<
"Checksum of downloaded file does not match: "
543 emit
error(QNetworkReply::UnknownContentError,
544 QString(
"Checksum of downloaded file does not match: %1")
545 .arg(targetFilename));
548 registerResource(targetFilename);
553 while (!job->queue.isEmpty()) {
554 job->url = job->queue.takeFirst();
555 QString relPath = getRelativeResourcePath(getFilenameForUrl(job->url));
557 foreach (
const QString &base, getSystemResourcePaths()) {
558 QString filename = base +
'/' + relPath;
559 if (QFile::exists(filename)
560 && checksumMatches(job, filename))
563 qDebug() <<
"Local resource is up-to-date:"
564 << QFileInfo(filename).fileName();
565 registerResource(filename);
576 if (job->file.isOpen())
579 QMutexLocker locker(&jobsMutex);
580 activeJobs.removeOne(job);
582 emit downloadFinished(code);
588 if (job->url.fileName() == contentsFilename) {
592 while (!job->queue.isEmpty()) {
593 nUrl = job->queue.takeFirst();
594 QString relPath = getRelativeResourcePath(getFilenameForUrl(nUrl));
595 foreach (
const QString &base, getSystemResourcePaths()) {
596 QString filename = base +
'/' + relPath;
597 if (QFile::exists(filename))
598 registerResource(filename);
603 if (job->file.isOpen())
607 QMutexLocker locker(&jobsMutex);
608 activeJobs.removeOne(job);
621 QStringList DownloadManager::getLocalResources()
625 foreach (
const QString &path, getSystemResourcePaths()) {
627 if (!dir.exists(path) && !dir.mkpath(path)) {
628 qWarning() <<
"Could not create resource path " << path;
632 QDirIterator it(dir, QDirIterator::Subdirectories);
633 while (it.hasNext()) {
634 QString filename = it.next();
635 QFileInfo fi = it.fileInfo();
637 (filename.endsWith(QLatin1String(
".rcc"))))
638 result.append(filename);
Q_INVOKABLE void shutdown()
Shutdown DownloadManager instance.
Q_INVOKABLE void abortDownloads()
Abort all currently running downloads.
Download finished successfully.
void downloadStarted(const QString &resource)
Emitted when a download has started.
void resourceRegistered(const QString &resource)
Emitted when a resource has been registered.
Local files are up-to-date, no download was needed.
A singleton class responsible for downloading, updating and maintaining remote resources.
Q_INVOKABLE bool updateResource(const QString &path)
Updates a resource path from the upstream server unless prohibited by the settings and registers it i...
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
Emitted during a running download.
Q_INVOKABLE bool downloadResource(const QString &path)
Download a resource specified by the relative resource path and register it if possible.
static void init()
Registers DownloadManager singleton in the QML engine.
Q_INVOKABLE bool downloadIsRunning() const
Whether any download is currently running.
void error(int code, const QString &msg)
Emitted when a download error occurs.
Singleton that contains GCompris' persistent settings.
Q_INVOKABLE bool isDataRegistered(const QString &data) const
Whether the passed relative data is registered.
Q_INVOKABLE QString getVoicesResourceForLocale(const QString &locale) const
Generates a relative voices resources file-path for a given locale.
void voicesRegistered()
Emitted when voices resources for current locale have been registered.
DownloadFinishedCode
Possible return codes of a finished download.
Q_INVOKABLE bool areVoicesRegistered() const
Whether voices for the currently active locale are registered.
Q_INVOKABLE bool haveLocalResource(const QString &path) const
Checks whether the given relative resource path exists locally.