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: