KNewStuff

cache.cpp
1 /*
2  SPDX-FileCopyrightText: 2009 Frederik Gladhorn <[email protected]>
3  SPDX-FileCopyrightText: 2010 Matthias Fuchs <[email protected]>
4 
5  SPDX-License-Identifier: LGPL-2.1-or-later
6 */
7 
8 #include "cache.h"
9 
10 #include <QFile>
11 #include <QDir>
12 #include <QFileInfo>
13 #include <QFileSystemWatcher>
14 #include <QPointer>
15 #include <QTimer>
16 #include <QXmlStreamReader>
17 #include <qstandardpaths.h>
18 #include <knewstuffcore_debug.h>
19 
20 class KNSCore::CachePrivate {
21 public:
22  CachePrivate(Cache* qq)
23  : q(qq)
24  {}
25  ~CachePrivate() {}
26 
27  Cache* q;
29 
30  QPointer<QTimer> throttleTimer;
31  void throttleWrite() {
32  if (!throttleTimer) {
33  throttleTimer = new QTimer(q);
34  QObject::connect(throttleTimer, &QTimer::timeout, q, [this](){ q->writeRegistry(); });
35  throttleTimer->setSingleShot(true);
36  throttleTimer->setInterval(1000);
37  }
38  throttleTimer->start();
39  }
40 };
41 
42 using namespace KNSCore;
43 
45 Q_GLOBAL_STATIC(CacheHash, s_caches)
46 
47 Cache::Cache(const QString &appName)
48  : QObject(nullptr)
49  , d(new CachePrivate(this))
50 {
51  m_kns2ComponentName = appName;
52 
54  QDir().mkpath(path);
55  registryFile = path + appName + QStringLiteral(".knsregistry");
56  qCDebug(KNEWSTUFFCORE) << "Using registry file: " << registryFile;
57  setProperty("dirty", false); //KF6 make normal variable
58 
59  QFileSystemWatcher* watcher = new QFileSystemWatcher(QStringList{registryFile}, this);
60  std::function<void()> changeChecker = [this, &changeChecker](){
61  if (property("writingRegistry").toBool()) {
62  QTimer::singleShot(0, this, changeChecker);
63  } else {
64  setProperty("reloadingRegistry", true);
65  const QSet<KNSCore::EntryInternal> oldCache = cache;
66  cache.clear();
67  readRegistry();
68  // First run through the old cache and see if any have disappeared (at
69  // which point we need to set them as available and emit that change)
70  for (const EntryInternal &entry : oldCache) {
71  if (!cache.contains(entry)) {
72  EntryInternal removedEntry(entry);
73  removedEntry.setStatus(KNS3::Entry::Deleted);
74  Q_EMIT entryChanged(removedEntry);
75  }
76  }
77  // Then run through the new cache and see if there's any that were not
78  // in the old cache (at which point just emit those as having changed,
79  // they're already the correct status)
80  for (const EntryInternal &entry: cache) {
81  auto iterator = oldCache.constFind(entry);
82  if (iterator == oldCache.constEnd()) {
83  Q_EMIT entryChanged(entry);
84  } else if ((*iterator).status() != entry.status()) {
85  // If there are entries which are in both, but which have changed their
86  // status, we should adopt the status from the newly loaded cache in place
87  // of the one in the old cache. In reality, what this means is we just
88  // need to emit the changed signal for anything in the new cache which
89  // doesn't match the old one
90  Q_EMIT entryChanged(entry);
91  }
92  }
93  setProperty("reloadingRegistry", false);
94  }
95  };
96  connect(watcher, &QFileSystemWatcher::fileChanged, this, changeChecker);
97 }
98 
99 QSharedPointer<Cache> Cache::getCache(const QString &appName)
100 {
101  CacheHash::const_iterator it = s_caches()->constFind(appName);
102  if ((it != s_caches()->constEnd()) && !(*it).isNull()) {
103  return QSharedPointer<Cache>(*it);
104  }
105 
106  QSharedPointer<Cache> p(new Cache(appName));
107  s_caches()->insert(appName, QWeakPointer<Cache>(p));
108  QObject::connect(p.data(), &QObject::destroyed, [appName] {
109  if (auto cache = s_caches()) {
110  cache->remove(appName);
111  }
112  });
113 
114  return p;
115 }
116 
117 Cache::~Cache()
118 {
119 }
120 
121 void Cache::readRegistry()
122 {
123 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 77)
124  // read KNS2 registry first to migrate it
125  readKns2MetaFiles();
126 #endif
127 
128  QFile f(registryFile);
129  if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
130  if (QFileInfo::exists(registryFile)) {
131  qWarning() << "The file " << registryFile << " could not be opened.";
132  }
133  return;
134  }
135 
136  QXmlStreamReader reader(&f);
137  if (reader.hasError() || !reader.readNextStartElement()) {
138  qCWarning(KNEWSTUFFCORE) << "The file could not be parsed.";
139  return;
140  }
141 
142  if (reader.name() != QLatin1String("hotnewstuffregistry")) {
143  qCWarning(KNEWSTUFFCORE) << "The file doesn't seem to be of interest.";
144  return;
145  }
146 
147  for (auto token = reader.readNext(); !reader.atEnd(); token = reader.readNext()) {
148  if (token != QXmlStreamReader::StartElement)
149  continue;
150  EntryInternal e;
151  e.setEntryXML(reader);
152  e.setSource(EntryInternal::Cache);
153  cache.insert(e);
154  Q_ASSERT(reader.tokenType() == QXmlStreamReader::EndElement);
155  }
156 
157  qCDebug(KNEWSTUFFCORE) << "Cache read... entries: " << cache.size();
158 }
159 
160 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 77)
161 void Cache::readKns2MetaFiles()
162 {
163  qCDebug(KNEWSTUFFCORE) << "Loading KNS2 registry of files for the component: " << m_kns2ComponentName;
164 
165  const auto realAppName = m_kns2ComponentName.splitRef(QLatin1Char(':'))[0];
166 
168  for (QStringList::ConstIterator it = dirs.begin(); it != dirs.end(); ++it) {
169  qCDebug(KNEWSTUFFCORE) << QStringLiteral(" + Load from directory '") + (*it) + QStringLiteral("'.");
170  QDir dir((*it));
171  const QStringList files = dir.entryList(QDir::Files | QDir::Readable);
172  for (QStringList::const_iterator fit = files.begin(); fit != files.end(); ++fit) {
173  QString filepath = (*it) + QLatin1Char('/') + (*fit);
174 
175  qCDebug(KNEWSTUFFCORE) << QStringLiteral(" Load from file '") + filepath + QStringLiteral("'.");
176 
177  QFileInfo info(filepath);
178  QFile f(filepath);
179 
180  // first see if this file is even for this app
181  // because the registry contains entries for all apps
182  // FIXMEE: should be able to do this with a filter on the entryList above probably
183  QString thisAppName = QString::fromUtf8(QByteArray::fromBase64(info.baseName().toUtf8()));
184 
185  // NOTE: the ":" needs to always coincide with the separator character used in
186  // the id(Entry*) method
187  thisAppName = thisAppName.split(QLatin1Char(':'))[0];
188 
189  if (thisAppName != realAppName) {
190  continue;
191  }
192 
193  if (!f.open(QIODevice::ReadOnly)) {
194  qWarning() << "The file: " << filepath << " could not be opened.";
195  continue;
196  }
197 
198  QDomDocument doc;
199  if (!doc.setContent(&f)) {
200  qWarning() << "The file could not be parsed.";
201  return;
202  }
203  qCDebug(KNEWSTUFFCORE) << "found entry: " << doc.toString();
204 
205  QDomElement root = doc.documentElement();
206  if (root.tagName() != QLatin1String("ghnsinstall")) {
207  qWarning() << "The file doesn't seem to be of interest.";
208  return;
209  }
210 
211  // The .meta files only contain one entry
212  QDomElement stuff = root.firstChildElement(QStringLiteral("stuff"));
213  EntryInternal e;
214  e.setEntryXML(stuff);
215  e.setSource(EntryInternal::Cache);
216 
217  if (e.payload().startsWith(QLatin1String("http://download.kde.org/khotnewstuff"))) {
218  // This is 99% sure a opendesktop file, make it a real one.
219  e.setProviderId(QStringLiteral("https://api.opendesktop.org/v1/"));
220  e.setHomepage(QUrl(QString(QLatin1String("http://opendesktop.org/content/show.php?content=") + e.uniqueId())));
221 
222  } else if (e.payload().startsWith(QLatin1String("http://edu.kde.org/contrib/kvtml/"))) {
223  // kvmtl-1
224  e.setProviderId(QStringLiteral("http://edu.kde.org/contrib/kvtml/kvtml.xml"));
225  } else if (e.payload().startsWith(QLatin1String("http://edu.kde.org/contrib/kvtml2/"))) {
226  // kvmtl-2
227  e.setProviderId(QStringLiteral("http://edu.kde.org/contrib/kvtml2/provider41.xml"));
228  } else {
229  // we failed, skip
230  qWarning() << "Could not load entry: " << filepath;
231  continue;
232  }
233 
234  e.setStatus(KNS3::Entry::Installed);
235 
236  cache.insert(e);
237  QDomDocument tmp(QStringLiteral("yay"));
238  tmp.appendChild(e.entryXML());
239  qCDebug(KNEWSTUFFCORE) << "new entry: " << tmp.toString();
240 
241  f.close();
242 
243  QDir dir;
244  if (!dir.remove(filepath)) {
245  qWarning() << "could not delete old kns2 .meta file: " << filepath;
246  } else {
247  qCDebug(KNEWSTUFFCORE) << "Migrated KNS2 entry to KNS3.";
248  }
249 
250  }
251  }
252  setProperty("dirty", false);
253 }
254 #endif
255 
256 EntryInternal::List Cache::registryForProvider(const QString &providerId)
257 {
258  EntryInternal::List entries;
259  for (const EntryInternal &e : qAsConst(cache)) {
260  if (e.providerId() == providerId) {
261  entries.append(e);
262  }
263  }
264  return entries;
265 }
266 
267 void Cache::writeRegistry()
268 {
269  if (!property("dirty").toBool())
270  return;
271 
272  qCDebug(KNEWSTUFFCORE) << "Write registry";
273 
274  setProperty("writingRegistry", true);
275  QFile f(registryFile);
276  if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
277  qWarning() << "Cannot write meta information to '" << registryFile << "'.";
278  return;
279  }
280 
281  QDomDocument doc(QStringLiteral("khotnewstuff3"));
282  doc.appendChild(doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"")));
283  QDomElement root = doc.createElement(QStringLiteral("hotnewstuffregistry"));
284  doc.appendChild(root);
285 
286  for (const EntryInternal &entry : qAsConst(cache)) {
287  // Write the entry, unless the policy is CacheNever and the entry is not installed.
288  if (entry.status() == KNS3::Entry::Installed || entry.status() == KNS3::Entry::Updateable) {
289  QDomElement exml = entry.entryXML();
290  root.appendChild(exml);
291  }
292  }
293 
294  QTextStream metastream(&f);
295  metastream << doc.toByteArray();
296 
297  setProperty("dirty", false);
298  setProperty("writingRegistry", false);
299 }
300 
301 void Cache::registerChangedEntry(const KNSCore::EntryInternal &entry)
302 {
303 
304  // If we have intermediate states, like updating or installing we do not want to write them
305  if (entry.status() == KNS3::Entry::Updating || entry.status() == KNS3::Entry::Installing) {
306  return;
307  }
308  if (!property("reloadingRegistry").toBool()) {
309  setProperty("dirty", true);
310  cache.remove(entry); // If value already exists in the set, the set is left unchanged
311  cache.insert(entry);
312  d->throttleWrite();
313  }
314 }
315 
316 void Cache::insertRequest(const KNSCore::Provider::SearchRequest &request, const KNSCore::EntryInternal::List &entries)
317 {
318  // append new entries
319  auto &cacheList = d->requestCache[request.hashForRequest()];
320  for (const auto &entry : entries) {
321  if (!cacheList.contains(entry)) {
322  cacheList.append(entry);
323  }
324  }
325  qCDebug(KNEWSTUFFCORE) << request.hashForRequest() << " add: " << entries.size() << " keys: " << d->requestCache.keys();
326 }
327 
328 EntryInternal::List Cache::requestFromCache(const KNSCore::Provider::SearchRequest &request)
329 {
330  qCDebug(KNEWSTUFFCORE) << request.hashForRequest();
331  return d->requestCache.value(request.hashForRequest());
332 }
333 
334 void KNSCore::Cache::removeDeletedEntries()
335 {
337  while (i.hasNext()) {
338  const KNSCore::EntryInternal &entry = i.next();
339  bool installedFileExists{false};
340  const QStringList installedFiles = entry.installedFiles();
341  for (const auto &installedFile: installedFiles) {
342  // Handle the /* notation, BUG: 425704
343  if (installedFile.endsWith(QLatin1String("/*"))) {
344  if (QDir(installedFile.left(installedFile.size() - 2)).exists()) {
345  installedFileExists = true;
346  break;
347  }
348  } else if (QFile::exists(installedFile)) {
349  installedFileExists = true;
350  break;
351  }
352  }
353  if (!installedFileExists) {
354  i.remove();
355  setProperty("dirty", true);
356  }
357  }
358  writeRegistry();
359 }
360 
361 KNSCore::EntryInternal KNSCore::Cache::entryFromInstalledFile(const QString& installedFile) const
362 {
363  for (const EntryInternal& entry : cache) {
364  if (entry.installedFiles().contains(installedFile)) {
365  return entry;
366  }
367  }
368  return EntryInternal{};
369 }
QDomProcessingInstruction createProcessingInstruction(const QString &target, const QString &data)
QString writableLocation(QStandardPaths::StandardLocation type)
QString uniqueId() const
Get the object&#39;s unique ID.
QStringList locateAll(QStandardPaths::StandardLocation type, const QString &fileName, QStandardPaths::LocateOptions options)
QDomNode appendChild(const QDomNode &newChild)
QString toString(int indent) const const
Contains the core functionality for handling interaction with NewStuff providers. ...
int size() const const
bool remove(const QString &fileName)
bool contains(const QString &str, Qt::CaseSensitivity cs) const const
QDomElement documentElement() const const
used to keep track of a search
Definition: provider.h:67
bool exists() const const
void timeout()
QDomElement entryXML() const
get the xml string for the entry
QString payload() const
Retrieve the file name of the object.
bool exists() const const
void append(const T &value)
QString fromUtf8(const char *str, int size)
QStringList installedFiles() const
Retrieve the locally installed files.
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool setEntryXML(QXmlStreamReader &reader)
set the xml for the entry parses the xml and sets the private members accordingly used to deserialize...
void setSource(Source source)
The source of this entry can be Cache, Registry or Online -.
void fileChanged(const QString &path)
void setStatus(KNS3::Entry::Status status)
Sets the entry&#39;s status.
QList::iterator end()
bool exists() const const
KIOFILEWIDGETS_EXPORT QString dir(const QString &fileClass)
KNewStuff data entry container.
Definition: entryinternal.h:49
QByteArray fromBase64(const QByteArray &base64, QByteArray::Base64Options options)
QDomElement firstChildElement(const QString &tagName) const const
typedef ConstIterator
QString tagName() const const
void setHomepage(const QUrl &page)
Set a link to a website containing information about this entry.
QDomElement createElement(const QString &tagName)
void clear()
KNS3::Entry::Status status() const
Retrieves the entry&#39;s status.
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QCA_EXPORT void setProperty(const QString &name, const QVariant &value)
QString providerId() const
The id of the provider this entry belongs to.
QList::iterator begin()
void destroyed(QObject *obj)
bool mkpath(const QString &dirPath) const const
QByteArray toByteArray(int indent) const const
bool setContent(const QByteArray &data, bool namespaceProcessing, QString *errorMsg, int *errorLine, int *errorColumn)
KStandardDirs * dirs()
QCA_EXPORT QString appName()
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Mon Jan 18 2021 22:43:49 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.