From 98ecccba8d8dbcb8bd65a1ca9eb0e2447e37b0ce Mon Sep 17 00:00:00 2001 From: Ali Kianian Date: Fri, 8 Mar 2024 17:14:15 +0200 Subject: [PATCH] Add AssetDownloader to QtBase as a static library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task-number: QTBUG-122550 Change-Id: I97419f27079475784ae05e1150493abc87e1b119 Reviewed-by: Kai Köhne (cherry picked from commit cbb2493cf76eaad23315ad1a4676d44d396fafc3) Reviewed-by: Qt Cherry-pick Bot --- src/assets/CMakeLists.txt | 4 + src/assets/downloader/CMakeLists.txt | 18 + src/assets/downloader/assetdownloader.cpp | 853 ++++++++++++++++++++++ src/assets/downloader/assetdownloader.h | 150 ++++ 4 files changed, 1025 insertions(+) create mode 100644 src/assets/downloader/CMakeLists.txt create mode 100644 src/assets/downloader/assetdownloader.cpp create mode 100644 src/assets/downloader/assetdownloader.h diff --git a/src/assets/CMakeLists.txt b/src/assets/CMakeLists.txt index 2ab060b0222..da9c0c14eea 100644 --- a/src/assets/CMakeLists.txt +++ b/src/assets/CMakeLists.txt @@ -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() diff --git a/src/assets/downloader/CMakeLists.txt b/src/assets/downloader/CMakeLists.txt new file mode 100644 index 00000000000..1d93b021d8d --- /dev/null +++ b/src/assets/downloader/CMakeLists.txt @@ -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 +) + diff --git a/src/assets/downloader/assetdownloader.cpp b/src/assets/downloader/assetdownloader.cpp new file mode 100644 index 00000000000..85699cfe733 --- /dev/null +++ b/src/assets/downloader/assetdownloader.cpp @@ -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 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using QtExamples::AssetDownloader; +using QtExamples::AssetDownloaderPrivate; + +namespace { + +enum class UnzipRule { DeleteArchive, KeepArchive }; + +struct DownloadableAssets +{ + QUrl remoteUrl; + QList 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 &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 filterDownloadableAssets(const QList &assetFiles, const QDir &expectedDir) +{ + QList 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 download(const QUrl &url); + QFuture downloadToFile(const QUrl &url, const QUrl &destPath); + QFuture downloadOrRead(const QUrl &url, const QUrl &localFile); + QFuture downloadAssets(const DownloadableAssets &assets); + QFuture maybeReadZipFile(const DownloadableAssets &assets); + + State state = State::NotDownloadedState; + int allDownloadsCount = 0; + std::unique_ptr providedTempDir; + QDir baseLocal; + QDir tempDir; + int completedDownloadsCount = 0; + double progress = 0; + bool downloadStarted = false; + DownloadableAssets downloadableAssets; + QNetworkAccessManager *networkManager = nullptr; + QFutureWatcher *downloadWatcher = nullptr; + QFutureWatcher *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(parent)) + , fileDownloadWatcher(new QFutureWatcher(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 AssetDownloaderPrivate::download(const QUrl &url) +{ + Q_Q(AssetDownloader); + Q_ASSERT_X(QThread::currentThread() != q->thread(), + "AssetDownloader::download()", + "Method called from wrong thread"); + + QSharedPointer> promise(new QPromise); + QFuture 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 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> promise(new QPromise); + QFuture result = promise->future(); + promise->start(); + + QFileInfo fileInfo(pathFromUrl(destPath)); + createDirectory(fileInfo.absolutePath()); + + QSharedPointer 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 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> promise(new QPromise); + QFuture 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 AssetDownloaderPrivate::downloadAssets(const DownloadableAssets &assets) +{ + Q_Q(AssetDownloader); + + Q_ASSERT_X(QThread::currentThread() != q->thread(), + "AssetDownloader::downloadAssets()", + "Method called from wrong thread"); + + QSharedPointer> promise(new QPromise); + QFuture 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 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 AssetDownloaderPrivate::maybeReadZipFile(const DownloadableAssets &assets) +{ + Q_Q(AssetDownloader); + Q_ASSERT_X(QThread::currentThread() != q->thread(), + "AssetDownloader::maybeReadZipFile()", + "Method called from wrong thread"); + + QSharedPointer> promise(new QPromise); + QFuture 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); + } +} diff --git a/src/assets/downloader/assetdownloader.h b/src/assets/downloader/assetdownloader.h new file mode 100644 index 00000000000..70b710ff198 --- /dev/null +++ b/src/assets/downloader/assetdownloader.h @@ -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 +#include +#include + +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 d_ptr; +}; + +} // namespace QtExamples + +QT_END_NAMESPACE + +#endif // ASSETDOWNLOADER_H