File indexing completed on 2024-04-28 11:43:31
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 <QFile> 0012 #include <QFileInfo> 0013 #include <QFileSystemWatcher> 0014 #include <QPointer> 0015 #include <QTimer> 0016 #include <QXmlStreamReader> 0017 #include <knewstuffcore_debug.h> 0018 #include <qstandardpaths.h> 0019 0020 class KNSCore::CachePrivate 0021 { 0022 public: 0023 CachePrivate(Cache *qq) 0024 : q(qq) 0025 { 0026 } 0027 ~CachePrivate() 0028 { 0029 } 0030 0031 Cache *q; 0032 QHash<QString, EntryInternal::List> requestCache; 0033 0034 QPointer<QTimer> throttleTimer; 0035 void throttleWrite() 0036 { 0037 if (!throttleTimer) { 0038 throttleTimer = new QTimer(q); 0039 QObject::connect(throttleTimer, &QTimer::timeout, q, [this]() { 0040 q->writeRegistry(); 0041 }); 0042 throttleTimer->setSingleShot(true); 0043 throttleTimer->setInterval(1000); 0044 } 0045 throttleTimer->start(); 0046 } 0047 }; 0048 0049 using namespace KNSCore; 0050 0051 typedef QHash<QString, QWeakPointer<Cache>> CacheHash; 0052 Q_GLOBAL_STATIC(CacheHash, s_caches) 0053 Q_GLOBAL_STATIC(QFileSystemWatcher, s_watcher) 0054 0055 Cache::Cache(const QString &appName) 0056 : QObject(nullptr) 0057 , m_kns2ComponentName(appName) 0058 , d(new CachePrivate(this)) 0059 { 0060 const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/knewstuff3/"); 0061 QDir().mkpath(path); 0062 registryFile = path + appName + QStringLiteral(".knsregistry"); 0063 qCDebug(KNEWSTUFFCORE) << "Using registry file: " << registryFile; 0064 setProperty("dirty", false); // KF6 make normal variable 0065 0066 s_watcher->addPath(registryFile); 0067 0068 std::function<void()> changeChecker = [this, &changeChecker]() { 0069 if (property("writingRegistry").toBool()) { 0070 QTimer::singleShot(0, this, changeChecker); 0071 } else { 0072 setProperty("reloadingRegistry", true); 0073 const QSet<KNSCore::EntryInternal> oldCache = cache; 0074 cache.clear(); 0075 readRegistry(); 0076 // First run through the old cache and see if any have disappeared (at 0077 // which point we need to set them as available and emit that change) 0078 for (const EntryInternal &entry : oldCache) { 0079 if (!cache.contains(entry)) { 0080 EntryInternal removedEntry(entry); 0081 removedEntry.setStatus(KNS3::Entry::Deleted); 0082 Q_EMIT entryChanged(removedEntry); 0083 } 0084 } 0085 // Then run through the new cache and see if there's any that were not 0086 // in the old cache (at which point just emit those as having changed, 0087 // they're already the correct status) 0088 for (const EntryInternal &entry : cache) { 0089 auto iterator = oldCache.constFind(entry); 0090 if (iterator == oldCache.constEnd()) { 0091 Q_EMIT entryChanged(entry); 0092 } else if ((*iterator).status() != entry.status()) { 0093 // If there are entries which are in both, but which have changed their 0094 // status, we should adopt the status from the newly loaded cache in place 0095 // of the one in the old cache. In reality, what this means is we just 0096 // need to emit the changed signal for anything in the new cache which 0097 // doesn't match the old one 0098 Q_EMIT entryChanged(entry); 0099 } 0100 } 0101 setProperty("reloadingRegistry", false); 0102 } 0103 }; 0104 connect(&*s_watcher, &QFileSystemWatcher::fileChanged, this, [this, changeChecker](const QString &file) { 0105 if (file == registryFile) { 0106 changeChecker(); 0107 } 0108 }); 0109 } 0110 0111 QSharedPointer<Cache> Cache::getCache(const QString &appName) 0112 { 0113 CacheHash::const_iterator it = s_caches()->constFind(appName); 0114 if ((it != s_caches()->constEnd()) && !(*it).isNull()) { 0115 return QSharedPointer<Cache>(*it); 0116 } 0117 0118 QSharedPointer<Cache> p(new Cache(appName)); 0119 s_caches()->insert(appName, QWeakPointer<Cache>(p)); 0120 QObject::connect(p.data(), &QObject::destroyed, [appName] { 0121 if (auto cache = s_caches()) { 0122 cache->remove(appName); 0123 } 0124 }); 0125 0126 return p; 0127 } 0128 0129 Cache::~Cache() 0130 { 0131 s_watcher->removePath(registryFile); 0132 } 0133 0134 void Cache::readRegistry() 0135 { 0136 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 77) 0137 // read KNS2 registry first to migrate it 0138 readKns2MetaFiles(); 0139 #endif 0140 0141 QFile f(registryFile); 0142 if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { 0143 if (QFileInfo::exists(registryFile)) { 0144 qWarning() << "The file " << registryFile << " could not be opened."; 0145 } 0146 return; 0147 } 0148 0149 QXmlStreamReader reader(&f); 0150 if (reader.hasError() || !reader.readNextStartElement()) { 0151 qCWarning(KNEWSTUFFCORE) << "The file could not be parsed."; 0152 return; 0153 } 0154 0155 if (reader.name() != QLatin1String("hotnewstuffregistry")) { 0156 qCWarning(KNEWSTUFFCORE) << "The file doesn't seem to be of interest."; 0157 return; 0158 } 0159 0160 for (auto token = reader.readNext(); !reader.atEnd(); token = reader.readNext()) { 0161 if (token != QXmlStreamReader::StartElement) { 0162 continue; 0163 } 0164 EntryInternal e; 0165 e.setEntryXML(reader); 0166 e.setSource(EntryInternal::Cache); 0167 cache.insert(e); 0168 Q_ASSERT(reader.tokenType() == QXmlStreamReader::EndElement); 0169 } 0170 0171 qCDebug(KNEWSTUFFCORE) << "Cache read... entries: " << cache.size(); 0172 } 0173 0174 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 77) 0175 void Cache::readKns2MetaFiles() 0176 { 0177 qCDebug(KNEWSTUFFCORE) << "Loading KNS2 registry of files for the component: " << m_kns2ComponentName; 0178 0179 const auto realAppName = m_kns2ComponentName.split(QLatin1Char(':'))[0]; 0180 0181 const QStringList dirs = 0182 QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("knewstuff2-entries.registry"), QStandardPaths::LocateDirectory); 0183 for (QStringList::ConstIterator it = dirs.begin(); it != dirs.end(); ++it) { 0184 qCDebug(KNEWSTUFFCORE) << QStringLiteral(" + Load from directory '") + (*it) + QStringLiteral("'."); 0185 QDir dir((*it)); 0186 const QStringList files = dir.entryList(QDir::Files | QDir::Readable); 0187 for (QStringList::const_iterator fit = files.begin(); fit != files.end(); ++fit) { 0188 QString filepath = (*it) + QLatin1Char('/') + (*fit); 0189 0190 qCDebug(KNEWSTUFFCORE) << QStringLiteral(" Load from file '") + filepath + QStringLiteral("'."); 0191 0192 QFileInfo info(filepath); 0193 QFile f(filepath); 0194 0195 // first see if this file is even for this app 0196 // because the registry contains entries for all apps 0197 // FIXMEE: should be able to do this with a filter on the entryList above probably 0198 QString thisAppName = QString::fromUtf8(QByteArray::fromBase64(info.baseName().toUtf8())); 0199 0200 // NOTE: the ":" needs to always coincide with the separator character used in 0201 // the id(Entry*) method 0202 thisAppName = thisAppName.split(QLatin1Char(':'))[0]; 0203 0204 if (thisAppName != realAppName) { 0205 continue; 0206 } 0207 0208 if (!f.open(QIODevice::ReadOnly)) { 0209 qWarning() << "The file: " << filepath << " could not be opened."; 0210 continue; 0211 } 0212 0213 QDomDocument doc; 0214 if (!doc.setContent(&f)) { 0215 qWarning() << "The file could not be parsed."; 0216 return; 0217 } 0218 qCDebug(KNEWSTUFFCORE) << "found entry: " << doc.toString(); 0219 0220 QDomElement root = doc.documentElement(); 0221 if (root.tagName() != QLatin1String("ghnsinstall")) { 0222 qWarning() << "The file doesn't seem to be of interest."; 0223 return; 0224 } 0225 0226 // The .meta files only contain one entry 0227 QDomElement stuff = root.firstChildElement(QStringLiteral("stuff")); 0228 EntryInternal e; 0229 e.setEntryXML(stuff); 0230 e.setSource(EntryInternal::Cache); 0231 0232 if (e.payload().startsWith(QLatin1String("http://download.kde.org/khotnewstuff"))) { 0233 // This is 99% sure a opendesktop file, make it a real one. 0234 e.setProviderId(QStringLiteral("https://api.opendesktop.org/v1/")); 0235 e.setHomepage(QUrl(QString(QLatin1String("http://opendesktop.org/content/show.php?content=") + e.uniqueId()))); 0236 0237 } else if (e.payload().startsWith(QLatin1String("http://edu.kde.org/contrib/kvtml/"))) { 0238 // kvmtl-1 0239 e.setProviderId(QStringLiteral("http://edu.kde.org/contrib/kvtml/kvtml.xml")); 0240 } else if (e.payload().startsWith(QLatin1String("http://edu.kde.org/contrib/kvtml2/"))) { 0241 // kvmtl-2 0242 e.setProviderId(QStringLiteral("http://edu.kde.org/contrib/kvtml2/provider41.xml")); 0243 } else { 0244 // we failed, skip 0245 qWarning() << "Could not load entry: " << filepath; 0246 continue; 0247 } 0248 0249 e.setStatus(KNS3::Entry::Installed); 0250 0251 cache.insert(e); 0252 QDomDocument tmp(QStringLiteral("yay")); 0253 tmp.appendChild(e.entryXML()); 0254 qCDebug(KNEWSTUFFCORE) << "new entry: " << tmp.toString(); 0255 0256 f.close(); 0257 0258 QDir dir; 0259 if (!dir.remove(filepath)) { 0260 qWarning() << "could not delete old kns2 .meta file: " << filepath; 0261 } else { 0262 qCDebug(KNEWSTUFFCORE) << "Migrated KNS2 entry to KNS3."; 0263 } 0264 } 0265 } 0266 setProperty("dirty", false); 0267 } 0268 #endif 0269 0270 EntryInternal::List Cache::registryForProvider(const QString &providerId) 0271 { 0272 EntryInternal::List entries; 0273 for (const EntryInternal &e : std::as_const(cache)) { 0274 if (e.providerId() == providerId) { 0275 entries.append(e); 0276 } 0277 } 0278 return entries; 0279 } 0280 0281 EntryInternal::List Cache::registry() const 0282 { 0283 EntryInternal::List entries; 0284 for (const EntryInternal &e : std::as_const(cache)) { 0285 entries.append(e); 0286 } 0287 return entries; 0288 } 0289 0290 void Cache::writeRegistry() 0291 { 0292 if (!property("dirty").toBool()) { 0293 return; 0294 } 0295 0296 qCDebug(KNEWSTUFFCORE) << "Write registry"; 0297 0298 setProperty("writingRegistry", true); 0299 QFile f(registryFile); 0300 if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { 0301 qWarning() << "Cannot write meta information to '" << registryFile << "'."; 0302 return; 0303 } 0304 0305 QDomDocument doc(QStringLiteral("khotnewstuff3")); 0306 doc.appendChild(doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"UTF-8\""))); 0307 QDomElement root = doc.createElement(QStringLiteral("hotnewstuffregistry")); 0308 doc.appendChild(root); 0309 0310 for (const EntryInternal &entry : std::as_const(cache)) { 0311 // Write the entry, unless the policy is CacheNever and the entry is not installed. 0312 if (entry.status() == KNS3::Entry::Installed || entry.status() == KNS3::Entry::Updateable) { 0313 QDomElement exml = entry.entryXML(); 0314 root.appendChild(exml); 0315 } 0316 } 0317 0318 QTextStream metastream(&f); 0319 metastream << doc.toByteArray(); 0320 0321 setProperty("dirty", false); 0322 setProperty("writingRegistry", false); 0323 } 0324 0325 void Cache::registerChangedEntry(const KNSCore::EntryInternal &entry) 0326 { 0327 // If we have intermediate states, like updating or installing we do not want to write them 0328 if (entry.status() == KNS3::Entry::Updating || entry.status() == KNS3::Entry::Installing) { 0329 return; 0330 } 0331 if (!property("reloadingRegistry").toBool()) { 0332 setProperty("dirty", true); 0333 cache.remove(entry); // If value already exists in the set, the set is left unchanged 0334 cache.insert(entry); 0335 d->throttleWrite(); 0336 } 0337 } 0338 0339 void Cache::insertRequest(const KNSCore::Provider::SearchRequest &request, const KNSCore::EntryInternal::List &entries) 0340 { 0341 // append new entries 0342 auto &cacheList = d->requestCache[request.hashForRequest()]; 0343 for (const auto &entry : entries) { 0344 if (!cacheList.contains(entry)) { 0345 cacheList.append(entry); 0346 } 0347 } 0348 qCDebug(KNEWSTUFFCORE) << request.hashForRequest() << " add: " << entries.size() << " keys: " << d->requestCache.keys(); 0349 } 0350 0351 EntryInternal::List Cache::requestFromCache(const KNSCore::Provider::SearchRequest &request) 0352 { 0353 qCDebug(KNEWSTUFFCORE) << request.hashForRequest(); 0354 return d->requestCache.value(request.hashForRequest()); 0355 } 0356 0357 void KNSCore::Cache::removeDeletedEntries() 0358 { 0359 QMutableSetIterator<KNSCore::EntryInternal> i(cache); 0360 while (i.hasNext()) { 0361 const KNSCore::EntryInternal &entry = i.next(); 0362 bool installedFileExists{false}; 0363 const QStringList installedFiles = entry.installedFiles(); 0364 for (const auto &installedFile : installedFiles) { 0365 // Handle the /* notation, BUG: 425704 0366 if (installedFile.endsWith(QLatin1String("/*"))) { 0367 if (QDir(installedFile.left(installedFile.size() - 2)).exists()) { 0368 installedFileExists = true; 0369 break; 0370 } 0371 } else if (QFile::exists(installedFile)) { 0372 installedFileExists = true; 0373 break; 0374 } 0375 } 0376 if (!installedFileExists) { 0377 i.remove(); 0378 setProperty("dirty", true); 0379 } 0380 } 0381 writeRegistry(); 0382 } 0383 0384 KNSCore::EntryInternal KNSCore::Cache::entryFromInstalledFile(const QString &installedFile) const 0385 { 0386 for (const EntryInternal &entry : cache) { 0387 if (entry.installedFiles().contains(installedFile)) { 0388 return entry; 0389 } 0390 } 0391 return EntryInternal{}; 0392 } 0393 0394 #include "moc_cache.cpp"