File indexing completed on 2024-05-12 05:12:40

0001 /*
0002     Copyright (C) 2013  Jonathan Marten <jjm@keelhaul.me.uk>
0003 
0004     This program is free software; you can redistribute it and/or modify
0005     it under the terms of the GNU General Public License as published by
0006     the Free Software Foundation; either version 2 of the License, or
0007     (at your option) any later version.
0008 
0009     This program is distributed in the hope that it will be useful,
0010     but WITHOUT ANY WARRANTY; without even the implied warranty of
0011     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0012     GNU General Public License for more details.
0013 
0014     You should have received a copy of the GNU General Public License along
0015     with this program; if not, write to the Free Software Foundation, Inc.,
0016     51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
0017 */
0018 
0019 #include "copycommand.h"
0020 
0021 #include "collectionresolvejob.h"
0022 #include "collectionpathjob.h"
0023 #include "errorreporter.h"
0024 
0025 #include <Akonadi/CollectionCopyJob>
0026 #include <Akonadi/CollectionMoveJob>
0027 #include <Akonadi/CollectionFetchJob>
0028 #include <Akonadi/CollectionFetchScope>
0029 #include <Akonadi/ItemCopyJob>
0030 #include <Akonadi/ItemMoveJob>
0031 #include <Akonadi/ItemFetchJob>
0032 #include <Akonadi/ItemFetchScope>
0033 
0034 #include <iostream>
0035 
0036 #include "commandfactory.h"
0037 
0038 using namespace Akonadi;
0039 
0040 DEFINE_COMMAND("copy", CopyCommand, I18N_NOOP("Copy collections or items into a new collection"));
0041 
0042 CopyCommand::CopyCommand(QObject *parent)
0043     : AbstractCommand(parent)
0044 {
0045     mMoving = false;
0046 }
0047 
0048 void CopyCommand::start()
0049 {
0050     connect(resolveJob(), &KJob::result, this, &CopyCommand::onTargetFetched);
0051     resolveJob()->start();
0052 }
0053 
0054 void CopyCommand::setupCommandOptions(QCommandLineParser *parser)
0055 {
0056     addOptionsOption(parser);
0057     addDryRunOption(parser);
0058 
0059     parser->addPositionalArgument("source", i18nc("@info:shell", "Existing collections or items to copy"), i18n("source..."));
0060     parser->addPositionalArgument("destination", i18nc("@info:shell", "Destination collection to copy into"));
0061 }
0062 
0063 int CopyCommand::initCommand(QCommandLineParser *parser)
0064 {
0065     if (!getCommonOptions(parser)) return InvalidUsage;
0066 
0067     QStringList sourceArgs = parser->positionalArguments();
0068     if (!checkArgCount(sourceArgs, 2, i18nc("@info:shell", "Missing source/destination arguments"))) return InvalidUsage;
0069 
0070     mDestinationArg = sourceArgs.takeLast();        // extract the destination
0071     Q_ASSERT(!sourceArgs.isEmpty());            // must have some left
0072     if (!getResolveJob(mDestinationArg)) return InvalidUsage;
0073 
0074     initProcessLoop(sourceArgs, i18n("No more sources to process"));
0075     return NoError;
0076 }
0077 
0078 void CopyCommand::onTargetFetched(KJob *job)
0079 {
0080     if (!checkJobResult(job, i18nc("@info:shell", "Cannot resolve destination collection '%1', %2",
0081                                    mDestinationArg, job->errorString()))) return;
0082     CollectionResolveJob *res = resolveJob();
0083     Q_ASSERT(job == res);
0084     mDestinationCollection = res->collection();
0085     Q_ASSERT(mDestinationCollection.isValid());
0086 
0087     startProcessLoop("processNextSource");
0088 }
0089 
0090 void CopyCommand::processNextSource()
0091 {
0092     const QString &sourceArg = currentArg();
0093 
0094     CollectionResolveJob *sourceJob = new CollectionResolveJob(sourceArg, this);
0095     sourceJob->setProperty("arg", sourceArg);
0096     connect(sourceJob, &KJob::result, this, &CopyCommand::onSourceResolved);
0097     sourceJob->start();
0098 }
0099 
0100 void CopyCommand::onSourceResolved(KJob *job)
0101 {
0102     Q_ASSERT(mDestinationCollection.isValid());
0103 
0104     const QString sourceArg = job->property("arg").toString();
0105     if (job->error() != 0) {
0106         Item item;
0107         if (job->error() == Akonadi::Job::Unknown) {    // failed to resolve as collection
0108             // try as item instead
0109             item = CollectionResolveJob::parseItem(sourceArg);
0110         }
0111 
0112         if (!item.isValid()) {              // couldn't parse as item either
0113             emit error(i18nc("@info:shell", "Cannot resolve source '%1', %2",
0114                              sourceArg, job->errorString()));
0115             processNext();
0116             return;
0117         }
0118 
0119         ItemFetchJob *fetchJob = new ItemFetchJob(item, this);
0120         fetchJob->fetchScope().fetchFullPayload(false);
0121         fetchJob->setProperty("arg", sourceArg);
0122         connect(fetchJob, &KJob::result, this, &CopyCommand::onItemsFetched);
0123         return;
0124     }
0125 
0126     CollectionResolveJob *sourceJob = qobject_cast<CollectionResolveJob *>(job);
0127     Q_ASSERT(sourceJob != nullptr);
0128     Akonadi::Collection sourceCollection = sourceJob->collection();
0129 
0130     if (sourceJob->hadSlash()) {
0131         // The source is specified as a collection that ends with a '/'.
0132         // This means that the contents of the source collection
0133         // (both items and sub-collections) are to be copied into
0134         // the destination collection, losing the original collection
0135         // name.  This interpretation is the same as that of the
0136         // source argument to rsync(1).
0137 
0138         if (mMoving) {
0139             ErrorReporter::progress(i18n("Moving contents of %1 -> %2",
0140                                          sourceJob->formattedCollectionName(),
0141                                          resolveJob()->formattedCollectionName()));
0142         } else {
0143             ErrorReporter::progress(i18n("Copying contents of %1 -> %2",
0144                                          sourceJob->formattedCollectionName(),
0145                                          resolveJob()->formattedCollectionName()));
0146         }
0147 
0148         mSourceCollection = sourceJob->collection();
0149         CollectionFetchJob *fetchJob = new CollectionFetchJob(mSourceCollection,
0150                 CollectionFetchJob::FirstLevel, this);
0151         fetchJob->fetchScope().setListFilter(CollectionFetchScope::NoFilter);
0152         fetchJob->setProperty("arg", sourceArg);
0153         connect(fetchJob, &KJob::result, this, &CopyCommand::onCollectionsFetched);
0154     } else {
0155         // The source is a collection that does not end with a '/'.
0156         // This means that the entire source collection is to be copied
0157         // recursively into the destination collection, under the
0158         // original collection name.  This case is simpler!
0159 
0160         Akonadi::Job *copyMovejob;
0161         if (mMoving) {
0162             ErrorReporter::progress(i18n("Moving collection %1 -> %2",
0163                                          sourceJob->formattedCollectionName(),
0164                                          resolveJob()->formattedCollectionName()));
0165             if (!isDryRun()) {
0166                 copyMovejob = new CollectionMoveJob(sourceCollection, mDestinationCollection, this);
0167             }
0168         } else {
0169             ErrorReporter::progress(i18n("Copying collection %1 -> %2",
0170                                          sourceJob->formattedCollectionName(),
0171                                          resolveJob()->formattedCollectionName()));
0172 
0173             if (!isDryRun()) {
0174                 copyMovejob = new CollectionCopyJob(sourceCollection, mDestinationCollection, this);
0175             }
0176         }
0177 
0178         if (!isDryRun()) {
0179             copyMovejob->setProperty("arg", sourceArg);
0180             connect(copyMovejob, &KJob::result, this, &CopyCommand::onRecursiveCopyFinished);
0181         } else {
0182             processNext();
0183         }
0184     }
0185 }
0186 
0187 void CopyCommand::onRecursiveCopyFinished(KJob *job)
0188 {
0189     const QString sourceArg = job->property("arg").toString();
0190     if (!checkJobResult(job, i18nc("@info:shell", "Cannot copy/move from '%1', %2",
0191                                    sourceArg, job->errorString()))) return;
0192     processNext();
0193 }
0194 
0195 void CopyCommand::onCollectionsFetched(KJob *job)
0196 {
0197     const QString sourceArg = job->property("arg").toString();
0198     if (!checkJobResult(job, i18nc("@info:shell", "Cannot fetch subcollections of '%1', %2",
0199                                    sourceArg, job->errorString()))) return;
0200 
0201     CollectionFetchJob *fetchJob = qobject_cast<CollectionFetchJob *>(job);
0202     Q_ASSERT(fetchJob != nullptr);
0203 
0204     mSubCollections = fetchJob->collections();
0205     if (mSubCollections.isEmpty()) {          // no sub-collections, no problem
0206         ErrorReporter::progress(i18n("No sub-collections to copy/move"));
0207         fetchItems(sourceArg);              // go on to do items
0208         return;
0209     }
0210 
0211     if (mMoving) {
0212         ErrorReporter::progress(i18ncp("@info:shell",
0213                                        "Moving %1 sub-collection",
0214                                        "Moving %1 sub-collections",
0215                                        mSubCollections.count()));
0216     } else {
0217         ErrorReporter::progress(i18ncp("@info:shell",
0218                                        "Copying %1 sub-collection",
0219                                        "Copying %1 sub-collections",
0220                                        mSubCollections.count()));
0221     }
0222     doNextSubcollection(sourceArg);           // start processing them
0223 }
0224 
0225 inline void CopyCommand::doNextSubcollection(const QString &sourceArg)
0226 {
0227     QMetaObject::invokeMethod(this, "processNextSubcollection", Qt::QueuedConnection,
0228                               Q_ARG(QString, sourceArg));
0229 }
0230 
0231 void CopyCommand::processNextSubcollection(const QString &sourceArg)
0232 {
0233     if (mSubCollections.isEmpty()) {          // no more to do
0234         ErrorReporter::progress(i18n("No more sub-collections to process"));
0235         fetchItems(sourceArg);
0236         return;
0237     }
0238 
0239     Akonadi::Collection collection = mSubCollections.takeFirst();
0240     Akonadi::Job *job;
0241 
0242     if (mMoving) {
0243         if (!isDryRun()) {
0244             job = new CollectionMoveJob(collection, mDestinationCollection, this);
0245         }
0246     } else {
0247         if (!isDryRun()) {
0248             job = new CollectionCopyJob(collection, mDestinationCollection, this);
0249         }
0250     }
0251 
0252     if (!isDryRun()) {
0253         job->setProperty("arg", sourceArg);
0254         job->setProperty("collection", collection.name());
0255 
0256         connect(job, &KJob::result, this, &CopyCommand::onCollectionCopyFinished);
0257     } else {
0258         doNextSubcollection(sourceArg);
0259     }
0260 }
0261 
0262 void CopyCommand::onCollectionCopyFinished(KJob *job)
0263 {
0264     const QString sourceArg = job->property("arg").toString();
0265     if (job->error() != 0) {
0266         emit error(i18nc("@info:shell", "Cannot copy/move sub-collection '%2' from '%1', %3",
0267                          sourceArg, job->property("collection").toString(), job->errorString()));
0268     }
0269 
0270     doNextSubcollection(sourceArg);           // copy the next one
0271 }
0272 
0273 void CopyCommand::fetchItems(const QString &sourceArg)
0274 {
0275     ItemFetchJob *fetchJob = new ItemFetchJob(mSourceCollection, this);
0276     fetchJob->fetchScope().fetchFullPayload(false);
0277     fetchJob->setProperty("arg", sourceArg);
0278     connect(fetchJob, &KJob::result, this, &CopyCommand::onItemsFetched);
0279 }
0280 
0281 void CopyCommand::onItemsFetched(KJob *job)
0282 {
0283     const QString sourceArg = job->property("arg").toString();
0284     if (!checkJobResult(job, i18nc("@info:shell", "Cannot fetch items of '%1', %2",
0285                                    sourceArg, job->errorString()))) return;
0286 
0287     ItemFetchJob *fetchJob = qobject_cast<ItemFetchJob *>(job);
0288     Q_ASSERT(fetchJob != nullptr);
0289 
0290     Akonadi::Item::List items = fetchJob->items();
0291     if (items.isEmpty()) {                // no items, no problem
0292         ErrorReporter::progress(i18n("No items to process"));
0293         processNext();
0294         return;
0295     }
0296 
0297     Akonadi::Job *copyJob;
0298     if (mMoving) {
0299         ErrorReporter::progress(i18ncp("@info:shell", "Moving %1 item", "Moving %1 items", items.count()));
0300         if (!isDryRun()) {
0301             copyJob = new ItemMoveJob(items, mDestinationCollection, this);
0302         }
0303     } else {
0304         ErrorReporter::progress(i18ncp("@info:shell", "Copying %1 item", "Copying %1 items", items.count()));
0305         if (!isDryRun()) {
0306             copyJob = new ItemCopyJob(items, mDestinationCollection, this);
0307         }
0308     }
0309     if (!isDryRun()) {
0310         copyJob->setProperty("arg", sourceArg);
0311         connect(copyJob, &KJob::result, this, &CopyCommand::onItemCopyFinished);
0312     } else {
0313         processNext();
0314     }
0315 }
0316 
0317 void CopyCommand::onItemCopyFinished(KJob *job)
0318 {
0319     const QString sourceArg = job->property("arg").toString();
0320     if (!checkJobResult(job, i18nc("@info:shell", "Cannot copy/move items from '%1', %2",
0321                                    sourceArg, job->errorString()))) return;
0322     processNext();
0323 }