Add AssetDownloader to QtBase as a static library

Task-number: QTBUG-122550
Change-Id: I97419f27079475784ae05e1150493abc87e1b119
Reviewed-by: Kai Köhne <kai.koehne@qt.io>
(cherry picked from commit cbb2493cf76eaad23315ad1a4676d44d396fafc3)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Ali Kianian 2024-03-08 17:14:15 +02:00 committed by Qt Cherry-pick Bot
parent 6d95a07af0
commit 98ecccba8d
4 changed files with 1025 additions and 0 deletions

View File

@ -1,3 +1,7 @@
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
add_subdirectory(icons)
if (NOT INTEGRITY AND TARGET Qt6::Network AND TARGET Qt6::Concurrent)
add_subdirectory(downloader)
endif()

View File

@ -0,0 +1,18 @@
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
qt_internal_add_module(ExamplesAssetDownloaderPrivate
CONFIG_MODULE_NAME examples_asset_downloader
STATIC
EXCEPTIONS
INTERNAL_MODULE
SOURCES
assetdownloader.cpp assetdownloader.h
LIBRARIES
Qt6::Concurrent
Qt6::CorePrivate
Qt6::Network
PUBLIC_LIBRARIES
Qt6::Core
)

View File

@ -0,0 +1,853 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "assetdownloader.h"
#include <QtConcurrent/QtConcurrent>
#include <QtCore/QtAssert>
#include <QtCore/private/qzipreader_p.h>
#include <QCoreApplication>
#include <QFile>
#include <QFuture>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLatin1StringView>
#include <QNetworkReply>
#include <QNetworkRequest>
using QtExamples::AssetDownloader;
using QtExamples::AssetDownloaderPrivate;
namespace {
enum class UnzipRule { DeleteArchive, KeepArchive };
struct DownloadableAssets
{
QUrl remoteUrl;
QList<QUrl> files;
};
class Exception : public QException
{
public:
Exception(const QString &errorString)
: m_errorString(errorString)
{}
QString message() const { return m_errorString; }
private:
QString m_errorString;
};
class NetworkException : public Exception
{
public:
NetworkException(QNetworkReply *reply)
: Exception(reply->errorString())
{}
void raise() const override { throw *this; }
NetworkException *clone() const override { return new NetworkException(*this); }
};
class FileException : public Exception
{
public:
FileException(QFile *file)
: Exception(QString::fromUtf8("%1 \"%2\"").arg(file->errorString(), file->fileName()))
{}
void raise() const override { throw *this; }
FileException *clone() const override { return new FileException(*this); }
};
class FileRemoveException : public Exception
{
public:
FileRemoveException(QFile *file)
: Exception(QString::fromLatin1("Unable to remove file \"%1\"")
.arg(QFileInfo(file->fileName()).absoluteFilePath()))
{}
void raise() const override { throw *this; }
FileRemoveException *clone() const override { return new FileRemoveException(*this); }
};
class FileCopyException : public Exception
{
public:
FileCopyException(const QString &src, const QString &dst)
: Exception(QString::fromLatin1("Unable to copy file \"%1\" to \"%2\"")
.arg(QFileInfo(src).absoluteFilePath(), QFileInfo(dst).absoluteFilePath()))
{}
void raise() const override { throw *this; }
FileCopyException *clone() const override { return new FileCopyException(*this); }
};
class DirException : public Exception
{
public:
DirException(const QDir &dir)
: Exception(QString::fromLatin1("Cannot create directory %1").arg(dir.absolutePath()))
{}
void raise() const override { throw *this; }
DirException *clone() const override { return new DirException(*this); }
};
class AssetFileException : public Exception
{
public:
AssetFileException()
: Exception(QString::fromUtf8("Asset file is broken."))
{}
void raise() const override { throw *this; }
AssetFileException *clone() const override { return new AssetFileException(*this); }
};
bool isWritableDir(const QDir &dir)
{
if (dir.exists()) {
QTemporaryFile file(dir.filePath(QString::fromLatin1("tmp")));
return file.open();
}
return false;
}
bool sameFileContent(const QFileInfo &first, const QFileInfo &second)
{
if (first.exists() ^ second.exists())
return false;
if (first.size() != second.size())
return false;
QFile firstFile(first.absoluteFilePath());
QFile secondFile(second.absoluteFilePath());
if (firstFile.open(QFile::ReadOnly) && secondFile.open(QFile::ReadOnly)) {
char char1;
char char2;
int readBytes1 = 0;
int readBytes2 = 0;
while (!firstFile.atEnd()) {
readBytes1 = firstFile.read(&char1, 1);
readBytes2 = secondFile.read(&char2, 1);
if (readBytes1 != readBytes2 || readBytes1 != 1)
return false;
if (char1 != char2)
return false;
}
return true;
}
return false;
}
bool createDirectory(const QDir &dir)
{
if (dir.exists())
return true;
if (!createDirectory(dir.absoluteFilePath(QString::fromUtf8(".."))))
return false;
return dir.mkpath(QString::fromUtf8("."));
}
bool canBeALocalBaseDir(QDir &dir)
{
if (dir.exists()) {
if (dir.isEmpty())
return isWritableDir(dir);
return true;
} else {
return createDirectory(dir) && isWritableDir(dir);
}
return false;
}
bool unzip(const QString &fileName, const QDir &directory, UnzipRule rule)
{
QZipReader zipFile(fileName);
const bool zipExtracted = zipFile.extractAll(directory.absolutePath());
zipFile.close();
if (zipExtracted && (rule == UnzipRule::DeleteArchive))
QFile::remove(fileName);
return zipExtracted;
}
void ensureDirOrThrow(const QDir &dir)
{
if (createDirectory(dir))
return;
throw DirException(dir);
}
void ensureRemoveFileOrThrow(QFile &file)
{
if (!file.exists() || file.remove())
return;
throw FileRemoveException(&file);
}
bool shouldDownloadEverything(const QList<QUrl> &assetFiles, const QDir &expectedDir)
{
return std::any_of(assetFiles.begin(), assetFiles.end(), [&](const QUrl &imgPath) -> bool {
const QString localPath = expectedDir.absoluteFilePath(imgPath.toString());
return !QFileInfo::exists(localPath);
});
}
QString pathFromUrl(const QUrl &url)
{
return url.isLocalFile() ? url.toLocalFile() : url.toString();
}
DownloadableAssets readAssetsFile(const QByteArray &assetFileContent)
{
DownloadableAssets result;
QJsonObject json = QJsonDocument::fromJson(assetFileContent).object();
const QJsonArray assetsArray = json[u"assets"].toArray();
for (const QJsonValue &asset : assetsArray)
result.files.append(asset.toString());
result.remoteUrl = json[u"url"].toString();
if (result.files.isEmpty() || result.remoteUrl.isEmpty())
throw AssetFileException{};
return result;
}
QList<QUrl> filterDownloadableAssets(const QList<QUrl> &assetFiles, const QDir &expectedDir)
{
QList<QUrl> downloadList;
std::copy_if(assetFiles.begin(),
assetFiles.end(),
std::back_inserter(downloadList),
[&](const QUrl &imgPath) {
const QString tempFilePath = expectedDir.absoluteFilePath(imgPath.toString());
return !QFileInfo::exists(tempFilePath);
});
return downloadList;
}
} // namespace
class QtExamples::AssetDownloaderPrivate
{
AssetDownloader *q_ptr = nullptr;
Q_DECLARE_PUBLIC(AssetDownloader)
public:
using State = AssetDownloader::State;
AssetDownloaderPrivate(AssetDownloader *parent);
~AssetDownloaderPrivate();
void setupBase();
void setBaseLocalDir(const QDir &dir);
void moveAllAssets();
void setAllDownloadsCount(int count);
void setCompletedDownloadsCount(int count);
void finalizeDownload();
void setState(State state);
QFuture<QByteArray> download(const QUrl &url);
QFuture<QUrl> downloadToFile(const QUrl &url, const QUrl &destPath);
QFuture<QByteArray> downloadOrRead(const QUrl &url, const QUrl &localFile);
QFuture<DownloadableAssets> downloadAssets(const DownloadableAssets &assets);
QFuture<DownloadableAssets> maybeReadZipFile(const DownloadableAssets &assets);
State state = State::NotDownloadedState;
int allDownloadsCount = 0;
std::unique_ptr<QTemporaryDir> providedTempDir;
QDir baseLocal;
QDir tempDir;
int completedDownloadsCount = 0;
double progress = 0;
bool downloadStarted = false;
DownloadableAssets downloadableAssets;
QNetworkAccessManager *networkManager = nullptr;
QFutureWatcher<QByteArray> *downloadWatcher = nullptr;
QFutureWatcher<QUrl> *fileDownloadWatcher = nullptr;
QString jsonFileName;
QString zipFileName;
QDir preferredLocalDownloadDir;
QUrl offlineAssetsFilePath;
QUrl downloadBase = {QString::fromLatin1("https://download.qt.io/learning/examples/")};
};
AssetDownloaderPrivate::AssetDownloaderPrivate(AssetDownloader *parent)
: q_ptr(parent)
, networkManager(new QNetworkAccessManager(parent))
, downloadWatcher(new QFutureWatcher<QByteArray>(parent))
, fileDownloadWatcher(new QFutureWatcher<QUrl>(parent))
, preferredLocalDownloadDir(
QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation))
{
QObject::connect(downloadWatcher,
&QFutureWatcherBase::progressValueChanged,
parent,
&AssetDownloader::setProgressPercent);
QObject::connect(fileDownloadWatcher,
&QFutureWatcherBase::progressValueChanged,
parent,
&AssetDownloader::setProgressPercent);
}
AssetDownloaderPrivate::~AssetDownloaderPrivate()
{
downloadWatcher->cancel();
fileDownloadWatcher->cancel();
downloadWatcher->waitForFinished();
fileDownloadWatcher->waitForFinished();
}
void AssetDownloaderPrivate::setupBase()
{
providedTempDir.reset(new QTemporaryDir{});
Q_ASSERT_X(providedTempDir->isValid(),
"AssetDownloader::setupBase()",
"Cannot create a temporary directory!");
if (canBeALocalBaseDir(preferredLocalDownloadDir)) {
setBaseLocalDir(preferredLocalDownloadDir);
} else {
qWarning().noquote() << "AssetDownloader: Cannot set \"" << preferredLocalDownloadDir
<< "\" as a local download directory!";
setBaseLocalDir(QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation));
}
tempDir = providedTempDir->path();
}
void AssetDownloaderPrivate::setBaseLocalDir(const QDir &dir)
{
Q_Q(AssetDownloader);
if (baseLocal != dir) {
baseLocal = dir;
emit q->localDownloadDirChanged(QUrl::fromLocalFile(baseLocal.absolutePath()));
}
}
void AssetDownloaderPrivate::moveAllAssets()
{
if (tempDir != baseLocal && !tempDir.isEmpty()) {
try {
setState(State::MovingFilesState);
QtConcurrent::blockingMap(downloadableAssets.files, [this](const QUrl &asset) {
QFile srcFile(tempDir.absoluteFilePath(asset.toString()));
QFile dstFile(baseLocal.absoluteFilePath(asset.toString()));
QFileInfo srcFileInfo(srcFile.fileName());
QFileInfo dstFileInfo(dstFile.fileName());
ensureRemoveFileOrThrow(dstFile);
ensureDirOrThrow(dstFileInfo.absolutePath());
if (!srcFile.copy(dstFile.fileName())) {
if (!sameFileContent(srcFileInfo, dstFileInfo))
throw FileCopyException(srcFile.fileName(), dstFile.fileName());
}
});
} catch (const Exception &exception) {
qWarning() << exception.message();
setBaseLocalDir(tempDir);
} catch (const std::exception &exception) {
qWarning() << exception.what();
setBaseLocalDir(tempDir);
}
}
}
void AssetDownloaderPrivate::setAllDownloadsCount(int count)
{
Q_Q(AssetDownloader);
if (allDownloadsCount != count) {
allDownloadsCount = count;
emit q->allDownloadsCountChanged(allDownloadsCount);
}
}
void AssetDownloaderPrivate::setCompletedDownloadsCount(int count)
{
Q_Q(AssetDownloader);
if (completedDownloadsCount != count) {
completedDownloadsCount = count;
emit q->completedDownloadsCountChanged(completedDownloadsCount);
}
}
void AssetDownloaderPrivate::finalizeDownload()
{
Q_Q(AssetDownloader);
if (downloadStarted) {
downloadStarted = false;
emit q->downloadFinished();
}
}
void AssetDownloaderPrivate::setState(State state)
{
Q_Q(AssetDownloader);
if (this->state != state) {
this->state = state;
emit q->stateChanged(this->state);
}
}
QFuture<QByteArray> AssetDownloaderPrivate::download(const QUrl &url)
{
Q_Q(AssetDownloader);
Q_ASSERT_X(QThread::currentThread() != q->thread(),
"AssetDownloader::download()",
"Method called from wrong thread");
QSharedPointer<QPromise<QByteArray>> promise(new QPromise<QByteArray>);
QFuture<QByteArray> result = promise->future();
promise->start();
QNetworkReply *reply = networkManager->get(QNetworkRequest(url));
QObject::connect(reply, &QNetworkReply::finished, reply, [reply, promise]() {
if (reply->error() == QNetworkReply::NoError)
promise->addResult(reply->readAll());
else
promise->setException(NetworkException(reply));
promise->finish();
reply->deleteLater();
});
QObject::connect(reply,
&QNetworkReply::downloadProgress,
reply,
[promise, reply](qint64 bytesReceived, qint64 totalBytes) {
if (promise->isCanceled()) {
reply->abort();
return;
}
int progress = (totalBytes > 0) ? 100.0 * bytesReceived / totalBytes : 0;
promise->setProgressRange(0, 100);
promise->setProgressValue(progress);
});
downloadWatcher->setFuture(result);
emit q->downloadingFileChanged(url);
return result;
}
QFuture<QUrl> AssetDownloaderPrivate::downloadToFile(const QUrl &url, const QUrl &destPath)
{
Q_Q(AssetDownloader);
Q_ASSERT_X(QThread::currentThread() != q->thread(),
"AssetDownloader::download()",
"Method called from wrong thread");
QSharedPointer<QPromise<QUrl>> promise(new QPromise<QUrl>);
QFuture<QUrl> result = promise->future();
promise->start();
QFileInfo fileInfo(pathFromUrl(destPath));
createDirectory(fileInfo.absolutePath());
QSharedPointer<QFile> file(new QFile(fileInfo.absoluteFilePath()));
if (!file->open(QFile::WriteOnly)) {
promise->setException(FileException(file.data()));
promise->finish();
return result;
}
QNetworkReply *reply = networkManager->get(QNetworkRequest(url), file.data());
QObject::connect(reply, &QNetworkReply::finished, reply, [reply, promise, destPath, file]() {
if (reply->error() == QNetworkReply::NoError)
promise->addResult(destPath);
else
promise->setException(NetworkException(reply));
const qint64 bytesAvailable = reply->bytesAvailable();
if (bytesAvailable)
file->write(reply->read(bytesAvailable));
file->close();
promise->finish();
reply->deleteLater();
});
QObject::connect(reply, &QNetworkReply::readyRead, reply, [promise, reply, file]() {
if (promise->isCanceled()) {
reply->abort();
return;
}
const qint64 available = reply->bytesAvailable();
file->write(reply->read(available));
});
QObject::connect(reply,
&QNetworkReply::downloadProgress,
reply,
[promise, reply](qint64 bytesReceived, qint64 totalBytes) {
if (promise->isCanceled()) {
reply->abort();
return;
}
int progress = (totalBytes > 0) ? 100.0 * bytesReceived / totalBytes : 0;
promise->setProgressRange(0, 100);
promise->setProgressValue(progress);
});
fileDownloadWatcher->setFuture(result);
emit q->downloadingFileChanged(url);
return result;
}
QFuture<QByteArray> AssetDownloaderPrivate::downloadOrRead(const QUrl &url, const QUrl &localFile)
{
Q_Q(AssetDownloader);
Q_ASSERT_X(QThread::currentThread() != q->thread(),
"AssetDownloader::downloadOrRead()",
"Method called from wrong thread");
QSharedPointer<QPromise<QByteArray>> promise(new QPromise<QByteArray>);
QFuture<QByteArray> futureResult = promise->future();
promise->start();
// Precheck local file
if (!localFile.isEmpty()) {
QFile file(pathFromUrl(localFile));
if (!file.open(QIODevice::ReadOnly))
qWarning() << "Cannot open local file" << localFile;
}
download(url)
.then([promise](const QByteArray &content) {
if (promise->isCanceled())
return;
promise->addResult(content);
promise->finish();
})
.onFailed([promise, url, localFile](const QException &downloadException) {
QFile file(pathFromUrl(localFile));
qWarning() << "Cannot download" << url;
if (promise->isCanceled())
return;
if (localFile.isEmpty()) {
qWarning() << "Also there is no local file as a replacement";
promise->setException(downloadException);
} else if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "Also failed to open" << localFile;
promise->setException(FileException(&file));
} else {
promise->addResult(file.readAll());
}
promise->finish();
});
return futureResult;
}
/*!
* \internal
* Schedules \a assets for downloading.
* Returns a future representing the files which are not downloaded.
* \note call this method on the AssetDownloader thread
*/
QFuture<DownloadableAssets> AssetDownloaderPrivate::downloadAssets(const DownloadableAssets &assets)
{
Q_Q(AssetDownloader);
Q_ASSERT_X(QThread::currentThread() != q->thread(),
"AssetDownloader::downloadAssets()",
"Method called from wrong thread");
QSharedPointer<QPromise<DownloadableAssets>> promise(new QPromise<DownloadableAssets>);
QFuture<DownloadableAssets> future = promise->future();
promise->start();
if (assets.files.isEmpty()) {
promise->addResult(assets);
promise->finish();
return future;
}
setCompletedDownloadsCount(0);
setAllDownloadsCount(assets.files.size());
setState(State::DownloadingFilesState);
QFuture<void> schedule = QtConcurrent::run([] {});
for (const QUrl &assetFile : assets.files) {
const QUrl url = assets.remoteUrl.resolved(assetFile);
const QUrl dstFilePath = tempDir.absoluteFilePath(assetFile.toString());
schedule = schedule
.then(q,
[this, url, dstFilePath] { return downloadToFile(url, dstFilePath); })
.unwrap()
.then(q,
[this, promise]([[maybe_unused]] const QUrl &url) {
if (promise->isCanceled())
return;
setCompletedDownloadsCount(completedDownloadsCount + 1);
})
.onFailed([](const Exception &exception) {
qWarning() << "Download failed" << exception.message();
});
;
}
schedule = schedule
.then([assets, promise, tempDir = tempDir]() {
if (promise->isCanceled())
return;
DownloadableAssets result = assets;
result.files = filterDownloadableAssets(result.files, tempDir);
promise->addResult(result);
promise->finish();
})
.onFailed([promise](const QException &exception) {
promise->setException(exception);
promise->finish();
});
return future;
}
QFuture<DownloadableAssets> AssetDownloaderPrivate::maybeReadZipFile(const DownloadableAssets &assets)
{
Q_Q(AssetDownloader);
Q_ASSERT_X(QThread::currentThread() != q->thread(),
"AssetDownloader::maybeReadZipFile()",
"Method called from wrong thread");
QSharedPointer<QPromise<DownloadableAssets>> promise(new QPromise<DownloadableAssets>);
QFuture<DownloadableAssets> future = promise->future();
promise->start();
if (assets.files.isEmpty() || zipFileName.isEmpty()) {
promise->addResult(assets);
promise->finish();
return future;
}
setState(State::DownloadingZipState);
const QUrl zipUrl = downloadBase.resolved(zipFileName);
const QUrl dstUrl = tempDir.filePath(zipFileName);
downloadToFile(zipUrl, dstUrl)
.then(q,
[this, promise](const QUrl &downloadedFile) -> QUrl {
if (promise->isCanceled())
return {};
setState(State::ExtractingZipState);
return downloadedFile;
})
.then(QtFuture::Launch::Async,
[promise, zipFileName = zipFileName, tempDir = tempDir](const QUrl &downloadedFile) {
if (promise->isCanceled())
return;
unzip(pathFromUrl(downloadedFile), tempDir, UnzipRule::KeepArchive);
})
.then([promise, assets, tempDir = tempDir]() {
if (promise->isCanceled())
return;
DownloadableAssets result = assets;
result.files = filterDownloadableAssets(result.files, tempDir);
promise->addResult(result);
promise->finish();
})
.onFailed([promise, assets](const Exception &exception) {
qWarning() << "ZipFile failed" << exception.message();
promise->addResult(assets);
promise->finish();
});
return future;
}
AssetDownloader::AssetDownloader(QObject *parent)
: QObject(parent)
, d_ptr(new AssetDownloaderPrivate(this))
{}
AssetDownloader::~AssetDownloader() = default;
QUrl AssetDownloader::downloadBase() const
{
Q_D(const AssetDownloader);
return d->downloadBase;
}
void AssetDownloader::setDownloadBase(const QUrl &downloadBase)
{
Q_D(AssetDownloader);
if (d->downloadBase != downloadBase) {
d->downloadBase = downloadBase;
emit downloadBaseChanged(d->downloadBase);
}
}
int AssetDownloader::allDownloadsCount() const
{
Q_D(const AssetDownloader);
return d->allDownloadsCount;
}
int AssetDownloader::completedDownloadsCount() const
{
Q_D(const AssetDownloader);
return d->completedDownloadsCount;
}
QUrl AssetDownloader::localDownloadDir() const
{
Q_D(const AssetDownloader);
return QUrl::fromLocalFile(d->baseLocal.absolutePath());
}
double AssetDownloader::progress() const
{
Q_D(const AssetDownloader);
return d->progress;
}
QUrl AssetDownloader::preferredLocalDownloadDir() const
{
Q_D(const AssetDownloader);
return QUrl::fromLocalFile(d->preferredLocalDownloadDir.absolutePath());
}
void AssetDownloader::setPreferredLocalDownloadDir(const QUrl &localDir)
{
Q_D(AssetDownloader);
if (!localDir.isLocalFile())
qWarning() << "preferredLocalDownloadDir Should be a local directory";
const QString path = pathFromUrl(localDir);
if (d->preferredLocalDownloadDir != path) {
d->preferredLocalDownloadDir.setPath(path);
emit preferredLocalDownloadDirChanged(preferredLocalDownloadDir());
}
}
QUrl AssetDownloader::offlineAssetsFilePath() const
{
Q_D(const AssetDownloader);
return d->offlineAssetsFilePath;
}
void AssetDownloader::setOfflineAssetsFilePath(const QUrl &offlineAssetsFilePath)
{
Q_D(AssetDownloader);
if (d->offlineAssetsFilePath != offlineAssetsFilePath) {
d->offlineAssetsFilePath = offlineAssetsFilePath;
emit offlineAssetsFilePathChanged(d->offlineAssetsFilePath);
}
}
QString AssetDownloader::jsonFileName() const
{
Q_D(const AssetDownloader);
return d->jsonFileName;
}
void AssetDownloader::setJsonFileName(const QString &jsonFileName)
{
Q_D(AssetDownloader);
if (d->jsonFileName != jsonFileName) {
d->jsonFileName = jsonFileName;
emit jsonFileNameChanged(d->jsonFileName);
}
}
QString AssetDownloader::zipFileName() const
{
Q_D(const AssetDownloader);
return d->zipFileName;
}
void AssetDownloader::setZipFileName(const QString &zipFileName)
{
Q_D(AssetDownloader);
if (d->zipFileName != zipFileName) {
d->zipFileName = zipFileName;
emit zipFileNameChanged(d->zipFileName);
}
}
AssetDownloader::State AssetDownloader::state() const
{
Q_D(const AssetDownloader);
return d->state;
}
void AssetDownloader::start()
{
Q_D(AssetDownloader);
if (d->downloadStarted)
return;
d->setState(State::NotDownloadedState);
d->setupBase();
d->downloadOrRead(d->downloadBase.resolved(d->jsonFileName), d->offlineAssetsFilePath)
.then(QtFuture::Launch::Async, &readAssetsFile)
.then(this,
[this, d](const DownloadableAssets &assets) {
d->downloadStarted = true;
d->downloadableAssets = assets;
emit downloadStarted();
return assets;
})
.then(QtFuture::Launch::Async,
[localDir = d->baseLocal](DownloadableAssets assets) -> DownloadableAssets {
if (shouldDownloadEverything(assets.files, localDir))
return assets;
return {};
})
.then(this, [d](const DownloadableAssets &assets) { return d->maybeReadZipFile(assets); })
.unwrap()
.then(this, [d](const DownloadableAssets &assets) { return d->downloadAssets(assets); })
.unwrap()
.then(this, [d](const DownloadableAssets &) { return d->moveAllAssets(); })
.then(this,
[d]() {
d->setState(State::DownloadedState);
d->finalizeDownload();
})
.onFailed(this, [d](const Exception &exception) {
qWarning() << "Exception: " << exception.message();
d->setState(State::ErrorState);
d->finalizeDownload();
});
;
}
void AssetDownloader::setProgressPercent(int progress)
{
Q_D(AssetDownloader);
const double progressInRange = 0.01 * progress;
if (d->progress != progressInRange) {
d->progress = progressInRange;
emit progressChanged(d->progress);
}
}

