#include "downloadmanager.h" #include "common/coreservices.h" #include "common/exceptions.h" #include "common/shaderpack.h" DownloadManager::DownloadManager(QObject *parent) : QObject{parent} { m_compiler = new PackCompiler(this); QObject::connect ( m_compiler, &PackCompiler::progressChanged, this, &DownloadManager::compileProgressChanged ); QObject::connect ( m_compiler, &PackCompiler::currentStepChanged, this, &DownloadManager::compileStepsCompletedChanged ); QObject::connect ( m_compiler, &PackCompiler::totalStepsChanged, this, &DownloadManager::compileStepsChanged ); QObject::connect ( &m_moveProcess, &QProcess::readyReadStandardOutput, this, [this]() { QByteArray processData = m_moveProcess.readAllStandardOutput(); if(!processData.isValidUtf8()) { qWarning() << QStringLiteral("Process output not valid UTF8 data"); return; } setCompilerOutput(m_compilerOutput + processData); } ); QObject::connect ( &m_moveProcess, &QProcess::readyReadStandardError, this, [this]() { QByteArray processData = m_moveProcess.readAllStandardError(); if(!processData.isValidUtf8()) { qWarning() << QStringLiteral("Process output not valid UTF8 data"); return; } setCompilerOutput(m_compilerOutput + processData); } ); } /** * [1] Download image from endpoint * [2] Apply generic single-media template, with an image source * [3] Optionally ask user if they would like to apply manipulative shader * [4] Install locally */ auto DownloadManager::downloadImage(const QString &author, const QString &authorId, const QString &description, const QUrl &url) -> void { QNetworkRequest request(url); QString requestUrl = request.url().toString(); QString id = QUuid::createUuidV7().toString(QUuid::WithoutBraces); setState(Downloading); //QFuture downloadUri = download(request, id, Get); if(m_downloadFuture.isRunning()) { m_downloadFuture.cancelChain(); } m_downloadFuture = download(request, id, Get); m_downloadFuture .then ( [this, author, description, id](QUrl result) -> QUrl { setState(Installing); ShaderPack metadata; metadata.setAuthor(author); metadata.setDescription(description); metadata.setName(QStringLiteral("Pexels Image (%1)").arg(id)); metadata.setSource(QStringLiteral("./images/%1").arg(result.fileName())); QUrl packUri ( QStringLiteral("file://%1/%2").arg ( QStandardPaths::writableLocation(QStandardPaths::TempLocation), id ) ); QUrl packImageUri ( QStringLiteral("file://%1/%2/images/%3").arg ( QStandardPaths::writableLocation(QStandardPaths::TempLocation), id, result.fileName() ) ); QDir packDirectory(packUri.toLocalFile()); if(!packDirectory.mkpath(packUri.toLocalFile())) { throw FileException(QStringLiteral("Could not create directory")); } packDirectory.mkdir(QStringLiteral("images")); QFile::rename(result.toLocalFile(), packImageUri.toLocalFile()); QFile packFile ( packDirectory.absoluteFilePath ( QStringLiteral("pack.json") ) ); if(!packFile.open(QFile::ReadWrite)) { setError(QStringLiteral("File Error"), packFile.errorString()); throw FileException(packFile.errorString()); } QByteArray data = metadata.json().toJson(QJsonDocument::Indented); if(packFile.write(data) != data.length()) { setError(QStringLiteral("File Error"), packFile.errorString()); throw FileException(packFile.errorString()); } packFile.close(); return packUri; } ) .then ( [this](QUrl result) -> QUrl { return install(result); } ) .then ( [this](QUrl result) { setLastInstalledFile(result.toLocalFile()); setState(Complete); } ) .onCanceled ( [this]() { reset(); } ) .onFailed ( [this] (const NetworkException &e) { reset(); setError(QStringLiteral("Network Exception %1").arg(QString::number(e.errorCode)), e.message); } ) .onFailed ( [this] (const FileException &e) { reset(); setError(QStringLiteral("File Exception %1").arg(QString::number(e.errorCode)), e.message); } ) .onFailed ( [this] () { reset(); setError(QStringLiteral("Unknown Exception"), QString()); } ); } auto DownloadManager::downloadVideo(const QString &author, const QString &authorId, const QUrl &url) -> void { QNetworkRequest request(url); QString id = QUuid::createUuidV7().toString(QUuid::WithoutBraces); setState(Downloading); if(m_downloadFuture.isRunning()) { m_downloadFuture.cancelChain(); } m_downloadFuture = download(request, id, Get); m_downloadFuture .then ( [this, author, id](QUrl result) -> QUrl { ShaderPack metadata; metadata.setAuthor(author); metadata.setType(ShaderPack::Video); metadata.setName(QStringLiteral("Pexels Video (%1)").arg(id)); metadata.setSource(QStringLiteral("./videos/%1").arg(result.fileName())); QUrl packUri ( QStringLiteral("%1/%2").arg ( QStandardPaths::writableLocation(QStandardPaths::TempLocation), id ) ); QUrl packVideoUri ( QStringLiteral("%1/%2/videos/%3").arg ( QStandardPaths::writableLocation(QStandardPaths::TempLocation), id, result.fileName() ) ); QDir packDirectory(packUri.toLocalFile()); packDirectory.mkpath(packUri.toLocalFile()); packDirectory.mkdir(QStringLiteral("images")); QFile::rename(result.toLocalFile(), packVideoUri.toLocalFile()); QFile packFile ( packDirectory.absoluteFilePath ( QStringLiteral("pack.json") ) ); if(!packFile.open(QFile::ReadWrite)) { setError(QStringLiteral("File Error"), packFile.errorString()); throw FileException(packFile.errorString()); } QByteArray data = metadata.json().toJson(QJsonDocument::Compact); if(packFile.write(data) != data.length()) { setError(QStringLiteral("File Error"), packFile.errorString()); throw FileException(packFile.errorString()); } packFile.close(); return packUri; } ) .then ( [this](QUrl result) -> QUrl { return install(result); } ) .then ( [this](QUrl result) { setLastInstalledFile(result.toLocalFile()); setState(Complete); } ) .onCanceled ( [this]() { reset(); } ) .onFailed ( [this] (const NetworkException &e) { reset(); setError(QStringLiteral("Network Exception %1").arg(QString::number(e.errorCode)), e.message); } ) .onFailed ( [this] (const FileException &e) { reset(); setError(QStringLiteral("File Exception %1").arg(QString::number(e.errorCode)), e.message); } ) .onFailed ( [this] () { reset(); setError(QStringLiteral("Unknown Exception"), QString()); } ); } auto DownloadManager::downloadPack(const QString &id) -> void { QUrl downloadUrl = QUrl ( QStringLiteral("%1/%2/%3").arg ( KOMPLEX_API_HOST, KOMPLEX_API_VERSION, KOMPLEX_ENDPOINT_PACKS_ITEM ) ); QNetworkRequest request(downloadUrl); request.setRawHeader(QByteArray("uuid"), id.toUtf8()); QString tempFile(QString("%1.tar.gz").arg(id)); QFuture downloadUri = download(request, id, Post, tempFile); downloadUri .then ( [this](QUrl result) -> QUrl { return m_compiler->process(result).result(); } ) .then ( [this](QUrl result) -> QUrl { return install(result); } ) .then ( [this](QUrl result) { setLastInstalledFile(result.toLocalFile()); setState(Complete); } ) .onCanceled ( [this, tempFile]() { QFile::remove(QStringLiteral("/tmp/") + tempFile); reset(); } ) .onFailed ( [this, tempFile] (const std::filesystem::filesystem_error &e) { QFile::remove(QStringLiteral("/tmp/") + tempFile); reset(); setError(QStringLiteral("Network Exception %1").arg(QString::number(e.code().value())), e.what()); } ) .onFailed ( [this, tempFile] (const std::logic_error &e) { QFile::remove(QStringLiteral("/tmp/") + tempFile); reset(); setError(QStringLiteral("Logic Exception %1"), e.what()); } ) .onFailed ( [this, tempFile] (const std::exception &e) { QFile::remove(QStringLiteral("/tmp/") + tempFile); reset(); setError(QStringLiteral("Logic Exception %1"), e.what()); } ) .onFailed ( [this, tempFile] () { QFile::remove(QStringLiteral("/tmp/") + tempFile); reset(); setError(QStringLiteral("Unknown Exception"), QString()); } ); } auto DownloadManager::reset() -> void { setError(QString(), QString()); setState(Idle); } auto DownloadManager::compileProgress() -> qreal { if(m_compiler == nullptr) { return 0; } return m_compiler->progress(); } auto DownloadManager::compileSteps() -> qint64 { if(m_compiler == nullptr) { return 0; } return m_compiler->totalSteps(); } auto DownloadManager::compileStepsCompleted() -> qint64 { if(nullptr == m_compiler) { return 0; } return m_compiler->currentStep(); } auto DownloadManager::setError(const QString &title, const QString &message) -> void { if(title == m_errorTitle && message == m_errorMessage) { return; } m_errorMessage = std::move(message); m_errorTitle = std::move(title); Q_EMIT errorChanged(); setState(Error); } void DownloadManager::setCompilerOutput(const QString &compilerOutput) { if (m_compilerOutput == compilerOutput) { return; } m_compilerOutput = compilerOutput; emit compilerOutputChanged(); } auto DownloadManager::setState(State state) -> void { if(state == m_state) { return; } m_state = state; Q_EMIT stateChanged(); return; } auto DownloadManager::setDownloadProgress(qreal progress) -> void { if(qFuzzyCompare(progress, m_downloadProgress)) { return; } m_downloadProgress = progress; Q_EMIT downloadProgressChanged(); } auto DownloadManager::install(const QUrl &uri) noexcept(false) -> QUrl { validateUri(uri); QDir tempDirectory(uri.toLocalFile()); QUrl installLocation = QStringLiteral("file://%1/.local/share/komplex/packs/%2").arg ( QStandardPaths::writableLocation(QStandardPaths::HomeLocation), tempDirectory.dirName() ); QDir installDirectory(installLocation.toLocalFile()); if(installDirectory.exists()) installDirectory.removeRecursively(); QStringList arguments = QStringList { uri.toLocalFile(), installLocation.toLocalFile() }; m_moveProcess.start(QStringLiteral("mv"), arguments); if(!m_moveProcess.waitForStarted(3000)) { qWarning() << QStringLiteral("Could not start copy process: %1").arg(m_moveProcess.readAllStandardError()); throw FileException(QStringLiteral("Could not start install process")); } if(!m_moveProcess.waitForFinished()) { qWarning() << QStringLiteral("Copy process took longer than expected (>30s)"); throw FileException(QStringLiteral("Install process took longer than expected (>30s)")); } return installLocation; } auto DownloadManager::download(const QNetworkRequest &request, const QString &id, RequestType type, const QString &filename) -> QFuture { return QtConcurrent::run ( [this, request, id, type, _filename = filename]() -> QUrl { QEventLoop loop; QString filename = _filename; if(filename.isEmpty() && !request.url().fileName().isEmpty()) { filename = request.url().fileName(); } else if(filename.isEmpty()) { filename = id; } QUrl downloadUri = QStringLiteral("file://%1/%2").arg ( QStandardPaths::writableLocation(QStandardPaths::TempLocation), filename ); auto manager = CoreServices::networkAccessManager().toStrongRef(); if(manager == nullptr) { throw NetworkException { QStringLiteral("Network Manager reference has already been deleted") }; } QFile downloadFile(downloadUri.toLocalFile()); if(downloadFile.exists() && !downloadFile.remove()) { throw FileException(downloadFile.errorString()); } QNetworkReply *reply = nullptr; switch(type) { case Post: reply = manager->post(request, nullptr); break; default: case Get: reply = manager->get(request, nullptr); break; } QObject::connect ( reply, &QNetworkReply::errorOccurred, &loop, [&loop](QNetworkReply::NetworkError error) -> void { loop.quit(); throw NetworkException ( QStringLiteral("Network Error %1").arg ( QString::number(static_cast(error)) ) ); } ); QObject::connect ( reply, &QNetworkReply::downloadProgress, this, [this](qint64 bytesDownloaded, qint64 bytesTotal) { setDownloadSize(bytesTotal); setDownloadedBytes(bytesDownloaded); setDownloadProgress(static_cast(bytesDownloaded) / bytesTotal); } ); QObject::connect ( reply, &QNetworkReply::readyRead, this, [this, downloadUri]() { QMutexLocker locker(&m_fileMutex); QFile downloadFile(downloadUri.toLocalFile()); if(!downloadFile.open(QFile::ReadWrite | QFile::Append)) { throw FileException(QStringLiteral("Could not open temp file location")); } QNetworkReply *reply = qobject_cast(sender()); if(reply == nullptr) { throw std::system_error { std::error_code ( ENOENT, std::system_category() ), QStringLiteral("Reply is not a qobject").toStdString() }; } quint64 bytes = reply->bytesAvailable(); QByteArray data = reply->readAll(); quint64 bytesWritten = downloadFile.write(data/*bytes)*/); downloadFile.close(); } ); QObject::connect ( reply, &QNetworkReply::finished, &loop, &QEventLoop::quit ); if(!reply->isFinished()) { loop.exec(); } manager.clear(); return downloadUri; } ); } auto DownloadManager::validateUri(const QUrl &uri) noexcept(false) -> void { if(!uri.isValid() || !uri.isLocalFile()) { throw std::filesystem::filesystem_error ( QStringLiteral("URI %1 is not a local file uri").arg ( uri.toString() ).toStdString(), std::error_code ( ENOENT, std::system_category() ) ); } } auto DownloadManager::readShaderToyEntry(const QUrl &uri) noexcept(false) -> ShaderToyEntry { validateUri(uri); QDir packDir(uri.toLocalFile()); if(!packDir.exists(QStringLiteral("pack.json"))) { throw std::filesystem::filesystem_error ( QStringLiteral("Pack file not found").arg ( uri.toString() ).toStdString(), std::error_code ( ENOENT, std::system_category() ) ); } QFile packFile(packDir.absoluteFilePath(QStringLiteral("pack.json"))); if(!packFile.open(QFile::ReadOnly)) { throw std::filesystem::filesystem_error ( QStringLiteral("Could not open pack file").arg ( uri.toString() ).toStdString(), std::error_code ( errno, std::system_category() ) ); } QByteArray data = packFile.readAll(); QJsonParseError jsonError; QJsonDocument document = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error != QJsonParseError::NoError) { throw std::filesystem::filesystem_error ( QStringLiteral("URI %1 is not a local file uri").arg ( uri.toString() ).toStdString(), std::error_code ( EIO, std::system_category() ) ); } QJsonObject documentObject = document.object(); QJsonObject rootObject = documentObject[QStringLiteral("Shader")].toObject(); QJsonObject infoObject = rootObject[QStringLiteral("info")].toObject(); QJsonArray tagsArray = infoObject[QStringLiteral("tags")].toArray(); QStringList tags; for(const QJsonValue &tag : std::as_const(tagsArray)) tags += tag.toString(); ShaderToyEntry entry; entry.metadata = ShaderToyMetadata { QDateTime::fromSecsSinceEpoch(infoObject[QStringLiteral("date")].toInt()), infoObject[QStringLiteral("description")].toString(), static_cast(infoObject[QStringLiteral("flags")].toInt()), static_cast(infoObject[QStringLiteral("hasLiked")].toInt()), infoObject[QStringLiteral("id")].toString(), static_cast(infoObject[QStringLiteral("likes")].toInt()), infoObject[QStringLiteral("name")].toString(), static_cast(infoObject[QStringLiteral("published")].toInt()), tags, static_cast(infoObject[QStringLiteral("usePreview")].toInt()), infoObject[QStringLiteral("username")].toString(), rootObject[QStringLiteral("ver")].toString(), static_cast(infoObject[QStringLiteral("views")].toInt()) }; QJsonArray ShaderToyRenderPassArray = rootObject[QStringLiteral("renderpass")].toArray(); for(const QJsonValue &ShaderToyRenderPassValue : std::as_const(ShaderToyRenderPassArray)) { QJsonArray inputArray = ShaderToyRenderPassValue[QStringLiteral("inputs")].toArray(); QJsonArray outputArray = ShaderToyRenderPassValue[QStringLiteral("outputs")].toArray(); QList inputs; QList outputs; for(const QJsonValue &inputValue : std::as_const(inputArray)) { QJsonObject samplerObject = inputValue[QStringLiteral("sampler")].toObject(); inputs.append ( ShaderToyRenderInput { static_cast(inputValue[QStringLiteral("channel")].toInt()), inputValue[QStringLiteral("ctype")].toString(), samplerObject[QStringLiteral("filter")].toString(), static_cast(inputValue[QStringLiteral("id")].toInt()), samplerObject[QStringLiteral("internal")].toString(), static_cast(inputValue[QStringLiteral("published")].toInt()), inputValue[QStringLiteral("src")].toString(), samplerObject[QStringLiteral("srgb")].toBool(), samplerObject[QStringLiteral("verticalFlip")].toBool(), samplerObject[QStringLiteral("wrap")].toString() } ); } for(const QJsonValue &outputValue : std::as_const(outputArray)) { outputs.append ( ShaderToyRenderOutput { static_cast(outputValue[QStringLiteral("channel")].toInt()), static_cast(outputValue[QStringLiteral("id")].toInt()) } ); } entry.renderPasses.append ( ShaderToyRenderPass { ShaderToyRenderPassValue[QStringLiteral("code")].toVariant().toByteArray(), ShaderToyRenderPassValue[QStringLiteral("description")].toString(), inputs, ShaderToyRenderPassValue[QStringLiteral("name")].toString(), outputs, ShaderToyRenderPassValue[QStringLiteral("type")].toString() } ); } return std::move(entry); } auto DownloadManager::lastInstalledFile() const -> const QString & { return m_lastInstalledFile; } auto DownloadManager::setLastInstalledFile(const QString &lastInstalledFile) -> void { if (m_lastInstalledFile == lastInstalledFile) return; m_lastInstalledFile = lastInstalledFile; emit lastInstalledFileChanged(); } auto DownloadManager::setDownloadedBytes(qint64 downloadedBytes) -> void { if (m_downloadedBytes == downloadedBytes) return; m_downloadedBytes = downloadedBytes; emit downloadedBytesChanged(); } auto DownloadManager::setDownloadSize(qint64 downloadSize) -> void { if (m_downloadSize == downloadSize) return; m_downloadSize = downloadSize; emit downloadSizeChanged(); }