/* * SPDX-FileCopyrightText: 2020 Alexey Minnekhanov * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include #include #include #include #include #include #include #include #include #include // KF5 #include #include #include #include "AppstreamDataDownloader.h" #include "alpineapk_backend_logging.h" namespace DiscoverVersion { // contains static QLatin1String version("5.20.5"); definition // autogenerated from top CMakeLists.txt #include "../../../DiscoverVersion.h" } QString AppstreamDataDownloader::appStreamCacheDir() { const QString cachePath = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/external_appstream_data"); // ^^ "~/.cache/discover/external_appstream_data" QDir(cachePath).mkpath(QStringLiteral(".")); return cachePath; } AppstreamDataDownloader::AppstreamDataDownloader(QObject *parent) : QObject(parent) { m_jobTracker = new KUiServerJobTracker(this); } void AppstreamDataDownloader::setCacheExpirePeriodSecs(qint64 secs) { m_cacheExpireSeconds = secs; } void AppstreamDataDownloader::loadUrlsJson(const QString &jsonPath) { const QString jsonBaseName = QFileInfo(jsonPath).baseName(); QFile jsonFile(jsonPath); if (!jsonFile.open(QIODevice::ReadOnly)) { qCWarning(LOG_ALPINEAPK) << "Failed to open JSON:" << jsonPath << "for reading!"; Q_EMIT downloadFinished(); return; } const QByteArray jsonBa = jsonFile.readAll(); jsonFile.close(); QJsonParseError jsonError; const QJsonDocument jDoc = QJsonDocument::fromJson(jsonBa, &jsonError); if (jDoc.isNull()) { qCWarning(LOG_ALPINEAPK) << "Failed to parse JSON:" << jsonPath << "!"; qCWarning(LOG_ALPINEAPK) << jsonError.errorString(); Q_EMIT downloadFinished(); return; } // JSON structure: // { // "urls": [ // "https://...", "https://...", "https://..." // ] // } const QJsonObject rootObj = jDoc.object(); const QJsonArray urls = rootObj.value(QLatin1String("urls")).toArray(); for (const QJsonValue &urlValue : urls) { const QString url = urlValue.toString(); m_urls.append(url); // prefixes are used to avoid name clashes with potential similar // URL paths from other JSON files. Json file basename is used // as prefix m_urlPrefixes.insert(url, jsonBaseName); } } QString AppstreamDataDownloader::calcLocalFileSavePath(const QUrl &urlToDownload) { // we are adding a prefix here to local file name to avoid possible // file name clashes with files from other JSONs // urlToDownload looks like: // "https://appstream.alpinelinux.org/data/edge/main/Components-main-aarch64.xml.gz" // "https://appstream.alpinelinux.org/data/edge/community/Components-community-aarch64.xml.gz" // future update will change them to this form: // "https://appstream.alpinelinux.org/data/edge/main/Components-aarch64.xml.gz" // "https://appstream.alpinelinux.org/data/edge/community/Components-aarch64.xml.gz" // so, file names will clash. We also need to have full URL to affect local file name // to avoid name clashes. const QString urlPrefix = m_urlPrefixes.value(urlToDownload.toString(), QString()); const QString urlPathHash = urlToDownload.path().replace(QLatin1Char('/'), QLatin1Char('_')); const QString localCacheFile = AppstreamDataDownloader::appStreamCacheDir() + QDir::separator() + urlPrefix + QLatin1Char('_') + urlPathHash; // new "~/.cache/discover/external_appstream_data/alpine-appstream-data__data_edge_main_Components-aarch64.xml.gz" return localCacheFile; } QString AppstreamDataDownloader::calcLocalFileSavePathOld(const QUrl &urlToDownload) { // Calculate what file name was there for old format. // We keep a list of "old" file names so we can delete them. // "~/.cache/discover/external_appstream_data/alpine-appstream-data_Components-main-aarch64.xml.gz" // ^ ^/| ^ ^ // | appstream cache dir -------------------|/|--- urlPrefix -----|_|-- file name const QString urlPrefix = m_urlPrefixes.value(urlToDownload.toString(), QString()); const QString urlFileName = QFileInfo(urlToDownload.path()).fileName(); const QString oldFormatFileName = AppstreamDataDownloader::appStreamCacheDir() + QDir::separator() + urlPrefix + QLatin1Char('_') + urlFileName; return oldFormatFileName; } void AppstreamDataDownloader::cleanupOldCachedFiles() { if (m_oldFormatFileNames.isEmpty()) { return; } qCDebug(LOG_ALPINEAPK) << "appstream_downloader: removing old files:"; for (const QString &oldFn : m_oldFormatFileNames) { bool ok = QFile::remove(oldFn); qCDebug(LOG_ALPINEAPK) << " " << oldFn << (ok ? "OK" : "Fail"); } } void AppstreamDataDownloader::start() { m_urls.clear(); // load json files with appdata URLs configuration const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("libdiscover/external-appstream-urls"), QStandardPaths::LocateDirectory); if (path.isEmpty()) { qCWarning(LOG_ALPINEAPK) << "external-appstream-urls directory does not exist."; return; } QDir jsonsDir(path); // search for all JSON files in that directory and load each one QFileInfoList fileList = jsonsDir.entryInfoList({QStringLiteral("*.json")}, QDir::Files); for (const QFileInfo &fi : fileList) { qCDebug(LOG_ALPINEAPK) << " reading URLs JSON: " << fi.absoluteFilePath(); loadUrlsJson(fi.absoluteFilePath()); } qCDebug(LOG_ALPINEAPK) << "appstream_downloader: urls:" << m_urls; // check if download is needed at all, maybe all files are already up to date? appStreamCacheDir(); // can create a cache dir if not exists const QDateTime dtNow = QDateTime::currentDateTime(); m_urlsToDownload.clear(); m_oldFormatFileNames.clear(); for (const QString &url : m_urls) { const QUrl urlToDownload(url, QUrl::TolerantMode); const QString localCacheFile = calcLocalFileSavePath(urlToDownload); const QFileInfo localFi(localCacheFile); if (localFi.exists()) { int modifiedSecsAgo = localFi.lastModified().secsTo(dtNow); if (modifiedSecsAgo >= m_cacheExpireSeconds) { qCDebug(LOG_ALPINEAPK) << " appstream metadata file: " << localFi.fileName() << " was last modified " << modifiedSecsAgo << " seconds ago, need to download"; m_urlsToDownload.append(url); } } else { // locally downloaded file does not even exist, we need to download it m_urlsToDownload.append(url); qCDebug(LOG_ALPINEAPK) << " appstream metadata file: " << localFi.fileName() << " does not exist, queued for downloading"; } // create a set of possible cached files with old name format const QString localCacheFileOld = calcLocalFileSavePathOld(urlToDownload); m_oldFormatFileNames.insert(localCacheFileOld); } if (m_urlsToDownload.isEmpty()) { // no need to download anything qCDebug(LOG_ALPINEAPK) << "appstream_downloader: All appstream data files " "are up to date, not downloading anything"; cleanupOldCachedFiles(); Q_EMIT downloadFinished(); return; } // If we're here, some files are outdated; download is needed qCDebug(LOG_ALPINEAPK) << "appstream_downloader: We will need to download " << m_urlsToDownload.size() << " file(s)"; const QString discoverVersion(QStringLiteral("plasma-discover %1").arg(DiscoverVersion::version)); m_jobs.clear(); for (const QString &sUrl : std::as_const(m_urlsToDownload)) { const QUrl url(sUrl, QUrl::TolerantMode); KIO::TransferJob *job = KIO::get(url, KIO::LoadType::Reload, KIO::JobFlag::HideProgressInfo); job->addMetaData(QLatin1String("UserAgent"), discoverVersion); m_jobTracker->registerJob(job); QObject::connect(job, &KJob::result, this, &AppstreamDataDownloader::onJobResult); QObject::connect(job, &KIO::TransferJob::data, this, &AppstreamDataDownloader::onJobData); m_jobs.push_back(job); } } void AppstreamDataDownloader::onJobData(KIO::Job *job, const QByteArray &data) { KIO::TransferJob *tjob = qobject_cast(job); if (data.size() < 1) { return; } // while downloading, save data to temporary file const QString filePath = calcLocalFileSavePath(tjob->url()) + QLatin1String(".tmp"); QFile fout(filePath); if (!fout.open(QIODevice::WriteOnly | QIODevice::Append)) { qCWarning(LOG_ALPINEAPK) << "appstream_downloader: failed to write: " << filePath; return; } fout.write(data); fout.close(); } void AppstreamDataDownloader::onJobResult(KJob *job) { KIO::TransferJob *tjob = qobject_cast(job); m_jobs.removeOne(tjob); m_jobTracker->unregisterJob(tjob); const QString localCacheFile = calcLocalFileSavePath(tjob->url()); const QString localCacheFileTmp = localCacheFile + QLatin1String(".tmp"); if (tjob->error()) { qCWarning(LOG_ALPINEAPK) << "appstream_downloader: failed to download: " << tjob->url(); qCWarning(LOG_ALPINEAPK) << tjob->errorString(); // error cleanup - remove temp file QFile::remove(localCacheFileTmp); } else { // success - rename tmp file to real QFile::remove(localCacheFile); // just in case, or QFile::rename() will fail QFile::rename(localCacheFileTmp, localCacheFile); m_cacheWasUpdated = true; qCDebug(LOG_ALPINEAPK) << "appstream_downloader: saved: " << localCacheFile; } tjob->deleteLater(); qCDebug(LOG_ALPINEAPK).nospace() << "appstream_downloader: " << localCacheFile << " request finished (" << m_jobs.size() << " left)"; if (m_jobs.isEmpty()) { qCDebug(LOG_ALPINEAPK) << "appstream_downloader: all downloads have finished!"; cleanupOldCachedFiles(); Q_EMIT downloadFinished(); } }