KNewStuff

cache2.cpp
1/*
2 SPDX-FileCopyrightText: 2009 Frederik Gladhorn <gladhorn@kde.org>
3 SPDX-FileCopyrightText: 2010 Matthias Fuchs <mat69@gmx.net>
4
5 SPDX-License-Identifier: LGPL-2.1-or-later
6*/
7
8#include "cache2_p.h"
9
10#include <QDir>
11#include <QDomElement>
12#include <QFile>
13#include <QFileInfo>
14#include <QFileSystemWatcher>
15#include <QPointer>
16#include <QTimer>
17#include <QXmlStreamReader>
18#include <knewstuffcore_debug.h>
19#include <qstandardpaths.h>
20
21#include "searchrequest.h"
22#include "searchrequest_p.h"
23
24class KNSCore::Cache2Private
25{
26public:
27 Cache2Private(Cache2 *qq)
28 : q(qq)
29 {
30 }
31 ~Cache2Private()
32 {
33 }
34
35 Cache2 *q;
36 QHash<QString, Entry::List> requestCache;
37
38 QPointer<QTimer> throttleTimer;
39
40 // The file that is used to keep track of downloaded entries
41 QString registryFile;
42
43 QSet<Entry> cache;
44
45 bool dirty = false;
46 bool writingRegistry = false;
47 bool reloadingRegistry = false;
48
49 void throttleWrite()
50 {
51 if (!throttleTimer) {
52 throttleTimer = new QTimer(q);
53 QObject::connect(throttleTimer, &QTimer::timeout, q, [this]() {
54 q->writeRegistry();
55 });
56 throttleTimer->setSingleShot(true);
57 throttleTimer->setInterval(1000);
58 }
59 throttleTimer->start();
60 }
61};
62
63using namespace KNSCore;
64
66Q_GLOBAL_STATIC(CacheHash, s_caches)
67Q_GLOBAL_STATIC(QFileSystemWatcher, s_watcher)
68
69Cache2::Cache2(const QString &appName)
70 : QObject(nullptr)
71 , d(new Cache2Private(this))
72{
74 QDir().mkpath(path);
75 d->registryFile = path + appName + QStringLiteral(".knsregistry");
76 qCDebug(KNEWSTUFFCORE) << "Using registry file: " << d->registryFile;
77
78 s_watcher->addPath(d->registryFile);
79
80 std::function<void()> changeChecker = [this, &changeChecker]() {
81 if (d->writingRegistry) {
82 QTimer::singleShot(0, this, changeChecker);
83 } else {
84 d->reloadingRegistry = true;
85 const QSet<KNSCore::Entry> oldCache = d->cache;
86 d->cache.clear();
87 readRegistry();
88 // First run through the old cache and see if any have disappeared (at
89 // which point we need to set them as available and emit that change)
90 for (const Entry &entry : oldCache) {
91 if (!d->cache.contains(entry) && entry.status() != KNSCore::Entry::Deleted) {
92 Entry removedEntry(entry);
93 removedEntry.setEntryDeleted();
94 Q_EMIT entryChanged(removedEntry);
95 }
96 }
97 // Then run through the new cache and see if there's any that were not
98 // in the old cache (at which point just emit those as having changed,
99 // they're already the correct status)
100 for (const Entry &entry : std::as_const(d->cache)) {
101 auto iterator = oldCache.constFind(entry);
102 if (iterator == oldCache.constEnd()) {
103 Q_EMIT entryChanged(entry);
104 } else if ((*iterator).status() != entry.status()) {
105 // If there are entries which are in both, but which have changed their
106 // status, we should adopt the status from the newly loaded cache in place
107 // of the one in the old cache. In reality, what this means is we just
108 // need to emit the changed signal for anything in the new cache which
109 // doesn't match the old one
110 Q_EMIT entryChanged(entry);
111 }
112 }
113 d->reloadingRegistry = false;
114 }
115 };
116 connect(&*s_watcher, &QFileSystemWatcher::fileChanged, this, [this, changeChecker](const QString &file) {
117 if (file == d->registryFile) {
118 changeChecker();
119 }
120 });
121}
122
123QSharedPointer<Cache2> Cache2::getCache(const QString &appName)
124{
125 CacheHash::const_iterator it = s_caches()->constFind(appName);
126 if ((it != s_caches()->constEnd()) && !(*it).isNull()) {
127 return QSharedPointer<Cache2>(*it);
128 }
129
130 QSharedPointer<Cache2> p(new Cache2(appName));
131 s_caches()->insert(appName, QWeakPointer<Cache2>(p));
132 QObject::connect(p.data(), &QObject::destroyed, [appName] {
133 if (auto cache = s_caches()) {
134 cache->remove(appName);
135 }
136 });
137
138 return p;
139}
140
141Cache2::~Cache2()
142{
143 s_watcher->removePath(d->registryFile);
144}
145
146void Cache2::readRegistry()
147{
148 QFile f(d->registryFile);
149 if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
150 if (QFileInfo::exists(d->registryFile)) {
151 qWarning() << "The file " << d->registryFile << " could not be opened.";
152 }
153 return;
154 }
155
156 QXmlStreamReader reader(&f);
157 if (reader.hasError() || !reader.readNextStartElement()) {
158 qCWarning(KNEWSTUFFCORE) << "The file could not be parsed.";
159 return;
160 }
161
162 if (reader.name() != QLatin1String("hotnewstuffregistry")) {
163 qCWarning(KNEWSTUFFCORE) << "The file doesn't seem to be of interest.";
164 return;
165 }
166
167 for (auto token = reader.readNext(); !reader.atEnd(); token = reader.readNext()) {
168 if (token != QXmlStreamReader::StartElement) {
169 continue;
170 }
171 Entry e;
172 e.setEntryXML(reader);
173 e.setSource(Entry::Cache);
174 d->cache.insert(e);
175 Q_ASSERT(reader.tokenType() == QXmlStreamReader::EndElement);
176 }
177
178 qCDebug(KNEWSTUFFCORE) << "Cache read... entries: " << d->cache.size();
179}
180
181Entry::List Cache2::registryForProvider(const QString &providerId)
182{
183 Entry::List entries;
184 for (const Entry &e : std::as_const(d->cache)) {
185 if (e.providerId() == providerId) {
186 entries.append(e);
187 }
188 }
189 return entries;
190}
191
192Entry::List Cache2::registry() const
193{
194 Entry::List entries;
195 for (const Entry &e : std::as_const(d->cache)) {
196 entries.append(e);
197 }
198 return entries;
199}
200
201void Cache2::writeRegistry()
202{
203 if (!d->dirty) {
204 return;
205 }
206
207 qCDebug(KNEWSTUFFCORE) << "Write registry";
208
209 d->writingRegistry = true;
210 QFile f(d->registryFile);
211 if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
212 qWarning() << "Cannot write meta information to" << d->registryFile;
213 return;
214 }
215
216 QDomDocument doc(QStringLiteral("khotnewstuff3"));
217 doc.appendChild(doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"")));
218 QDomElement root = doc.createElement(QStringLiteral("hotnewstuffregistry"));
219 doc.appendChild(root);
220
221 for (const Entry &entry : std::as_const(d->cache)) {
222 // Write the entry, unless the policy is CacheNever and the entry is not installed.
223 if (entry.status() == KNSCore::Entry::Installed || entry.status() == KNSCore::Entry::Updateable) {
224 QDomElement exml = entry.entryXML();
225 root.appendChild(exml);
226 }
227 }
228
229 QTextStream metastream(&f);
230 metastream << doc.toByteArray();
231
232 d->dirty = false;
233 d->writingRegistry = false;
234}
235
236void Cache2::registerChangedEntry(const KNSCore::Entry &entry)
237{
238 // If we have intermediate states, like updating or installing we do not want to write them
239 if (entry.status() == KNSCore::Entry::Updating || entry.status() == KNSCore::Entry::Installing) {
240 return;
241 }
242 if (!d->reloadingRegistry) {
243 d->dirty = true;
244 d->cache.remove(entry); // If value already exists in the set, the set is left unchanged
245 d->cache.insert(entry);
246 d->throttleWrite();
247 }
248}
249
250void Cache2::insertRequest(const KNSCore::SearchRequest &request, const KNSCore::Entry::List &entries)
251{
252 // append new entries
253 auto &cacheList = d->requestCache[request.d->hashForRequest()];
254 for (const auto &entry : entries) {
255 if (!cacheList.contains(entry)) {
256 cacheList.append(entry);
257 }
258 }
259 qCDebug(KNEWSTUFFCORE) << request.d->hashForRequest() << " add to cache: " << entries.size() << " keys: " << d->requestCache.keys();
260}
261
262Entry::List Cache2::requestFromCache(const KNSCore::SearchRequest &request)
263{
264 qCDebug(KNEWSTUFFCORE) << "from cache" << request.d->hashForRequest();
265 return d->requestCache.value(request.d->hashForRequest());
266}
267
268void KNSCore::Cache2::removeDeletedEntries()
269{
271 while (i.hasNext()) {
272 const KNSCore::Entry &entry = i.next();
273 bool installedFileExists{false};
274 const QStringList installedFiles = entry.installedFiles();
275 for (const auto &installedFile : installedFiles) {
276 // Handle the /* notation, BUG: 425704
277 if (installedFile.endsWith(QLatin1String("/*"))) {
278 if (QDir(installedFile.left(installedFile.size() - 2)).exists()) {
279 installedFileExists = true;
280 break;
281 }
282 } else if (QFile::exists(installedFile)) {
283 installedFileExists = true;
284 break;
285 }
286 }
287 if (!installedFileExists) {
288 i.remove();
289 d->dirty = true;
290 }
291 }
292 writeRegistry();
293}
294
295KNSCore::Entry KNSCore::Cache2::entryFromInstalledFile(const QString &installedFile) const
296{
297 for (const Entry &entry : std::as_const(d->cache)) {
298 if (entry.installedFiles().contains(installedFile)) {
299 return entry;
300 }
301 }
302 return Entry{};
303}
KNewStuff data entry container.
Definition entry.h:48
bool setEntryXML(QXmlStreamReader &reader)
set the xml for the entry parses the xml and sets the private members accordingly used to deserialize...
Definition entry.cpp:431
QStringList installedFiles() const
Retrieve the locally installed files.
Definition entry.cpp:339
void setSource(Source source)
The source of this entry can be Cache, Registry or Online -.
Definition entry.cpp:319
A search request.
QString path(const QString &relativePath)
QCA_EXPORT QString appName()
bool exists() const const
bool mkpath(const QString &dirPath) const const
QDomNode appendChild(const QDomNode &newChild)
bool exists() const const
bool exists() const const
void fileChanged(const QString &path)
void append(QList< T > &&value)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void destroyed(QObject *obj)
void clear()
QString writableLocation(StandardLocation type)
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
void timeout()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 15 2024 11:54:00 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.