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 }