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 }