File indexing completed on 2024-12-22 04:55:36
0001 /* 0002 * akonadiresourcemigrator.cpp - migrates KAlarm Akonadi resources 0003 * Program: kalarm 0004 * SPDX-FileCopyrightText: 2011-2022 David Jarvie <djarvie@kde.org> 0005 * 0006 * SPDX-License-Identifier: GPL-2.0-or-later 0007 */ 0008 0009 #include "akonadiresourcemigrator.h" 0010 0011 #include "collectionattribute.h" 0012 #include "akonadiplugin_debug.h" 0013 0014 #include <Akonadi/AgentManager> 0015 #include <Akonadi/AttributeFactory> 0016 #include <Akonadi/CollectionFetchJob> 0017 #include <Akonadi/CollectionFetchScope> 0018 #include <Akonadi/CollectionModifyJob> 0019 0020 using namespace KAlarmCal; 0021 0022 //clazy:excludeall=non-pod-global-static 0023 0024 namespace 0025 { 0026 const QString KALARM_RESOURCE(QStringLiteral("akonadi_kalarm_resource")); 0027 const QString KALARM_DIR_RESOURCE(QStringLiteral("akonadi_kalarm_dir_resource")); 0028 0029 // Holds an Akonadi collection's properties. 0030 struct CollectionProperties 0031 { 0032 QColor backgroundColour; 0033 CalEvent::Types alarmTypes; 0034 CalEvent::Types enabledTypes {CalEvent::EMPTY}; 0035 CalEvent::Types standardTypes {CalEvent::EMPTY}; 0036 bool readOnly; 0037 0038 // Fetch the properties of a collection which has been fetched by CollectionFetchJob. 0039 explicit CollectionProperties(const Akonadi::Collection&); 0040 }; 0041 0042 const Akonadi::Collection::Rights WritableRights = Akonadi::Collection::CanChangeItem | Akonadi::Collection::CanCreateItem | Akonadi::Collection::CanDeleteItem; 0043 } 0044 0045 AkonadiResourceMigrator* AkonadiResourceMigrator::mInstance = nullptr; 0046 bool AkonadiResourceMigrator::mCompleted = false; 0047 0048 /****************************************************************************** 0049 * Constructor. 0050 */ 0051 AkonadiResourceMigrator::AkonadiResourceMigrator(QObject* parent) 0052 : QObject(parent) 0053 { 0054 } 0055 0056 AkonadiResourceMigrator::~AkonadiResourceMigrator() 0057 { 0058 qCDebug(AKONADIPLUGIN_LOG) << "~AkonadiResourceMigrator"; 0059 mInstance = nullptr; 0060 mCompleted = true; 0061 } 0062 0063 /****************************************************************************** 0064 * Create and return the unique AkonadiResourceMigrator instance. 0065 */ 0066 AkonadiResourceMigrator* AkonadiResourceMigrator::instance() 0067 { 0068 if (!mInstance && !mCompleted) 0069 mInstance = new AkonadiResourceMigrator; 0070 return mInstance; 0071 } 0072 0073 /****************************************************************************** 0074 * Initiate migration of old Akonadi calendars, and create default file system 0075 * resources. 0076 */ 0077 void AkonadiResourceMigrator::initiateMigration() 0078 { 0079 connect(Akonadi::ServerManager::self(), &Akonadi::ServerManager::stateChanged, this, &AkonadiResourceMigrator::checkServer); 0080 auto akstate = Akonadi::ServerManager::state(); 0081 mAkonadiStarted = (akstate == Akonadi::ServerManager::NotRunning); 0082 checkServer(akstate); 0083 } 0084 0085 /****************************************************************************** 0086 * Called when the Akonadi server manager changes state. 0087 * Once it is running, migrate any Akonadi KAlarm resources. 0088 */ 0089 void AkonadiResourceMigrator::checkServer(Akonadi::ServerManager::State state) 0090 { 0091 switch (state) 0092 { 0093 case Akonadi::ServerManager::Running: 0094 migrateResources(); 0095 break; 0096 0097 case Akonadi::ServerManager::Stopping: 0098 // Wait until the server has stopped, so that we can restart it. 0099 return; 0100 0101 default: 0102 if (Akonadi::ServerManager::start()) 0103 return; // wait for the server to change to Running state 0104 0105 // Can't start Akonadi, so give up trying to migrate. 0106 qCWarning(AKONADIPLUGIN_LOG) << "AkonadiResourceMigrator::checkServer: Failed to start Akonadi server"; 0107 terminate(false); 0108 break; 0109 } 0110 0111 disconnect(Akonadi::ServerManager::self(), nullptr, this, nullptr); 0112 } 0113 0114 /****************************************************************************** 0115 * Initiate migration of Akonadi KAlarm resources. 0116 * Reply = true if migration initiated; 0117 * = false if no KAlarm Akonadi resources found. 0118 */ 0119 void AkonadiResourceMigrator::migrateResources() 0120 { 0121 qCDebug(AKONADIPLUGIN_LOG) << "AkonadiResourceMigrator::migrateResources: initiated"; 0122 mCollectionPaths.clear(); 0123 mFetchesPending.clear(); 0124 Akonadi::AttributeFactory::registerAttribute<CollectionAttribute>(); 0125 0126 // Create jobs to fetch all KAlarm Akonadi collections. 0127 bool migrating = false; 0128 const Akonadi::AgentInstance::List agents = Akonadi::AgentManager::self()->instances(); 0129 for (const Akonadi::AgentInstance& agent : agents) 0130 { 0131 const QString type = agent.type().identifier(); 0132 if (type == KALARM_RESOURCE || type == KALARM_DIR_RESOURCE) 0133 { 0134 Akonadi::CollectionFetchJob* job = new Akonadi::CollectionFetchJob(Akonadi::Collection::root(), Akonadi::CollectionFetchJob::FirstLevel); 0135 job->fetchScope().setResource(agent.identifier()); 0136 mFetchesPending[job] = (type == KALARM_DIR_RESOURCE); 0137 connect(job, &KJob::result, this, &AkonadiResourceMigrator::collectionFetchResult); 0138 migrating = true; 0139 } 0140 } 0141 if (!migrating) 0142 terminate(false); // there are no Akonadi resources to migrate 0143 } 0144 0145 /****************************************************************************** 0146 * Called when an Akonadi collection fetch job has completed. 0147 * Check for, and remove, any Akonadi resources which duplicate use of calendar 0148 * files/directories. 0149 */ 0150 void AkonadiResourceMigrator::collectionFetchResult(KJob* j) 0151 { 0152 auto job = qobject_cast<Akonadi::CollectionFetchJob*>(j); 0153 const QString id = job->fetchScope().resource(); 0154 if (j->error()) 0155 qCCritical(AKONADIPLUGIN_LOG) << "AkonadiResourceMigrator::collectionFetchResult: CollectionFetchJob" << id << "error: " << j->errorString(); 0156 else 0157 { 0158 const Akonadi::Collection::List collections = job->collections(); 0159 if (collections.isEmpty()) 0160 qCCritical(AKONADIPLUGIN_LOG) << "AkonadiResourceMigrator::collectionFetchResult: No collections found for resource" << id; 0161 else 0162 { 0163 // Note that a KAlarm Akonadi agent contains only one collection. 0164 const Akonadi::Collection& collection(collections[0]); 0165 const bool dirType = mFetchesPending.value(job, false); 0166 const AkResourceData thisRes(job->fetchScope().resource(), collection, dirType); 0167 bool saveThis = true; 0168 auto it = mCollectionPaths.constFind(collection.remoteId()); 0169 if (it != mCollectionPaths.constEnd()) 0170 { 0171 // Remove the resource which, in decreasing order of priority: 0172 // - Is disabled; 0173 // - Is not a standard resource; 0174 // - Contains the higher numbered Collection ID, which is likely 0175 // to be the more recently created. 0176 const AkResourceData prevRes = it.value(); 0177 const CollectionProperties properties[2] = { CollectionProperties(prevRes.collection), 0178 CollectionProperties(thisRes.collection) }; 0179 int propToUse = (thisRes.collection.id() < prevRes.collection.id()) ? 1 : 0; 0180 if (properties[1 - propToUse].standardTypes && !properties[propToUse].standardTypes) 0181 propToUse = 1 - propToUse; 0182 if (properties[1 - propToUse].enabledTypes && !properties[propToUse].enabledTypes) 0183 propToUse = 1 - propToUse; 0184 saveThis = (propToUse == 1); 0185 0186 const auto resourceToRemove = saveThis ? prevRes.resourceId : thisRes.resourceId; 0187 qCWarning(AKONADIPLUGIN_LOG) << "AkonadiResourceMigrator::collectionFetchResult: Removing duplicate resource" << resourceToRemove; 0188 Akonadi::AgentManager* agentManager = Akonadi::AgentManager::self(); 0189 agentManager->removeInstance(agentManager->instance(resourceToRemove)); 0190 } 0191 if (saveThis) 0192 mCollectionPaths[collection.remoteId()] = thisRes; 0193 } 0194 } 0195 mFetchesPending.remove(job); 0196 if (mFetchesPending.isEmpty()) 0197 { 0198 // De-duplication is complete. Migrate the remaining Akonadi resources. 0199 doMigrateResources(); 0200 } 0201 } 0202 0203 /****************************************************************************** 0204 * Migrate Akonadi KAlarm resources to file system resources. 0205 */ 0206 void AkonadiResourceMigrator::doMigrateResources() 0207 { 0208 qCDebug(AKONADIPLUGIN_LOG) << "AkonadiResourceMigrator::doMigrateResources"; 0209 0210 // First, migrate KAlarm calendar file Akonadi resources. 0211 // This will allow any KAlarm directory resources to be merged into 0212 // single file resources, if the user prefers that. 0213 for (auto it = mCollectionPaths.constBegin(); it != mCollectionPaths.constEnd(); ++it) 0214 { 0215 const AkResourceData& resourceData = it.value(); 0216 if (!resourceData.dirType) 0217 migrateCollection(resourceData.collection, false); 0218 } 0219 0220 // Now migrate KAlarm directory Akonadi resources, which must be merged 0221 // or converted into single file resources. 0222 for (auto it = mCollectionPaths.constBegin(); it != mCollectionPaths.constEnd(); ++it) 0223 { 0224 const AkResourceData& resourceData = it.value(); 0225 if (resourceData.dirType) 0226 migrateCollection(resourceData.collection, true); 0227 } 0228 0229 // The alarm types of all collections have been found. 0230 mCollectionPaths.clear(); 0231 terminate(true); 0232 } 0233 0234 /****************************************************************************** 0235 * Migrate one Akonadi collection to a file system resource. 0236 */ 0237 void AkonadiResourceMigrator::migrateCollection(const Akonadi::Collection& collection, bool dirType) 0238 { 0239 // Fetch the collection's properties. 0240 const CollectionProperties colProperties(collection); 0241 0242 if (!dirType) 0243 { 0244 // It's a single file resource. 0245 qCDebug(AKONADIPLUGIN_LOG) << "AkonadiResourceMigrator: Migrate file resource" << collection.displayName() << ", alarm types:" << (int)colProperties.alarmTypes << ", enabled types:" << (int)colProperties.enabledTypes << ", standard types:" << (int)colProperties.standardTypes; 0246 Q_EMIT fileResource(collection.resource(), 0247 QUrl::fromUserInput(collection.remoteId(), QString(), QUrl::AssumeLocalFile), 0248 colProperties.alarmTypes, collection.displayName(), colProperties.backgroundColour, 0249 colProperties.enabledTypes, colProperties.standardTypes, colProperties.readOnly); 0250 } 0251 else 0252 { 0253 // Convert Akonadi directory resource to single file resources. 0254 qCDebug(AKONADIPLUGIN_LOG) << "AkonadiResourceMigrator: Migrate directory resource" << collection.displayName() << ", alarm types:" << (int)colProperties.alarmTypes << ", enabled types:" << (int)colProperties.enabledTypes; 0255 Q_EMIT dirResource(collection.resource(), collection.remoteId(), 0256 colProperties.alarmTypes, collection.displayName(), colProperties.backgroundColour, 0257 colProperties.enabledTypes, colProperties.standardTypes, colProperties.readOnly); 0258 } 0259 } 0260 0261 /****************************************************************************** 0262 * Delete an Akonadi resource after it has been migrated to a file system resource. 0263 */ 0264 void AkonadiResourceMigrator::deleteAkonadiResource(const QString& resourceName) 0265 { 0266 // Delete the Akonadi resource, to prevent it using CPU, on the 0267 // assumption that Akonadi access won't be needed by any other 0268 // application. Excess CPU usage is one of the major bugs which 0269 // prompted replacing Akonadi resources with file resources. 0270 Akonadi::AgentManager* agentManager = Akonadi::AgentManager::self(); 0271 const Akonadi::AgentInstance agent = agentManager->instance(resourceName); 0272 agentManager->removeInstance(agent); 0273 } 0274 0275 /****************************************************************************** 0276 * Called when Akonadi migration is complete or is known not to be possible. 0277 */ 0278 void AkonadiResourceMigrator::terminate(bool migrated) 0279 { 0280 qCDebug(AKONADIPLUGIN_LOG) << "AkonadiResourceMigrator::terminate" << migrated; 0281 0282 Q_EMIT migrationComplete(migrated); 0283 0284 // Ignore any further Akonadi server state changes, to prevent possible 0285 // repeated migrations. 0286 disconnect(Akonadi::ServerManager::self(), nullptr, this, nullptr); 0287 0288 if (mAkonadiStarted) 0289 { 0290 // The Akonadi server wasn't running before we started it, so stop it 0291 // now that it's no longer needed. 0292 Akonadi::ServerManager::stop(); 0293 } 0294 0295 deleteLater(); 0296 } 0297 0298 namespace 0299 { 0300 0301 /****************************************************************************** 0302 * Fetch an Akonadi collection's properties. 0303 */ 0304 CollectionProperties::CollectionProperties(const Akonadi::Collection& collection) 0305 { 0306 readOnly = (collection.rights() & WritableRights) != WritableRights; 0307 alarmTypes = CalEvent::types(collection.contentMimeTypes()); 0308 if (collection.hasAttribute<CollectionAttribute>()) 0309 { 0310 const auto* attr = collection.attribute<CollectionAttribute>(); 0311 enabledTypes = attr->enabled() & alarmTypes; 0312 standardTypes = attr->standard() & enabledTypes; 0313 backgroundColour = attr->backgroundColor(); 0314 } 0315 } 0316 0317 } 0318 0319 #include "moc_akonadiresourcemigrator.cpp" 0320 0321 // vim: et sw=4: