Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
assetdownloader.cpp
Go to the documentation of this file.
1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include "tasking/concurrentcall.h"
7#include "tasking/networkquery.h"
8#include "tasking/tasktreerunner.h"
9
10#include <QtCore/private/qzipreader_p.h>
11
12#include <QtCore/QDir>
13#include <QtCore/QFile>
14#include <QtCore/QJsonArray>
15#include <QtCore/QJsonDocument>
16#include <QtCore/QJsonObject>
17#include <QtCore/QStandardPaths>
18#include <QtCore/QTemporaryDir>
19#include <QtCore/QTemporaryFile>
20
21using namespace Tasking;
22
23QT_BEGIN_NAMESPACE
24
25namespace Assets::Downloader {
26
32
34{
35public:
37 AssetDownloader *m_q = nullptr;
38
44
53
54 void setLocalDownloadDir(const QDir &dir)
55 {
56 if (m_localDownloadDir != dir) {
57 m_localDownloadDir = dir;
58 emit m_q->localDownloadDirChanged(QUrl::fromLocalFile(m_localDownloadDir.absolutePath()));
59 }
60 }
61 void setProgress(int progressValue, int progressMaximum, const QString &progressText)
62 {
63 m_lastProgressText = progressText;
64 emit m_q->progressChanged(progressValue, progressMaximum, progressText);
65 }
66 void updateProgress(int progressValue, int progressMaximum)
67 {
68 setProgress(progressValue, progressMaximum, m_lastProgressText);
69 }
70 void clearProgress(const QString &progressText)
71 {
72 setProgress(0, 0, progressText);
73 }
74
75 void setupDownload(NetworkQuery *query, const QString &progressText)
76 {
77 query->setNetworkAccessManager(m_manager.get());
78 clearProgress(progressText);
79 QObject::connect(query, &NetworkQuery::started, query, [this, query] {
80 QNetworkReply *reply = query->reply();
81 QObject::connect(reply, &QNetworkReply::downloadProgress,
82 query, [this](qint64 bytesReceived, qint64 totalBytes) {
83 updateProgress((totalBytes > 0) ? 100.0 * bytesReceived / totalBytes : 0, 100);
84 });
85 QObject::connect(reply, &QNetworkReply::errorOccurred, query, [this, reply] {
86 m_networkErrors << reply->errorString();
87 });
88#if QT_CONFIG(ssl)
89 QObject::connect(reply, &QNetworkReply::sslErrors,
90 query, [this](const QList<QSslError> &sslErrors) {
91 for (const QSslError &sslError : sslErrors)
92 m_sslErrors << sslError.errorString();
93 });
94#endif
95 });
96 }
97};
98
99static bool isWritableDir(const QDir &dir)
100{
101 if (dir.exists()) {
102 QTemporaryFile file(dir.filePath(QString::fromLatin1("tmp")));
103 return file.open();
104 }
105 return false;
106}
107
108static bool sameFileContent(const QFileInfo &first, const QFileInfo &second)
109{
110 if (first.exists() ^ second.exists())
111 return false;
112
113 if (first.size() != second.size())
114 return false;
115
116 QFile firstFile(first.absoluteFilePath());
117 QFile secondFile(second.absoluteFilePath());
118
119 if (firstFile.open(QFile::ReadOnly) && secondFile.open(QFile::ReadOnly)) {
120 char char1;
121 char char2;
122 int readBytes1 = 0;
123 int readBytes2 = 0;
124 while (!firstFile.atEnd()) {
125 readBytes1 = firstFile.read(&char1, 1);
126 readBytes2 = secondFile.read(&char2, 1);
127 if (readBytes1 != readBytes2 || readBytes1 != 1)
128 return false;
129 if (char1 != char2)
130 return false;
131 }
132 return true;
133 }
134
135 return false;
136}
137
138static bool createDirectory(const QDir &dir)
139{
140 if (dir.exists())
141 return true;
142
143 if (!createDirectory(dir.absoluteFilePath(QString::fromUtf8(".."))))
144 return false;
145
146 return dir.mkpath(QString::fromUtf8("."));
147}
148
149static bool canBeALocalBaseDir(const QDir &dir)
150{
151 if (dir.exists())
152 return !dir.isEmpty() || isWritableDir(dir);
153 return createDirectory(dir) && isWritableDir(dir);
154}
155
156static QDir baseLocalDir(const QDir &preferredLocalDir)
157{
158 if (canBeALocalBaseDir(preferredLocalDir))
159 return preferredLocalDir;
160
161 qWarning().noquote() << "AssetDownloader: Cannot set \"" << preferredLocalDir
162 << "\" as a local download directory!";
163 return QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
164}
165
166static QString pathFromUrl(const QUrl &url)
167{
168 if (url.isLocalFile())
169 return url.toLocalFile();
170
171 if (url.scheme() == u"qrc")
172 return u":" + url.path();
173
174 return url.toString();
175}
176
177static QList<QUrl> filterDownloadableAssets(const QList<QUrl> &assetFiles, const QDir &expectedDir)
178{
179 QList<QUrl> downloadList;
180 std::copy_if(assetFiles.begin(), assetFiles.end(), std::back_inserter(downloadList),
181 [&](const QUrl &assetPath) {
182 return !QFileInfo::exists(expectedDir.absoluteFilePath(assetPath.toString()));
183 });
184 return downloadList;
185}
186
187static bool allAssetsPresent(const QList<QUrl> &assetFiles, const QDir &expectedDir)
188{
189 return std::all_of(assetFiles.begin(), assetFiles.end(), [&](const QUrl &assetPath) {
190 return QFileInfo::exists(expectedDir.absoluteFilePath(assetPath.toString()));
191 });
192}
193
194AssetDownloader::AssetDownloader(QObject *parent)
195 : QObject(parent)
196 , d(new AssetDownloaderPrivate(this))
197{}
198
199AssetDownloader::~AssetDownloader() = default;
200
202{
203 return d->m_downloadBase;
204}
205
206void AssetDownloader::setDownloadBase(const QUrl &downloadBase)
207{
208 if (d->m_downloadBase != downloadBase) {
209 d->m_downloadBase = downloadBase;
210 emit downloadBaseChanged(d->m_downloadBase);
211 }
212}
213
215{
216 return QUrl::fromLocalFile(d->m_preferredLocalDownloadDir.absolutePath());
217}
218
220{
221 if (localDir.scheme() == u"qrc") {
222 qWarning() << "Cannot set a qrc as preferredLocalDownloadDir";
223 return;
224 }
225
226 const QString path = pathFromUrl(localDir);
227 if (d->m_preferredLocalDownloadDir != path) {
228 d->m_preferredLocalDownloadDir.setPath(path);
229 emit preferredLocalDownloadDirChanged(preferredLocalDownloadDir());
230 }
231}
232
234{
235 return d->m_offlineAssetsFilePath;
236}
237
238void AssetDownloader::setOfflineAssetsFilePath(const QUrl &offlineAssetsFilePath)
239{
240 if (d->m_offlineAssetsFilePath != offlineAssetsFilePath) {
241 d->m_offlineAssetsFilePath = offlineAssetsFilePath;
242 emit offlineAssetsFilePathChanged(d->m_offlineAssetsFilePath);
243 }
244}
245
247{
248 return d->m_jsonFileName;
249}
250
251void AssetDownloader::setJsonFileName(const QString &jsonFileName)
252{
253 if (d->m_jsonFileName != jsonFileName) {
254 d->m_jsonFileName = jsonFileName;
255 emit jsonFileNameChanged(d->m_jsonFileName);
256 }
257}
258
260{
261 return d->m_zipFileName;
262}
263
264void AssetDownloader::setZipFileName(const QString &zipFileName)
265{
266 if (d->m_zipFileName != zipFileName) {
267 d->m_zipFileName = zipFileName;
268 emit zipFileNameChanged(d->m_zipFileName);
269 }
270}
271
273{
274 return QUrl::fromLocalFile(d->m_localDownloadDir.absolutePath());
275}
276
277QStringList AssetDownloader::networkErrors() const
278{
279 return d->m_networkErrors;
280}
281
282QStringList AssetDownloader::sslErrors() const
283{
284 return d->m_sslErrors;
285}
286
287static void precheckLocalFile(const QUrl &url)
288{
289 if (url.isEmpty())
290 return;
291 QFile file(pathFromUrl(url));
292 if (!file.open(QIODevice::ReadOnly))
293 qWarning() << "Cannot open local file" << url;
294}
295
296static void readAssetsFileContent(QPromise<DownloadableAssets> &promise, const QByteArray &content)
297{
298 const QJsonObject json = QJsonDocument::fromJson(content).object();
299 const QJsonArray assetsArray = json[u"assets"].toArray();
300 DownloadableAssets result;
301 result.remoteUrl = json[u"url"].toString();
302 for (const QJsonValue &asset : assetsArray) {
303 if (promise.isCanceled())
304 return;
305 result.files.append(asset.toString());
306 }
307
308 if (result.files.isEmpty() || result.remoteUrl.isEmpty())
309 promise.future().cancel();
310 else
311 promise.addResult(result);
312}
313
314static void unzip(QPromise<void> &promise, const QByteArray &content, const QDir &directory,
315 const QString &fileName)
316{
317 const QString zipFilePath = directory.absoluteFilePath(fileName);
318 QFile zipFile(zipFilePath);
319 if (!zipFile.open(QIODevice::WriteOnly)) {
320 promise.future().cancel();
321 return;
322 }
323 zipFile.write(content);
324 zipFile.close();
325
326 if (promise.isCanceled())
327 return;
328
329 QZipReader reader(zipFilePath);
330 const bool extracted = reader.extractAll(directory.absolutePath());
331 reader.close();
332 if (extracted)
333 QFile::remove(zipFilePath);
334 else
335 promise.future().cancel();
336}
337
338static void writeAsset(QPromise<void> &promise, const QByteArray &content, const QString &filePath)
339{
340 const QFileInfo fileInfo(filePath);
341 QFile file(fileInfo.absoluteFilePath());
342 if (!createDirectory(fileInfo.dir()) || !file.open(QFile::WriteOnly)) {
343 promise.future().cancel();
344 return;
345 }
346
347 if (promise.isCanceled())
348 return;
349
350 file.write(content);
351 file.close();
352}
353
354static void copyAndCheck(QPromise<void> &promise, const QString &sourcePath, const QString &destPath)
355{
356 QFile sourceFile(sourcePath);
357 QFile destFile(destPath);
358 const QFileInfo sourceFileInfo(sourceFile.fileName());
359 const QFileInfo destFileInfo(destFile.fileName());
360
361 if (destFile.exists() && !destFile.remove()) {
362 qWarning().noquote() << QString::fromLatin1("Unable to remove file \"%1\".")
363 .arg(QFileInfo(destFile.fileName()).absoluteFilePath());
364 promise.future().cancel();
365 return;
366 }
367
368 if (!createDirectory(destFileInfo.absolutePath())) {
369 qWarning().noquote() << QString::fromLatin1("Cannot create directory \"%1\".")
370 .arg(destFileInfo.absolutePath());
371 promise.future().cancel();
372 return;
373 }
374
375 if (promise.isCanceled())
376 return;
377
378 if (!sourceFile.copy(destFile.fileName()) && !sameFileContent(sourceFileInfo, destFileInfo))
379 promise.future().cancel();
380}
381
382void AssetDownloader::start()
383{
384 if (d->m_taskTreeRunner.isRunning())
385 return;
386
387 struct StorageData
388 {
389 QDir tempDir;
390 QByteArray jsonContent;
391 DownloadableAssets assets;
392 QList<QUrl> assetsToDownload;
393 QByteArray zipContent;
394 int doneCount = 0;
395 };
396
397 const Storage<StorageData> storage;
398
399 const auto onSetup = [this, storage] {
400 if (!d->m_manager)
401 d->m_manager = std::make_unique<QNetworkAccessManager>();
402 if (!d->m_temporaryDir)
403 d->m_temporaryDir = std::make_unique<QTemporaryDir>();
404 if (!d->m_temporaryDir->isValid()) {
405 qWarning() << "Cannot create a temporary directory.";
406 return SetupResult::StopWithError;
407 }
408 storage->tempDir = d->m_temporaryDir->path();
409 d->setLocalDownloadDir(baseLocalDir(d->m_preferredLocalDownloadDir));
410 d->m_networkErrors.clear();
411 d->m_sslErrors.clear();
412 precheckLocalFile(resolvedUrl(d->m_offlineAssetsFilePath));
413 return SetupResult::Continue;
414 };
415
416 const auto onJsonDownloadSetup = [this](NetworkQuery &query) {
417 query.setRequest(QNetworkRequest(d->m_downloadBase.resolved(d->m_jsonFileName)));
418 d->setupDownload(&query, tr("Downloading JSON file..."));
419 };
420 const auto onJsonDownloadDone = [this, storage](const NetworkQuery &query, DoneWith result) {
421 if (result == DoneWith::Success) {
422 storage->jsonContent = query.reply()->readAll();
423 return DoneResult::Success;
424 }
425 qWarning() << "Cannot download" << d->m_downloadBase.resolved(d->m_jsonFileName)
426 << query.reply()->errorString();
427 if (d->m_offlineAssetsFilePath.isEmpty()) {
428 qWarning() << "Also there is no local file as a replacement";
429 return DoneResult::Error;
430 }
431
432 QFile file(pathFromUrl(resolvedUrl(d->m_offlineAssetsFilePath)));
433 if (!file.open(QIODevice::ReadOnly)) {
434 qWarning() << "Also failed to open" << d->m_offlineAssetsFilePath;
435 return DoneResult::Error;
436 }
437
438 storage->jsonContent = file.readAll();
439 return DoneResult::Success;
440 };
441
442 const auto onReadAssetsFileSetup = [storage](ConcurrentCall<DownloadableAssets> &async) {
443 async.setConcurrentCallData(readAssetsFileContent, storage->jsonContent);
444 };
445 const auto onReadAssetsFileDone = [storage](const ConcurrentCall<DownloadableAssets> &async) {
446 storage->assets = async.result();
447 storage->assetsToDownload = storage->assets.files;
448 };
449
450 const auto onSkipIfAllAssetsPresent = [this, storage] {
451 return allAssetsPresent(storage->assets.files, d->m_localDownloadDir)
452 ? SetupResult::StopWithSuccess : SetupResult::Continue;
453 };
454
455 const auto onZipDownloadSetup = [this, storage](NetworkQuery &query) {
456 if (d->m_zipFileName.isEmpty())
457 return SetupResult::StopWithSuccess;
458
459 query.setRequest(QNetworkRequest(d->m_downloadBase.resolved(d->m_zipFileName)));
460 d->setupDownload(&query, tr("Downloading zip file..."));
461 return SetupResult::Continue;
462 };
463 const auto onZipDownloadDone = [storage](const NetworkQuery &query, DoneWith result) {
464 if (result == DoneWith::Success)
465 storage->zipContent = query.reply()->readAll();
466 return DoneResult::Success; // Ignore zip download failure
467 };
468
469 const auto onUnzipSetup = [this, storage](ConcurrentCall<void> &async) {
470 if (storage->zipContent.isEmpty())
471 return SetupResult::StopWithSuccess;
472
473 async.setConcurrentCallData(unzip, storage->zipContent, storage->tempDir, d->m_zipFileName);
474 d->clearProgress(tr("Unzipping..."));
475 return SetupResult::Continue;
476 };
477 const auto onUnzipDone = [storage](DoneWith result) {
478 if (result == DoneWith::Success) {
479 // Avoid downloading assets that are present in unzipped tree
480 StorageData &storageData = *storage;
481 storageData.assetsToDownload =
482 filterDownloadableAssets(storageData.assets.files, storageData.tempDir);
483 } else {
484 qWarning() << "ZipFile failed";
485 }
486 return DoneResult::Success; // Ignore unzip failure
487 };
488
489 const LoopUntil downloadIterator([storage](int iteration) {
490 return iteration < storage->assetsToDownload.count();
491 });
492
493 const Storage<QByteArray> assetStorage;
494
495 const auto onAssetsDownloadGroupSetup = [this, storage] {
496 d->setProgress(0, storage->assetsToDownload.size(), tr("Downloading assets..."));
497 };
498
499 const auto onAssetDownloadSetup = [this, storage, downloadIterator](NetworkQuery &query) {
500 query.setNetworkAccessManager(d->m_manager.get());
501 query.setRequest(QNetworkRequest(storage->assets.remoteUrl.resolved(
502 storage->assetsToDownload.at(downloadIterator.iteration()))));
503 };
504 const auto onAssetDownloadDone = [assetStorage](const NetworkQuery &query, DoneWith result) {
505 if (result == DoneWith::Success)
506 *assetStorage = query.reply()->readAll();
507 };
508
509 const auto onAssetWriteSetup = [storage, downloadIterator, assetStorage](
510 ConcurrentCall<void> &async) {
511 const QString filePath = storage->tempDir.absoluteFilePath(
512 storage->assetsToDownload.at(downloadIterator.iteration()).toString());
513 async.setConcurrentCallData(writeAsset, *assetStorage, filePath);
514 };
515 const auto onAssetWriteDone = [this, storage](DoneWith result) {
516 if (result != DoneWith::Success) {
517 qWarning() << "Asset write failed";
518 return;
519 }
520 StorageData &storageData = *storage;
521 ++storageData.doneCount;
522 d->updateProgress(storageData.doneCount, storageData.assetsToDownload.size());
523 };
524
525 const LoopUntil copyIterator([storage](int iteration) {
526 return iteration < storage->assets.files.count();
527 });
528
529 const auto onAssetsCopyGroupSetup = [this, storage] {
530 storage->doneCount = 0;
531 d->setProgress(0, storage->assets.files.size(), tr("Copying assets..."));
532 };
533
534 const auto onAssetCopySetup = [this, storage, copyIterator](ConcurrentCall<void> &async) {
535 const QString fileName = storage->assets.files.at(copyIterator.iteration()).toString();
536 const QString sourcePath = storage->tempDir.absoluteFilePath(fileName);
537 const QString destPath = d->m_localDownloadDir.absoluteFilePath(fileName);
538 async.setConcurrentCallData(copyAndCheck, sourcePath, destPath);
539 };
540 const auto onAssetCopyDone = [this, storage] {
541 StorageData &storageData = *storage;
542 ++storageData.doneCount;
543 d->updateProgress(storageData.doneCount, storageData.assets.files.size());
544 };
545
546 const auto onAssetsCopyGroupDone = [this, storage](DoneWith result) {
547 if (result != DoneWith::Success) {
548 d->setLocalDownloadDir(storage->tempDir);
549 qWarning() << "Asset copy failed";
550 return;
551 }
552 d->m_temporaryDir.reset();
553 };
554
555 const Group recipe {
556 storage,
557 onGroupSetup(onSetup),
558 NetworkQueryTask(onJsonDownloadSetup, onJsonDownloadDone),
559 ConcurrentCallTask<DownloadableAssets>(onReadAssetsFileSetup, onReadAssetsFileDone, CallDoneIf::Success),
560 Group {
561 onGroupSetup(onSkipIfAllAssetsPresent),
562 NetworkQueryTask(onZipDownloadSetup, onZipDownloadDone),
563 ConcurrentCallTask<void>(onUnzipSetup, onUnzipDone),
564 For (downloadIterator) >> Do {
565 parallelIdealThreadCountLimit,
566 onGroupSetup(onAssetsDownloadGroupSetup),
567 Group {
568 assetStorage,
569 NetworkQueryTask(onAssetDownloadSetup, onAssetDownloadDone),
570 ConcurrentCallTask<void>(onAssetWriteSetup, onAssetWriteDone)
571 }
572 },
573 For (copyIterator) >> Do {
574 parallelIdealThreadCountLimit,
575 onGroupSetup(onAssetsCopyGroupSetup),
576 ConcurrentCallTask<void>(onAssetCopySetup, onAssetCopyDone, CallDoneIf::Success),
577 onGroupDone(onAssetsCopyGroupDone)
578 }
579 }
580 };
581 d->m_taskTreeRunner.start(recipe, [this](TaskTree *) { emit started(); },
582 [this](DoneWith result) { emit finished(result == DoneWith::Success); });
583}
584
585QUrl AssetDownloader::resolvedUrl(const QUrl &url) const
586{
587 return url;
588}
589
590} // namespace Assets::Downloader
591
592QT_END_NAMESPACE
void setupDownload(NetworkQuery *query, const QString &progressText)
void setProgress(int progressValue, int progressMaximum, const QString &progressText)
void clearProgress(const QString &progressText)
void updateProgress(int progressValue, int progressMaximum)
std::unique_ptr< QNetworkAccessManager > m_manager
std::unique_ptr< QTemporaryDir > m_temporaryDir
void setOfflineAssetsFilePath(const QUrl &offlineAssetsFilePath)
virtual QUrl resolvedUrl(const QUrl &url) const
void setJsonFileName(const QString &jsonFileName)
void setPreferredLocalDownloadDir(const QUrl &localDir)
void setDownloadBase(const QUrl &downloadBase)
void setZipFileName(const QString &zipFileName)
static void unzip(QPromise< void > &promise, const QByteArray &content, const QDir &directory, const QString &fileName)
static bool sameFileContent(const QFileInfo &first, const QFileInfo &second)
static void writeAsset(QPromise< void > &promise, const QByteArray &content, const QString &filePath)
static void precheckLocalFile(const QUrl &url)
static QDir baseLocalDir(const QDir &preferredLocalDir)
static QString pathFromUrl(const QUrl &url)
static bool allAssetsPresent(const QList< QUrl > &assetFiles, const QDir &expectedDir)
static void copyAndCheck(QPromise< void > &promise, const QString &sourcePath, const QString &destPath)
static bool canBeALocalBaseDir(const QDir &dir)
static QList< QUrl > filterDownloadableAssets(const QList< QUrl > &assetFiles, const QDir &expectedDir)
static void readAssetsFileContent(QPromise< DownloadableAssets > &promise, const QByteArray &content)
static bool createDirectory(const QDir &dir)
static bool isWritableDir(const QDir &dir)