File indexing completed on 2024-05-12 04:42:32
0001 /* 0002 SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "cache.h" 0008 #include "logging.h" 0009 #include "datatypes/attributionutil_p.h" 0010 0011 #include <KPublicTransport/Journey> 0012 #include <KPublicTransport/Location> 0013 #include <KPublicTransport/Stopover> 0014 0015 #include <QDateTime> 0016 #include <QDebug> 0017 #include <QDir> 0018 #include <QDirIterator> 0019 #include <QFile> 0020 #include <QJsonArray> 0021 #include <QJsonDocument> 0022 #include <QJsonObject> 0023 #include <QStandardPaths> 0024 0025 using namespace KPublicTransport; 0026 0027 static QString cacheBasePath() 0028 { 0029 return QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1String("/org.kde.kpublictransport/backends/"); 0030 } 0031 0032 static QString cachePath(const QString &backendId, const QString &contentType) 0033 { 0034 return cacheBasePath() + backendId + QLatin1Char('/') + contentType + QLatin1Char('/'); 0035 } 0036 0037 static QString locationExtension() 0038 { 0039 return QStringLiteral(".json"); 0040 } 0041 0042 static QString attributionExtension() 0043 { 0044 return QStringLiteral(".attribution"); 0045 } 0046 0047 template <typename T> 0048 static void addCacheEntry(const QString &typeName, const QString &backendId, const QString &cacheKey, const std::vector<T> &data, const std::vector<Attribution> &attribution, std::chrono::seconds ttl) 0049 { 0050 const auto dir = cachePath(backendId, typeName); 0051 QDir().mkpath(dir); 0052 QFile f(dir + cacheKey + locationExtension()); 0053 f.open(QFile::WriteOnly | QFile::Truncate); 0054 f.write(QJsonDocument(T::toJson(data)).toJson()); 0055 f.close(); 0056 // mtime changes need to be done without content changes to take effect 0057 f.open(QFile::WriteOnly | QFile::Append); 0058 f.setFileTime(QDateTime::currentDateTimeUtc().addSecs(ttl.count()), QFile::FileModificationTime); 0059 f.close(); 0060 0061 if (!attribution.empty()) { 0062 QFile f(dir + cacheKey + attributionExtension()); 0063 f.open(QFile::WriteOnly | QFile::Truncate); 0064 f.write(QJsonDocument(Attribution::toJson(attribution)).toJson()); 0065 f.close(); 0066 // mtime changes need to be done without content changes to take effect 0067 f.open(QFile::WriteOnly | QFile::Append); 0068 f.setFileTime(QDateTime::currentDateTimeUtc().addSecs(ttl.count()), QFile::FileModificationTime); 0069 f.close(); 0070 } 0071 } 0072 0073 static void addNegativeCacheEntry(const QString &typeName, const QString &backendId, const QString &cacheKey, std::chrono::seconds ttl) 0074 { 0075 const auto dir = cachePath(backendId, typeName); 0076 QDir().mkpath(dir); 0077 QFile f(dir + cacheKey + locationExtension()); 0078 f.open(QFile::WriteOnly | QFile::Truncate); 0079 f.setFileTime(QDateTime::currentDateTimeUtc().addSecs(ttl.count()), QFile::FileModificationTime); 0080 // empty file is used as indicator for a negative hit 0081 } 0082 0083 template <typename T> 0084 static CacheEntry<T> lookup(const QString &typeName, const QString &backendId, const QString &cacheKey) 0085 { 0086 CacheEntry<T> entry; 0087 0088 const auto dir = cachePath(backendId, typeName); 0089 QFile f (dir + cacheKey + locationExtension()); 0090 if (!f.open(QFile::ReadOnly)) { 0091 entry.type = CacheHitType::Miss; 0092 return entry; 0093 } 0094 0095 // check if this entry is still valid before using it 0096 if (f.fileTime(QFile::FileModificationTime) < QDateTime::currentDateTimeUtc()) { 0097 qDebug() << "expiring cache entry" << f.fileName(); 0098 f.close(); 0099 f.remove(); 0100 entry.type = CacheHitType::Miss; 0101 return entry; 0102 } 0103 0104 if (f.size() == 0) { 0105 entry.type = CacheHitType::Negative; 0106 return entry; 0107 } 0108 0109 entry.type = CacheHitType::Positive; 0110 entry.data = T::fromJson(QJsonDocument::fromJson(f.readAll()).array()); 0111 0112 QFile attrFile (dir + cacheKey + attributionExtension()); 0113 if (attrFile.open(QFile::ReadOnly)) { 0114 entry.attributions = Attribution::fromJson(QJsonDocument::fromJson(attrFile.readAll()).array()); 0115 } 0116 0117 return entry; 0118 } 0119 0120 void Cache::addLocationCacheEntry(const QString &backendId, const QString &cacheKey, const std::vector<Location> &data, const std::vector<Attribution> &attribution, std::chrono::seconds ttl) 0121 { 0122 addCacheEntry(QStringLiteral("location"), backendId, cacheKey, data, attribution, ttl); 0123 } 0124 0125 void Cache::addNegativeLocationCacheEntry(const QString &backendId, const QString &cacheKey, std::chrono::seconds ttl) 0126 { 0127 addNegativeCacheEntry(QStringLiteral("location"), backendId, cacheKey, ttl); 0128 } 0129 0130 CacheEntry<Location> Cache::lookupLocation(const QString &backendId, const QString &cacheKey) 0131 { 0132 return lookup<Location>(QStringLiteral("location"), backendId, cacheKey); 0133 } 0134 0135 void Cache::addNegativeDepartureCacheEntry(const QString &backendId, const QString &cacheKey, std::chrono::seconds ttl) 0136 { 0137 addNegativeCacheEntry(QStringLiteral("departure"), backendId, cacheKey, ttl); 0138 } 0139 0140 CacheEntry<Stopover> Cache::lookupDeparture(const QString &backendId, const QString &cacheKey) 0141 { 0142 return lookup<Stopover>(QStringLiteral("departure"), backendId, cacheKey); 0143 } 0144 0145 void Cache::addNegativeJourneyCacheEntry(const QString &backendId, const QString &cacheKey, std::chrono::seconds ttl) 0146 { 0147 addNegativeCacheEntry(QStringLiteral("journey"), backendId, cacheKey, ttl); 0148 } 0149 0150 CacheEntry<Journey> Cache::lookupJourney(const QString &backendId, const QString &cacheKey) 0151 { 0152 return lookup<Journey>(QStringLiteral("journey"), backendId, cacheKey); 0153 } 0154 0155 void Cache::addVehicleLayoutCacheEntry(const QString &backendId, const QString &cacheKey, const Stopover &data, const std::vector<Attribution> &attributions, std::chrono::seconds ttl) 0156 { 0157 addCacheEntry<Stopover>(QStringLiteral("vehicle"), backendId, cacheKey, {data}, attributions, ttl); 0158 } 0159 0160 void Cache::addNegativeVehicleLayoutCacheEntry(const QString& backendId, const QString& cacheKey, std::chrono::seconds ttl) 0161 { 0162 addNegativeCacheEntry(QStringLiteral("vehicle"), backendId, cacheKey, ttl); 0163 } 0164 0165 CacheEntry<Stopover> Cache::lookupVehicleLayout(const QString &backendId, const QString &cacheKey) 0166 { 0167 return lookup<Stopover>(QStringLiteral("vehicle"), backendId, cacheKey); 0168 } 0169 0170 static void expireRecursive(const QString &path) 0171 { 0172 const auto now = QDateTime::currentDateTime(); 0173 QDirIterator it(path, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); 0174 while (it.hasNext()) { 0175 it.next(); 0176 0177 if (it.fileInfo().isDir()) { 0178 expireRecursive(it.filePath()); 0179 if (QDir(it.filePath()).isEmpty()) { 0180 qCDebug(Log) << "removing empty cache directory" << it.fileName(); 0181 QDir(path).rmdir(it.filePath()); 0182 } 0183 } else if (it.fileInfo().lastModified() < now) { 0184 qCDebug(Log) << "removing expired cache entry" << it.filePath(); 0185 QDir(path).remove(it.filePath()); 0186 } 0187 } 0188 } 0189 0190 void Cache::expire() 0191 { 0192 expireRecursive(cacheBasePath()); 0193 } 0194 0195 void Cache::allCachedAttributions(std::vector<Attribution> &attrs) 0196 { 0197 QDirIterator it(cacheBasePath(), {QLatin1Char('*') + attributionExtension()}, QDir::Files | QDir::NoSymLinks, QDirIterator::Subdirectories); 0198 while (it.hasNext()) { 0199 it.next(); 0200 QFile f(it.filePath()); 0201 f.open(QFile::ReadOnly); 0202 auto cached = Attribution::fromJson(QJsonDocument::fromJson(f.readAll()).array()); 0203 AttributionUtil::merge(attrs, std::move(cached)); 0204 } 0205 }