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:
parent
6d95a07af0
commit
98ecccba8d
@ -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()
|
||||
|
18
src/assets/downloader/CMakeLists.txt
Normal file
18
src/assets/downloader/CMakeLists.txt
Normal 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
|
||||
)
|
||||
|
853
src/assets/downloader/assetdownloader.cpp
Normal file
853
src/assets/downloader/assetdownloader.cpp
Normal 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);
|
||||
}
|
||||
}
|
150
src/assets/downloader/assetdownloader.h
Normal file
150
src/assets/downloader/assetdownloader.h
Normal 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
|
Loading…
x
Reference in New Issue
Block a user