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

source code of knewstuff/src/core/cache.cpp