View File

@ -0,0 +1,150 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef ASSETDOWNLOADER_H
#define ASSETDOWNLOADER_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include <QtCore/QObject>
#include <QtCore/QScopedPointer>
#include <QtCore/QUrl>
QT_BEGIN_NAMESPACE
namespace QtExamples {
class AssetDownloaderPrivate;
class AssetDownloader : public QObject
{
Q_OBJECT
Q_PROPERTY(
QUrl downloadBase
READ downloadBase
WRITE setDownloadBase
NOTIFY downloadBaseChanged)
Q_PROPERTY(
int completedDownloadsCount
READ completedDownloadsCount
NOTIFY completedDownloadsCountChanged)
Q_PROPERTY(
int allDownloadsCount
READ allDownloadsCount
NOTIFY allDownloadsCountChanged)
Q_PROPERTY(
double progress
READ progress
NOTIFY progressChanged)
Q_PROPERTY(
QUrl preferredLocalDownloadDir
READ preferredLocalDownloadDir
WRITE setPreferredLocalDownloadDir
NOTIFY preferredLocalDownloadDirChanged)
Q_PROPERTY(
QUrl localDownloadDir
READ localDownloadDir
NOTIFY localDownloadDirChanged)
Q_PROPERTY(
QUrl offlineAssetsFilePath
READ offlineAssetsFilePath
WRITE setOfflineAssetsFilePath
NOTIFY offlineAssetsFilePathChanged)
Q_PROPERTY(
QString jsonFileName
READ jsonFileName
WRITE setJsonFileName
NOTIFY jsonFileNameChanged)
Q_PROPERTY(
QString zipFileName
READ zipFileName
WRITE setZipFileName
NOTIFY zipFileNameChanged)
Q_PROPERTY(State state READ state NOTIFY stateChanged)
public:
enum class State {
NotDownloadedState,
DownloadingZipState,
ExtractingZipState,
DownloadingFilesState,
MovingFilesState,
DownloadedState,
ErrorState
};
Q_ENUM(State)
AssetDownloader(QObject *parent = nullptr);
~AssetDownloader();
QUrl downloadBase() const;
void setDownloadBase(const QUrl &downloadBase);
int allDownloadsCount() const;
int completedDownloadsCount() const;
QUrl localDownloadDir() const;
double progress() const;
QUrl preferredLocalDownloadDir() const;
void setPreferredLocalDownloadDir(const QUrl &localDir);
QUrl offlineAssetsFilePath() const;
void setOfflineAssetsFilePath(const QUrl &offlineAssetsFilePath);
QString jsonFileName() const;
void setJsonFileName(const QString &jsonFileName);
QString zipFileName() const;
void setZipFileName(const QString &zipFileName);
State state() const;
public Q_SLOTS:
void start();
private Q_SLOTS:
void setProgressPercent(int);
Q_SIGNALS:
void stateChanged(State);
void downloadBaseChanged(const QUrl &);
void allDownloadsCountChanged(int count);
void downloadStarted();
void completedDownloadsCountChanged(int count);
void downloadFinished();
void downloadingFileChanged(const QUrl &url);
void preferredLocalDownloadDirChanged(const QUrl &url);
void localDownloadDirChanged(const QUrl &url);
void progressChanged(double progress);
void offlineAssetsFilePathChanged(const QUrl &);
void jsonFileNameChanged(const QString &);
void zipFileNameChanged(const QString &);
private:
Q_DECLARE_PRIVATE(AssetDownloader)
QScopedPointer<AssetDownloaderPrivate> d_ptr;
};
} // namespace QtExamples
QT_END_NAMESPACE
#endif // ASSETDOWNLOADER_H