File indexing completed on 2024-11-24 04:44:35

0001 /*
0002     SPDX-FileCopyrightText: 2008 Bertjan Broeksema <broeksema@kde.org>
0003     SPDX-FileCopyrightText: 2008 Volker Krause <vkrause@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "singlefileresourcebase.h"
0009 
0010 #include <Akonadi/ChangeRecorder>
0011 #include <Akonadi/EntityDisplayAttribute>
0012 #include <Akonadi/ItemFetchScope>
0013 
0014 #include <KDirWatch>
0015 #include <KIO/Job>
0016 #include <KLocalizedString>
0017 #include <QDebug>
0018 #include <kio/global.h>
0019 
0020 #include <KConfigGroup>
0021 
0022 #include <QCryptographicHash>
0023 #include <QDir>
0024 #include <QStandardPaths>
0025 #include <QTimer>
0026 
0027 Q_DECLARE_METATYPE(QEventLoopLocker *)
0028 
0029 using namespace Akonadi;
0030 
0031 SingleFileResourceBase::SingleFileResourceBase(const QString &id)
0032     : ResourceBase(id)
0033 {
0034     connect(this, &SingleFileResourceBase::reloadConfiguration, this, [this]() {
0035         applyConfigurationChanges();
0036         reloadFile();
0037         synchronizeCollectionTree();
0038     });
0039     QTimer::singleShot(0, this, [this]() {
0040         readFile();
0041     });
0042 
0043     changeRecorder()->itemFetchScope().fetchFullPayload();
0044     changeRecorder()->fetchCollection(true);
0045 
0046     connect(changeRecorder(), &ChangeRecorder::changesAdded, this, &SingleFileResourceBase::scheduleWrite);
0047 
0048     connect(KDirWatch::self(), &KDirWatch::dirty, this, &SingleFileResourceBase::fileChanged);
0049     connect(KDirWatch::self(), &KDirWatch::created, this, &SingleFileResourceBase::fileChanged);
0050 }
0051 
0052 void SingleFileResourceBase::applyConfigurationChanges()
0053 {
0054 }
0055 
0056 KSharedConfig::Ptr SingleFileResourceBase::runtimeConfig() const
0057 {
0058     return KSharedConfig::openConfig(name() + QLatin1StringView("rc"), KConfig::SimpleConfig, QStandardPaths::CacheLocation);
0059 }
0060 
0061 bool SingleFileResourceBase::readLocalFile(const QString &fileName)
0062 {
0063     const QByteArray newHash = calculateHash(fileName);
0064     if (mCurrentHash != newHash) {
0065         if (!mCurrentHash.isEmpty()) {
0066             // There was a hash stored in the config file or a cached one from
0067             // a previous read and it is different from the hash we just read.
0068             handleHashChange();
0069         }
0070 
0071         if (!readFromFile(fileName)) {
0072             mCurrentHash.clear();
0073             mCurrentUrl = QUrl(); // reset so we don't accidentally overwrite the file
0074             return false;
0075         }
0076 
0077         if (mCurrentHash.isEmpty()) {
0078             // This is the very first time we read the file so make sure to store
0079             // the hash as writeFile() might not be called at all (e.g in case of
0080             // read only resources).
0081             saveHash(newHash);
0082         }
0083 
0084         // Only synchronize when the contents of the file have changed wrt to
0085         // the last time this file was read. Before we synchronize first
0086         // clearCache is called to make sure that the cached items get the
0087         // actual values as present in the file.
0088         invalidateCache(rootCollection());
0089         synchronize();
0090     } else {
0091         // The hash didn't change, notify implementing resources about the
0092         // actual file name that should be used when reading the file is
0093         // necessary.
0094         setLocalFileName(fileName);
0095     }
0096 
0097     mCurrentHash = newHash;
0098     return true;
0099 }
0100 
0101 void SingleFileResourceBase::setLocalFileName(const QString &fileName)
0102 {
0103     // Default implementation.
0104     if (!readFromFile(fileName)) {
0105         mCurrentHash.clear();
0106         mCurrentUrl = QUrl(); // reset so we don't accidentally overwrite the file
0107         return;
0108     }
0109 }
0110 
0111 QString SingleFileResourceBase::cacheFile() const
0112 {
0113     const QString currentDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
0114     QDir().mkpath(currentDir);
0115     return currentDir + QLatin1Char('/') + identifier();
0116 }
0117 
0118 QByteArray SingleFileResourceBase::calculateHash(const QString &fileName) const
0119 {
0120     QFile file(fileName);
0121     if (!file.exists()) {
0122         return {};
0123     }
0124 
0125     if (!file.open(QIODevice::ReadOnly)) {
0126         return {};
0127     }
0128 
0129     QCryptographicHash hash(QCryptographicHash::Md5);
0130     qint64 blockSize = 512 * 1024; // Read blocks of 512K
0131 
0132     while (!file.atEnd()) {
0133         hash.addData(file.read(blockSize));
0134     }
0135 
0136     file.close();
0137 
0138     return hash.result();
0139 }
0140 
0141 void SingleFileResourceBase::handleHashChange()
0142 {
0143     // Default implementation does nothing.
0144     qDebug() << "The hash has changed.";
0145 }
0146 
0147 QByteArray SingleFileResourceBase::loadHash() const
0148 {
0149     KConfigGroup generalGroup(runtimeConfig(), QStringLiteral("General"));
0150     return QByteArray::fromHex(generalGroup.readEntry<QByteArray>("hash", QByteArray()));
0151 }
0152 
0153 void SingleFileResourceBase::saveHash(const QByteArray &hash) const
0154 {
0155     KSharedConfig::Ptr config = runtimeConfig();
0156     KConfigGroup generalGroup(config, QStringLiteral("General"));
0157     generalGroup.writeEntry("hash", hash.toHex());
0158     config->sync();
0159 }
0160 
0161 void SingleFileResourceBase::setSupportedMimetypes(const QStringList &mimeTypes, const QString &icon)
0162 {
0163     mSupportedMimetypes = mimeTypes;
0164     mCollectionIcon = icon;
0165 }
0166 
0167 void SingleFileResourceBase::collectionChanged(const Akonadi::Collection &collection)
0168 {
0169     const QString newName = collection.displayName();
0170     if (collection.hasAttribute<EntityDisplayAttribute>()) {
0171         const auto attr = collection.attribute<EntityDisplayAttribute>();
0172         if (!attr->iconName().isEmpty()) {
0173             mCollectionIcon = attr->iconName();
0174         }
0175     }
0176 
0177     if (newName != name()) {
0178         setName(newName);
0179     }
0180 
0181     changeCommitted(collection);
0182 }
0183 
0184 void SingleFileResourceBase::reloadFile()
0185 {
0186     // Update the network setting.
0187     setNeedsNetwork(!mCurrentUrl.isEmpty() && !mCurrentUrl.isLocalFile());
0188 
0189     // if we have something loaded already, make sure we write that back in case
0190     // the settings changed
0191     if (!mCurrentUrl.isEmpty() && !readOnly()) {
0192         writeFile();
0193     }
0194 
0195     readFile();
0196 
0197     // name or rights could have changed
0198     synchronizeCollectionTree();
0199 }
0200 
0201 void SingleFileResourceBase::handleProgress(KJob *, unsigned long pct)
0202 {
0203     Q_EMIT percent(pct);
0204 }
0205 
0206 void SingleFileResourceBase::fileChanged(const QString &fileName)
0207 {
0208     if (fileName != mCurrentUrl.toLocalFile()) {
0209         return;
0210     }
0211 
0212     const QByteArray newHash = calculateHash(fileName);
0213 
0214     // There is only a need to synchronize when the file was changed by another
0215     // process. At this point we're sure that it is the file that the resource
0216     // was configured for because of the check at the beginning of this function.
0217     if (newHash == mCurrentHash) {
0218         return;
0219     }
0220 
0221     if (!mCurrentUrl.isEmpty()) {
0222         QString lostFoundFileName;
0223         const QUrl prevUrl = mCurrentUrl;
0224         int i = 0;
0225         do {
0226             lostFoundFileName = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + identifier() + QLatin1Char('/')
0227                 + prevUrl.fileName() + QLatin1Char('-') + QString::number(++i);
0228         } while (QFileInfo::exists(lostFoundFileName));
0229 
0230         // create the directory if it doesn't exist yet
0231         QDir dir = QFileInfo(lostFoundFileName).dir();
0232         if (!dir.exists()) {
0233             dir.mkpath(dir.path());
0234         }
0235 
0236         mCurrentUrl = QUrl::fromLocalFile(lostFoundFileName);
0237         writeFile();
0238         mCurrentUrl = prevUrl;
0239 
0240         const QString message = i18n(
0241             "The file '%1' was changed on disk. "
0242             "As a precaution, a backup of its previous contents has been created at '%2'.",
0243             prevUrl.toDisplayString(),
0244             QUrl::fromLocalFile(lostFoundFileName).toDisplayString());
0245         Q_EMIT warning(message);
0246     }
0247 
0248     readFile();
0249 
0250     // Notify resources, so that information bound to the file like indexes etc.
0251     // can be updated.
0252     handleHashChange();
0253     invalidateCache(rootCollection());
0254     synchronize();
0255 }
0256 
0257 void SingleFileResourceBase::scheduleWrite()
0258 {
0259     scheduleCustomTask(this, "writeFile", QVariant(true), ResourceBase::AfterChangeReplay);
0260 }
0261 
0262 void SingleFileResourceBase::slotDownloadJobResult(KJob *job)
0263 {
0264     if (job->error() && job->error() != KIO::ERR_DOES_NOT_EXIST) {
0265         const QString message = i18n("Could not load file '%1'.", mCurrentUrl.toDisplayString());
0266         qWarning() << message;
0267         Q_EMIT status(Broken, message);
0268     } else {
0269         readLocalFile(QUrl::fromLocalFile(cacheFile()).toLocalFile());
0270     }
0271 
0272     mDownloadJob = nullptr;
0273     auto ref = job->property("QEventLoopLocker").value<QEventLoopLocker *>();
0274     if (ref) {
0275         delete ref;
0276     }
0277 
0278     Q_EMIT status(Idle, i18nc("@info:status", "Ready"));
0279 }
0280 
0281 void SingleFileResourceBase::slotUploadJobResult(KJob *job)
0282 {
0283     if (job->error()) {
0284         const QString message = i18n("Could not save file '%1'.", mCurrentUrl.toDisplayString());
0285         qWarning() << message;
0286         Q_EMIT status(Broken, message);
0287     }
0288 
0289     mUploadJob = nullptr;
0290     auto ref = job->property("QEventLoopLocker").value<QEventLoopLocker *>();
0291     if (ref) {
0292         delete ref;
0293     }
0294 
0295     Q_EMIT status(Idle, i18nc("@info:status", "Ready"));
0296 }
0297 
0298 #include "moc_singlefileresourcebase.cpp"