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     SPDX-FileCopyrightText: 2010 David Jarvie <djarvie@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #pragma once
0010 
0011 #include "akonadi-singlefileresource_export.h"
0012 #include "singlefileresourcebase.h"
0013 
0014 #include <Akonadi/EntityDisplayAttribute>
0015 
0016 #include <KDirWatch>
0017 #include <KIO/FileCopyJob>
0018 #include <KIO/Job>
0019 #include <KLocalizedString>
0020 
0021 #include <Akonadi/CachePolicy>
0022 #include <Akonadi/CollectionModifyJob>
0023 #include <QDebug>
0024 #include <QDir>
0025 #include <QEventLoopLocker>
0026 #include <QFile>
0027 
0028 Q_DECLARE_METATYPE(QEventLoopLocker *)
0029 
0030 namespace Akonadi
0031 {
0032 /**
0033  * Base class for single file based resources.
0034  */
0035 template<typename Settings>
0036 class SingleFileResource : public SingleFileResourceBase
0037 {
0038 public:
0039     SingleFileResource(const QString &id)
0040         : SingleFileResourceBase(id)
0041         , mSettings(new Settings(config()))
0042     {
0043         // The resource needs network when the path refers to a non local file.
0044         setNeedsNetwork(!QUrl::fromUserInput(mSettings->path()).isLocalFile());
0045     }
0046 
0047     ~SingleFileResource() override
0048     {
0049         delete mSettings;
0050     }
0051 
0052     void applyConfigurationChanges() override
0053     {
0054         mSettings->load();
0055     }
0056 
0057     /**
0058      * Read changes from the backend file.
0059      */
0060     void readFile(bool taskContext = false) override
0061     {
0062         if (KDirWatch::self()->contains(mCurrentUrl.toLocalFile())) {
0063             KDirWatch::self()->removeFile(mCurrentUrl.toLocalFile());
0064         }
0065 
0066         if (mSettings->path().isEmpty()) {
0067             const QString message = i18n("No file selected.");
0068             qWarning() << message;
0069             Q_EMIT status(NotConfigured, i18n("The resource not configured yet"));
0070             if (taskContext) {
0071                 cancelTask();
0072             }
0073             return;
0074         }
0075 
0076         mCurrentUrl = QUrl::fromUserInput(mSettings->path()); // the string contains the scheme if remote, doesn't if local path
0077         if (mCurrentHash.isEmpty()) {
0078             // First call to readFile() lets see if there is a hash stored in a
0079             // cache file. If both are the same than there is no need to load the
0080             // file and synchronize the resource.
0081             mCurrentHash = loadHash();
0082         }
0083 
0084         if (mCurrentUrl.isLocalFile()) {
0085             if (mSettings->displayName().isEmpty() && (name().isEmpty() || name() == identifier()) && !mCurrentUrl.isEmpty()) {
0086                 setName(mCurrentUrl.fileName());
0087             }
0088 
0089             // check if the file does not exist yet, if so, create it
0090             if (!QFile::exists(mCurrentUrl.toLocalFile())) {
0091                 QFile f(mCurrentUrl.toLocalFile());
0092 
0093                 // first create try to create the directory the file should be located in
0094                 QDir dir = QFileInfo(f).dir();
0095                 if (!dir.exists()) {
0096                     dir.mkpath(dir.path());
0097                 }
0098 
0099                 if (f.open(QIODevice::WriteOnly) && f.resize(0)) {
0100                     Q_EMIT status(Idle, i18nc("@info:status", "Ready"));
0101                 } else {
0102                     const QString message = i18n("Could not create file '%1'.", mCurrentUrl.toDisplayString());
0103                     qWarning() << message;
0104                     Q_EMIT status(Broken, message);
0105                     mCurrentUrl.clear();
0106                     if (taskContext) {
0107                         cancelTask();
0108                     }
0109                     return;
0110                 }
0111             }
0112 
0113             // Cache, because readLocalFile will clear mCurrentUrl on failure.
0114             const QString localFileName = mCurrentUrl.toLocalFile();
0115             if (!readLocalFile(localFileName)) {
0116                 const QString message = i18n("Could not read file '%1'", localFileName);
0117                 qWarning() << message;
0118                 Q_EMIT status(Broken, message);
0119                 if (taskContext) {
0120                     cancelTask();
0121                 }
0122                 return;
0123             }
0124 
0125             if (mSettings->monitorFile()) {
0126                 KDirWatch::self()->addFile(localFileName);
0127             }
0128 
0129             Q_EMIT status(Idle, i18nc("@info:status", "Ready"));
0130         } else { // !mCurrentUrl.isLocalFile()
0131             if (mDownloadJob) {
0132                 const QString message = i18n("Another download is still in progress.");
0133                 qWarning() << message;
0134                 Q_EMIT error(message);
0135                 if (taskContext) {
0136                     cancelTask();
0137                 }
0138                 return;
0139             }
0140 
0141             if (mUploadJob) {
0142                 const QString message = i18n("Another file upload is still in progress.");
0143                 qWarning() << message;
0144                 Q_EMIT error(message);
0145                 if (taskContext) {
0146                     cancelTask();
0147                 }
0148                 return;
0149             }
0150 
0151             auto ref = new QEventLoopLocker();
0152             // NOTE: Test what happens with remotefile -> save, close before save is finished.
0153             mDownloadJob = KIO::file_copy(mCurrentUrl, QUrl::fromLocalFile(cacheFile()), -1, KIO::Overwrite | KIO::DefaultFlags | KIO::HideProgressInfo);
0154             mDownloadJob->setProperty("QEventLoopLocker", QVariant::fromValue(ref));
0155             connect(mDownloadJob, &KJob::result, this, &SingleFileResource<Settings>::slotDownloadJobResult);
0156             connect(mDownloadJob, SIGNAL(percent(KJob *, ulong)), SLOT(handleProgress(KJob *, ulong)));
0157 
0158             Q_EMIT status(Running, i18n("Downloading remote file."));
0159         }
0160 
0161         const QString display = mSettings->displayName();
0162         if (!display.isEmpty()) {
0163             setName(display);
0164         }
0165     }
0166 
0167     void writeFile(const QVariant &task_context) override
0168     {
0169         writeFile(task_context.canConvert<bool>() && task_context.toBool());
0170     }
0171 
0172     /**
0173      * Write changes to the backend file.
0174      */
0175     void writeFile(bool taskContext = false) override
0176     {
0177         if (mSettings->readOnly()) {
0178             const QString message = i18n("Trying to write to a read-only file: '%1'.", mSettings->path());
0179             qWarning() << message;
0180             Q_EMIT error(message);
0181             if (taskContext) {
0182                 cancelTask();
0183             }
0184             return;
0185         }
0186 
0187         // We don't use the Settings::self()->path() here as that might have changed
0188         // and in that case it would probably cause data lose.
0189         if (mCurrentUrl.isEmpty()) {
0190             const QString message = i18n("No file specified.");
0191             qWarning() << message;
0192             Q_EMIT status(Broken, message);
0193             if (taskContext) {
0194                 cancelTask();
0195             }
0196             return;
0197         }
0198 
0199         if (mCurrentUrl.isLocalFile()) {
0200             KDirWatch::self()->stopScan();
0201             const bool writeResult = writeToFile(mCurrentUrl.toLocalFile());
0202             // Update the hash so we can detect at fileChanged() if the file actually
0203             // did change.
0204             mCurrentHash = calculateHash(mCurrentUrl.toLocalFile());
0205             saveHash(mCurrentHash);
0206             KDirWatch::self()->startScan();
0207             if (!writeResult) {
0208                 qWarning() << "Error writing to file...";
0209                 if (taskContext) {
0210                     cancelTask();
0211                 }
0212                 return;
0213             }
0214             Q_EMIT status(Idle, i18nc("@info:status", "Ready"));
0215         } else {
0216             // Check if there is a download or an upload in progress.
0217             if (mDownloadJob) {
0218                 const QString message = i18n("A download is still in progress.");
0219                 qWarning() << message;
0220                 Q_EMIT error(message);
0221                 if (taskContext) {
0222                     cancelTask();
0223                 }
0224                 return;
0225             }
0226 
0227             if (mUploadJob) {
0228                 const QString message = i18n("Another file upload is still in progress.");
0229                 qWarning() << message;
0230                 Q_EMIT error(message);
0231                 if (taskContext) {
0232                     cancelTask();
0233                 }
0234                 return;
0235             }
0236 
0237             // Write te items to the locally cached file.
0238             if (!writeToFile(cacheFile())) {
0239                 qWarning() << "Error writing to file";
0240                 if (taskContext) {
0241                     cancelTask();
0242                 }
0243                 return;
0244             }
0245 
0246             // Update the hash so we can detect at fileChanged() if the file actually
0247             // did change.
0248             mCurrentHash = calculateHash(cacheFile());
0249             saveHash(mCurrentHash);
0250 
0251             auto ref = new QEventLoopLocker();
0252             // Start a job to upload the locally cached file to the remote location.
0253             mUploadJob = KIO::file_copy(QUrl::fromLocalFile(cacheFile()), mCurrentUrl, -1, KIO::Overwrite | KIO::DefaultFlags | KIO::HideProgressInfo);
0254             mUploadJob->setProperty("QEventLoopLocker", QVariant::fromValue(ref));
0255             connect(mUploadJob, &KJob::result, this, &SingleFileResource<Settings>::slotUploadJobResult);
0256             connect(mUploadJob, SIGNAL(percent(KJob *, ulong)), SLOT(handleProgress(KJob *, ulong)));
0257 
0258             Q_EMIT status(Running, i18n("Uploading cached file to remote location."));
0259         }
0260         if (taskContext) {
0261             taskDone();
0262         }
0263     }
0264 
0265     void collectionChanged(const Collection &collection) override
0266     {
0267         QString newName;
0268         if (collection.hasAttribute<EntityDisplayAttribute>()) {
0269             const auto attr = collection.attribute<EntityDisplayAttribute>();
0270             newName = attr->displayName();
0271         }
0272         const QString oldName = mSettings->displayName();
0273         if (newName != oldName) {
0274             mSettings->setDisplayName(newName);
0275             mSettings->save();
0276         }
0277         SingleFileResourceBase::collectionChanged(collection);
0278     }
0279 
0280     Collection rootCollection() const override
0281     {
0282         Collection c;
0283         c.setParentCollection(Collection::root());
0284         c.setRemoteId(mSettings->path());
0285         const QString display = mSettings->displayName();
0286         c.setName(display.isEmpty() ? identifier() : display);
0287         c.setContentMimeTypes(mSupportedMimetypes);
0288         if (readOnly()) {
0289             c.setRights(Collection::CanChangeCollection);
0290         } else {
0291             Collection::Rights rights;
0292             rights |= Collection::CanChangeItem;
0293             rights |= Collection::CanCreateItem;
0294             rights |= Collection::CanDeleteItem;
0295             rights |= Collection::CanChangeCollection;
0296             c.setRights(rights);
0297         }
0298         auto attr = c.attribute<EntityDisplayAttribute>(Collection::AddIfMissing);
0299         if (name() != attr->displayName()) {
0300             attr->setDisplayName(name());
0301             new CollectionModifyJob(c);
0302         }
0303         attr->setIconName(mCollectionIcon);
0304 
0305         if (mSettings->periodicUpdate()) {
0306             Akonadi::CachePolicy cachePolicy;
0307             cachePolicy.setInheritFromParent(false);
0308             cachePolicy.setIntervalCheckTime(mSettings->updatePeriod());
0309             c.setCachePolicy(cachePolicy);
0310         }
0311 
0312         return c;
0313     }
0314 
0315 protected:
0316     void retrieveCollections() override
0317     {
0318         Collection::List list;
0319         list << rootCollection();
0320         collectionsRetrieved(list);
0321     }
0322 
0323     bool readOnly() const override
0324     {
0325         return mSettings->readOnly();
0326     }
0327 
0328 protected:
0329     Settings *mSettings = nullptr;
0330 };
0331 }