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 }