File indexing completed on 2024-04-14 03:54:06

0001 /*
0002     SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
0003     SPDX-FileCopyrightText: 2010 Matthias Fuchs <mat69@gmx.net>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-or-later
0006 */
0007 
0008 #include "cache.h"
0009 
0010 #include <QDir>
0011 #include <QDomElement>
0012 #include <QFile>
0013 #include <QFileInfo>
0014 #include <QFileSystemWatcher>
0015 #include <QPointer>
0016 #include <QTimer>
0017 #include <QXmlStreamReader>
0018 #include <knewstuffcore_debug.h>
0019 #include <qstandardpaths.h>
0020 
0021 class KNSCore::CachePrivate
0022 {
0023 public:
0024     CachePrivate(Cache *qq)
0025         : q(qq)
0026     {
0027     }
0028     ~CachePrivate()
0029     {
0030     }
0031 
0032     Cache *q;
0033     QHash<QString, Entry::List> requestCache;
0034 
0035     QPointer<QTimer> throttleTimer;
0036 
0037     // The file that is used to keep track of downloaded entries
0038     QString registryFile;
0039 
0040     QSet<Entry> cache;
0041 
0042     bool dirty = false;
0043     bool writingRegistry = false;
0044     bool reloadingRegistry = false;
0045 
0046     void throttleWrite()
0047     {
0048         if (!throttleTimer) {
0049             throttleTimer = new QTimer(q);
0050             QObject::connect(throttleTimer, &QTimer::timeout, q, [this]() {
0051                 q->writeRegistry();
0052             });
0053             throttleTimer->setSingleShot(true);
0054             throttleTimer->setInterval(1000);
0055         }
0056         throttleTimer->start();
0057     }
0058 };
0059 
0060 using namespace KNSCore;
0061 
0062 typedef QHash<QString, QWeakPointer<Cache>> CacheHash;
0063 Q_GLOBAL_STATIC(CacheHash, s_caches)
0064 Q_GLOBAL_STATIC(QFileSystemWatcher, s_watcher)
0065 
0066 Cache::Cache(const QString &appName)
0067     : QObject(nullptr)
0068     , d(new CachePrivate(this))
0069 {
0070     const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/knewstuff3/");
0071     QDir().mkpath(path);
0072     d->registryFile = path + appName + QStringLiteral(".knsregistry");
0073     qCDebug(KNEWSTUFFCORE) << "Using registry file: " << d->registryFile;
0074 
0075     s_watcher->addPath(d->registryFile);
0076 
0077     std::function<void()> changeChecker = [this, &changeChecker]() {
0078         if (d->writingRegistry) {
0079             QTimer::singleShot(0, this, changeChecker);
0080         } else {
0081             d->reloadingRegistry = true;
0082             const QSet<KNSCore::Entry> oldCache = d->cache;
0083             d->cache.clear();
0084             readRegistry();
0085             // First run through the old cache and see if any have disappeared (at
0086             // which point we need to set them as available and emit that change)
0087             for (const Entry &entry : oldCache) {
0088                 if (!d->cache.contains(entry) && entry.status() != KNSCore::Entry::Deleted) {
0089                     Entry removedEntry(entry);
0090                     removedEntry.setEntryDeleted();
0091                     Q_EMIT entryChanged(removedEntry);
0092                 }
0093             }
0094             // Then run through the new cache and see if there's any that were not
0095             // in the old cache (at which point just emit those as having changed,
0096             // they're already the correct status)
0097             for (const Entry &entry : std::as_const(d->cache)) {
0098                 auto iterator = oldCache.constFind(entry);
0099                 if (iterator == oldCache.constEnd()) {
0100                     Q_EMIT entryChanged(entry);
0101                 } else if ((*iterator).status() != entry.status()) {
0102                     // If there are entries which are in both, but which have changed their
0103                     // status, we should adopt the status from the newly loaded cache in place
0104                     // of the one in the old cache. In reality, what this means is we just
0105                     // need to emit the changed signal for anything in the new cache which
0106                     // doesn't match the old one
0107                     Q_EMIT entryChanged(entry);
0108                 }
0109             }
0110             d->reloadingRegistry = false;
0111         }
0112     };
0113     connect(&*s_watcher, &QFileSystemWatcher::fileChanged, this, [this, changeChecker](const QString &file) {
0114         if (file == d->registryFile) {
0115             changeChecker();
0116         }
0117     });
0118 }
0119 
0120 QSharedPointer<Cache> Cache::getCache(const QString &appName)
0121 {
0122     CacheHash::const_iterator it = s_caches()->constFind(appName);
0123     if ((it != s_caches()->constEnd()) && !(*it).isNull()) {
0124         return QSharedPointer<Cache>(*it);
0125     }
0126 
0127     QSharedPointer<Cache> p(new Cache(appName));
0128     s_caches()->insert(appName, QWeakPointer<Cache>(p));
0129     QObject::connect(p.data(), &QObject::destroyed, [appName] {
0130         if (auto cache = s_caches()) {
0131             cache->remove(appName);
0132         }
0133     });
0134 
0135     return p;
0136 }
0137 
0138 Cache::~Cache()
0139 {
0140     s_watcher->removePath(d->registryFile);
0141 }
0142 
0143 void Cache::readRegistry()
0144 {
0145     QFile f(d->registryFile);
0146     if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
0147         if (QFileInfo::exists(d->registryFile)) {
0148             qWarning() << "The file " << d->registryFile << " could not be opened.";
0149         }
0150         return;
0151     }
0152 
0153     QXmlStreamReader reader(&f);
0154     if (reader.hasError() || !reader.readNextStartElement()) {
0155         qCWarning(KNEWSTUFFCORE) << "The file could not be parsed.";
0156         return;
0157     }
0158 
0159     if (reader.name() != QLatin1String("hotnewstuffregistry")) {
0160         qCWarning(KNEWSTUFFCORE) << "The file doesn't seem to be of interest.";
0161         return;
0162     }
0163 
0164     for (auto token = reader.readNext(); !reader.atEnd(); token = reader.readNext()) {
0165         if (token != QXmlStreamReader::StartElement) {
0166             continue;
0167         }
0168         Entry e;
0169         e.setEntryXML(reader);
0170         e.setSource(Entry::Cache);
0171         d->cache.insert(e);
0172         Q_ASSERT(reader.tokenType() == QXmlStreamReader::EndElement);
0173     }
0174 
0175     qCDebug(KNEWSTUFFCORE) << "Cache read... entries: " << d->cache.size();
0176 }
0177 
0178 Entry::List Cache::registryForProvider(const QString &providerId)
0179 {
0180     Entry::List entries;
0181     for (const Entry &e : std::as_const(d->cache)) {
0182         if (e.providerId() == providerId) {
0183             entries.append(e);
0184         }
0185     }
0186     return entries;
0187 }
0188 
0189 Entry::List Cache::registry() const
0190 {
0191     Entry::List entries;
0192     for (const Entry &e : std::as_const(d->cache)) {
0193         entries.append(e);
0194     }
0195     return entries;
0196 }
0197 
0198 void Cache::writeRegistry()
0199 {
0200     if (!d->dirty) {
0201         return;
0202     }
0203 
0204     qCDebug(KNEWSTUFFCORE) << "Write registry";
0205 
0206     d->writingRegistry = true;
0207     QFile f(d->registryFile);
0208     if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
0209         qWarning() << "Cannot write meta information to" << d->registryFile;
0210         return;
0211     }
0212 
0213     QDomDocument doc(QStringLiteral("khotnewstuff3"));
0214     doc.appendChild(doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"")));
0215     QDomElement root = doc.createElement(QStringLiteral("hotnewstuffregistry"));
0216     doc.appendChild(root);
0217 
0218     for (const Entry &entry : std::as_const(d->cache)) {
0219         // Write the entry, unless the policy is CacheNever and the entry is not installed.
0220         if (entry.status() == KNSCore::Entry::Installed || entry.status() == KNSCore::Entry::Updateable) {
0221             QDomElement exml = entry.entryXML();
0222             root.appendChild(exml);
0223         }
0224     }
0225 
0226     QTextStream metastream(&f);
0227     metastream << doc.toByteArray();
0228 
0229     d->dirty = false;
0230     d->writingRegistry = false;
0231 }
0232 
0233 void Cache::registerChangedEntry(const KNSCore::Entry &entry)
0234 {
0235     // If we have intermediate states, like updating or installing we do not want to write them
0236     if (entry.status() == KNSCore::Entry::Updating || entry.status() == KNSCore::Entry::Installing) {
0237         return;
0238     }
0239     if (!d->reloadingRegistry) {
0240         d->dirty = true;
0241         d->cache.remove(entry); // If value already exists in the set, the set is left unchanged
0242         d->cache.insert(entry);
0243         d->throttleWrite();
0244     }
0245 }
0246 
0247 void Cache::insertRequest(const KNSCore::Provider::SearchRequest &request, const KNSCore::Entry::List &entries)
0248 {
0249     // append new entries
0250     auto &cacheList = d->requestCache[request.hashForRequest()];
0251     for (const auto &entry : entries) {
0252         if (!cacheList.contains(entry)) {
0253             cacheList.append(entry);
0254         }
0255     }
0256     qCDebug(KNEWSTUFFCORE) << request.hashForRequest() << " add to cache: " << entries.size() << " keys: " << d->requestCache.keys();
0257 }
0258 
0259 Entry::List Cache::requestFromCache(const KNSCore::Provider::SearchRequest &request)
0260 {
0261     qCDebug(KNEWSTUFFCORE) << "from cache" << request.hashForRequest();
0262     return d->requestCache.value(request.hashForRequest());
0263 }
0264 
0265 void KNSCore::Cache::removeDeletedEntries()
0266 {
0267     QMutableSetIterator<KNSCore::Entry> i(d->cache);
0268     while (i.hasNext()) {
0269         const KNSCore::Entry &entry = i.next();
0270         bool installedFileExists{false};
0271         const QStringList installedFiles = entry.installedFiles();
0272         for (const auto &installedFile : installedFiles) {
0273             // Handle the /* notation, BUG: 425704
0274             if (installedFile.endsWith(QLatin1String("/*"))) {
0275                 if (QDir(installedFile.left(installedFile.size() - 2)).exists()) {
0276                     installedFileExists = true;
0277                     break;
0278                 }
0279             } else if (QFile::exists(installedFile)) {
0280                 installedFileExists = true;
0281                 break;
0282             }
0283         }
0284         if (!installedFileExists) {
0285             i.remove();
0286             d->dirty = true;
0287         }
0288     }
0289     writeRegistry();
0290 }
0291 
0292 KNSCore::Entry KNSCore::Cache::entryFromInstalledFile(const QString &installedFile) const
0293 {
0294     for (const Entry &entry : std::as_const(d->cache)) {
0295         if (entry.installedFiles().contains(installedFile)) {
0296             return entry;
0297         }
0298     }
0299     return Entry{};
0300 }
0301 
0302 #include "moc_cache.cpp"