File indexing completed on 2024-04-28 07:45:10
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"