File indexing completed on 2024-05-12 05:12:40
0001 /* 0002 Copyright (C) 2012 Kevin Krammer <krammer@kde.org> 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 "addcommand.h" 0020 0021 #include "collectionresolvejob.h" 0022 #include "errorreporter.h" 0023 0024 #include <Akonadi/CollectionCreateJob> 0025 #include <Akonadi/CollectionFetchScope> 0026 #include <Akonadi/CollectionFetchJob> 0027 #include <Akonadi/Item> 0028 #include <Akonadi/ItemCreateJob> 0029 0030 #include <QDir> 0031 #include <QFile> 0032 #include <QFileInfo> 0033 #include <QMimeDatabase> 0034 0035 #include <iostream> 0036 0037 #include "commandfactory.h" 0038 0039 using namespace Akonadi; 0040 0041 DEFINE_COMMAND("add", AddCommand, I18N_NOOP("Add items to a collection")); 0042 0043 AddCommand::AddCommand(QObject *parent) 0044 : AbstractCommand(parent) 0045 { 0046 } 0047 0048 void AddCommand::start() 0049 { 0050 connect(resolveJob(), &KJob::result, this, &AddCommand::onTargetFetched); 0051 resolveJob()->start(); 0052 } 0053 0054 void AddCommand::setupCommandOptions(QCommandLineParser *parser) 0055 { 0056 addOptionsOption(parser); 0057 parser->addOption(QCommandLineOption((QStringList() << "b" << "base"), i18n("Base directory for input files/directories, default is current"), i18n("directory"))); 0058 parser->addOption(QCommandLineOption((QStringList() << "f" << "flat"), i18n("Flat mode, do not duplicate subdirectory structure"))); 0059 parser->addOption(QCommandLineOption((QStringList() << "m" << "mime"), i18n("MIME type of added items, default is to auto-detect"), i18n("mimetype"))); 0060 addDryRunOption(parser); 0061 0062 parser->addPositionalArgument("collection", i18nc("@info:shell", "The collection to add to: an ID, path or Akonadi URL")); 0063 parser->addPositionalArgument("files", i18nc("@info:shell", "Files or directories to add to the collection"), i18n("files...")); 0064 } 0065 0066 int AddCommand::initCommand(QCommandLineParser *parser) 0067 { 0068 const QStringList args = parser->positionalArguments(); 0069 if (!checkArgCount(args, 1, i18nc("@info:shell", "Missing collection argument"))) return InvalidUsage; 0070 if (!checkArgCount(args, 2, i18nc("@info:shell", "No file or directory arguments"))) return InvalidUsage; 0071 0072 if (!getCommonOptions(parser)) return InvalidUsage; 0073 mFlatMode = parser->isSet("flat"); 0074 0075 const QString collectionArg = args.first(); 0076 if (!getResolveJob(collectionArg)) return InvalidUsage; 0077 0078 const QString mimeTypeArg = parser->value("mime"); 0079 if (!mimeTypeArg.isEmpty()) { // MIME type is specified 0080 QMimeDatabase db; 0081 mMimeType = db.mimeTypeForName(mimeTypeArg); 0082 if (!mMimeType.isValid()) { 0083 emit error(i18nc("@info:shell", "Invalid MIME type argument '%1'", mimeTypeArg)); 0084 return InvalidUsage; 0085 } 0086 } 0087 0088 mBasePath = parser->value("base"); 0089 if (!mBasePath.isEmpty()) { // base directory is specified 0090 QDir dir(mBasePath); 0091 if (!dir.exists()) { 0092 emit error(i18nc("@info:shell", "Base directory '%1' not found", mBasePath)); 0093 return InvalidUsage; 0094 } 0095 mBasePath = dir.absolutePath(); 0096 } else { // base is not specified 0097 mBasePath = QDir::currentPath(); 0098 } 0099 0100 const int parsedArgsCount = args.count(); 0101 for (int i = 1; i<parsedArgsCount; ++i) { // process all file/dir arguments 0102 QString path = args.at(i); 0103 while (path.endsWith(QLatin1Char('/'))) { // gives null collection name later 0104 path.chop(1); 0105 } 0106 0107 QFileInfo fileInfo(path); 0108 if (fileInfo.isRelative()) { 0109 fileInfo.setFile(mBasePath, path); 0110 } 0111 0112 if (!fileInfo.exists()) { 0113 emit error(i18n("File '%1' does not exist", path)); 0114 return InvalidUsage; 0115 } else if (!fileInfo.isReadable()) { 0116 emit error(i18n("Error accessing file '%1'", path)); 0117 return InvalidUsage; 0118 } 0119 0120 const QString absolutePath = fileInfo.absoluteFilePath(); 0121 0122 if (fileInfo.isDir()) { 0123 mDirectories[absolutePath] = AddRecursive; 0124 } else { 0125 mDirectories[fileInfo.absolutePath()] = AddDirOnly; 0126 mFiles.insert(absolutePath); 0127 } 0128 0129 if (absolutePath.startsWith(mBasePath)) { 0130 if (fileInfo.isDir()) { 0131 mBasePaths.insert(absolutePath, fileInfo.absoluteFilePath()); 0132 } else { 0133 mBasePaths.insert(absolutePath, fileInfo.absolutePath()); 0134 } 0135 } else { 0136 if (fileInfo.isFile()) { 0137 mBasePaths.insert(fileInfo.absolutePath(), mBasePath); 0138 } 0139 mBasePaths.insert(absolutePath, mBasePath); 0140 } 0141 } 0142 0143 if (mFiles.isEmpty() && mDirectories.isEmpty()) { 0144 emitErrorSeeHelp(i18nc("@info:shell", "No valid file or directory arguments")); 0145 return InvalidUsage; 0146 } 0147 0148 return NoError; 0149 } 0150 0151 void AddCommand::processNextDirectory() 0152 { 0153 if (mDirectories.isEmpty()) { 0154 ErrorReporter::progress(i18n("No more directories to process")); 0155 processNextFile(); 0156 return; 0157 } 0158 0159 const QMap<QString, AddDirectoryMode>::iterator directoriesBegin = mDirectories.begin(); 0160 const QString path = directoriesBegin.key(); 0161 const AddDirectoryMode mode = directoriesBegin.value(); 0162 mDirectories.erase(directoriesBegin); 0163 0164 if (mFlatMode) { 0165 mCollectionsByPath[ path ] = mBaseCollection; 0166 } 0167 0168 if (mCollectionsByPath.value(mBasePaths[ path ]).isValid()) { 0169 if (mode == AddDirOnly) { 0170 // already added 0171 QMetaObject::invokeMethod(this, "processNextDirectory", Qt::QueuedConnection); 0172 return; 0173 } 0174 0175 // exists but needs recursion and items 0176 QDir dir(path); 0177 if (!dir.exists()) { 0178 ErrorReporter::warning(i18n("Directory ‘%1’ no longer exists", path)); 0179 QMetaObject::invokeMethod(this, "processNextDirectory", Qt::QueuedConnection); 0180 return; 0181 } 0182 0183 const QFileInfoList children = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); 0184 Q_FOREACH (const QFileInfo &fileInfo, children) { 0185 if (fileInfo.isDir()) { 0186 mDirectories[ fileInfo.absoluteFilePath() ] = AddRecursive; 0187 mBasePaths[ fileInfo.absoluteFilePath() ] = fileInfo.absoluteFilePath(); 0188 } else { 0189 mFiles.insert(fileInfo.absoluteFilePath()); 0190 mBasePaths[ fileInfo.absoluteFilePath() ] = fileInfo.absolutePath(); 0191 } 0192 } 0193 0194 QMetaObject::invokeMethod(this, "processNextDirectory", Qt::QueuedConnection); 0195 return; 0196 } 0197 0198 QDir dir(path); 0199 if (!dir.exists()) { 0200 ErrorReporter::warning(i18n("Directory ‘%1’ no longer exists", path)); 0201 QMetaObject::invokeMethod(this, "processNextDirectory", Qt::QueuedConnection); 0202 return; 0203 } 0204 0205 // re-examine again later 0206 mDirectories[ path ] = mode; 0207 0208 dir.cdUp(); 0209 0210 const Collection parent = mCollectionsByPath.value(mBasePaths[ dir.absolutePath() ]); 0211 if (parent.isValid()) { 0212 Collection collection; 0213 collection.setName(QFileInfo(path).fileName()); 0214 collection.setParentCollection(parent); // set parent 0215 collection.setContentMimeTypes(parent.contentMimeTypes()); // "inherit" mime types from parent 0216 0217 ErrorReporter::progress(i18n("Fetching collection \"%3\" in parent %1 \"%2\"", 0218 QString::number(parent.id()), parent.name(), collection.name())); 0219 0220 CollectionFetchJob *job = new CollectionFetchJob(parent, CollectionFetchJob::FirstLevel); 0221 job->fetchScope().setListFilter(CollectionFetchScope::NoFilter); 0222 job->setProperty("path", path); 0223 job->setProperty("collection", QVariant::fromValue(collection)); 0224 connect(job, &KJob::result, this, &AddCommand::onCollectionFetched); 0225 return; 0226 } 0227 0228 // parent doesn't exist, generate parent chain creation entries 0229 while (!mCollectionsByPath.value(mBasePaths[ dir.absolutePath() ]).isValid()) { 0230 ErrorReporter::progress(i18n("Need to create collection for '%1'", 0231 QDir(mBasePath).relativeFilePath(dir.absolutePath()))); 0232 mDirectories[ dir.absolutePath() ] = AddDirOnly; 0233 mBasePaths[ dir.absolutePath() ] = dir.absolutePath(); 0234 dir.cdUp(); 0235 } 0236 0237 QMetaObject::invokeMethod(this, "processNextDirectory", Qt::QueuedConnection); 0238 } 0239 0240 void AddCommand::processNextFile() 0241 { 0242 if (mFiles.isEmpty()) { 0243 ErrorReporter::progress(i18n("No more files to process")); 0244 emit finished(NoError); 0245 return; 0246 } 0247 0248 const QSet<QString>::iterator filesBegin = mFiles.begin(); 0249 const QString fileName = *filesBegin; 0250 mFiles.erase(filesBegin); 0251 0252 QFile file(fileName); 0253 if (!file.exists()) { 0254 emit error(i18nc("@info:shell", "File ‘%1’ does not exist", fileName)); 0255 QMetaObject::invokeMethod(this, "processNextFile", Qt::QueuedConnection); 0256 return; 0257 } 0258 0259 if (!file.open(QIODevice::ReadOnly)) { 0260 emit error(i18nc("@info:shell", "File ‘%1’ cannot be read", fileName)); 0261 QMetaObject::invokeMethod(this, "processNextFile", Qt::QueuedConnection); 0262 return; 0263 } 0264 0265 QMimeDatabase db; 0266 QMimeType mimeType = mMimeType.isValid() ? mMimeType : db.mimeTypeForFileNameAndData(fileName, &file); 0267 if (!mimeType.isValid()) { 0268 emit error(i18nc("@info:shell", "Cannot determine MIME type of file ‘%1’", fileName)); 0269 QMetaObject::invokeMethod(this, "processNextFile", Qt::QueuedConnection); 0270 return; 0271 } 0272 0273 const QFileInfo fileInfo(fileName); 0274 0275 const Collection parent = mCollectionsByPath.value(mBasePaths[ fileInfo.absolutePath() ]); 0276 if (!parent.isValid()) { 0277 emit error(i18nc("@info:shell", "Cannot determine parent collection for file ‘%1’", 0278 QDir(mBasePath).relativeFilePath(fileName))); 0279 QMetaObject::invokeMethod(this, "processNextFile", Qt::QueuedConnection); 0280 return; 0281 } 0282 0283 const QString size = QLocale::system().formattedDataSize(fileInfo.size()); 0284 ErrorReporter::progress(i18n("Creating item in collection %1 \"%2\" from '%3' size %4", 0285 QString::number(parent.id()), parent.name(), 0286 QDir(mBasePath).relativeFilePath(fileName), 0287 size)); 0288 Item item; 0289 item.setMimeType(mimeType.name()); 0290 0291 file.reset(); 0292 item.setPayloadFromData(file.readAll()); 0293 0294 if (!isDryRun()) { 0295 ItemCreateJob *job = new ItemCreateJob(item, parent); 0296 job->setProperty("fileName", fileName); 0297 connect(job, &KJob::result, this, &AddCommand::onItemCreated); 0298 } else { 0299 processNextFile(); 0300 } 0301 } 0302 0303 void AddCommand::onTargetFetched(KJob *job) 0304 { 0305 if (!checkJobResult(job, i18nc("@info:shell", 0306 "Cannot fetch target collection, %1", 0307 job->errorString()))) return; 0308 CollectionResolveJob *res = resolveJob(); 0309 Q_ASSERT(job == res); 0310 0311 mBaseCollection = res->collection(); 0312 Q_ASSERT(mBaseCollection.isValid()); 0313 mCollectionsByPath[ mBasePath ] = mBaseCollection; 0314 mBasePaths[ mBasePath ] = mBasePath; 0315 0316 ErrorReporter::progress(i18n("Root folder is %1 \"%2\"", 0317 QString::number(mBaseCollection.id()), mBaseCollection.name())); 0318 processNextDirectory(); 0319 } 0320 0321 void AddCommand::onCollectionCreated(KJob *job) 0322 { 0323 const QString path = job->property("path").toString(); 0324 Q_ASSERT(!path.isEmpty()); 0325 0326 if (job->error() != 0) { 0327 qWarning() << "error=" << job->error() << "errorString=" << job->errorString(); 0328 0329 mDirectories.remove(path); 0330 } else { 0331 CollectionCreateJob *createJob = qobject_cast<CollectionCreateJob *>(job); 0332 Q_ASSERT(createJob != nullptr); 0333 0334 QFileInfo fileInfo(path); 0335 mBasePaths[ path ] = fileInfo.absoluteFilePath(); 0336 0337 mCollectionsByPath[ path ] = createJob->collection(); 0338 } 0339 0340 QMetaObject::invokeMethod(this, "processNextDirectory", Qt::QueuedConnection); 0341 } 0342 0343 void AddCommand::onCollectionFetched(KJob *job) 0344 { 0345 const QString path = job->property("path").toString(); 0346 Q_ASSERT(!path.isEmpty()); 0347 0348 Akonadi::Collection newCollection = job->property("collection").value<Collection>(); 0349 0350 CollectionFetchJob *fetchJob = qobject_cast<CollectionFetchJob *>(job); 0351 Q_ASSERT(fetchJob != nullptr); 0352 0353 bool found = false; 0354 Collection::List collections = fetchJob->collections(); 0355 Q_FOREACH (const Collection &col, collections) { 0356 if (col.name() == newCollection.name()) { 0357 found = true; 0358 newCollection = col; 0359 } 0360 } 0361 0362 if (!found) { 0363 if (mFlatMode) { // not creating any collections 0364 ErrorReporter::error(i18n("Error fetching collection %1 \"%2\", %3", 0365 QString::number(newCollection.id()), newCollection.name(), 0366 job->errorString())); 0367 QMetaObject::invokeMethod(this, "processNextDirectory", Qt::QueuedConnection); 0368 return; 0369 } 0370 0371 // no such collection, try creating it 0372 QString name = newCollection.name(); 0373 // Workaround for bug 319513 0374 if ((name == "cur") || (name == "new") || (name == "tmp")) { 0375 QString parentResource = newCollection.parentCollection().resource(); 0376 if (parentResource.startsWith(QLatin1String("akonadi_maildir_resource"))) { 0377 name += "_"; 0378 newCollection.setName(name); 0379 ErrorReporter::warning(i18n("Changed maildir folder name to '%1'", name)); 0380 } 0381 } 0382 0383 if (!isDryRun()) { 0384 CollectionCreateJob *createJob = new CollectionCreateJob(newCollection); 0385 createJob->setProperty("path", path); 0386 0387 Akonadi::Collection parent = newCollection.parentCollection(); 0388 ErrorReporter::progress(i18n("Creating collection \"%3\" under parent %1 \"%2\"", 0389 QString::number(parent.id()), parent.name(), 0390 newCollection.name())); 0391 0392 connect(createJob, &KJob::result, this, &AddCommand::onCollectionCreated); 0393 } else { 0394 QMetaObject::invokeMethod(this, "processNextDirectory", Qt::QueuedConnection); 0395 } 0396 return; 0397 } 0398 0399 mCollectionsByPath[ path ] = newCollection; 0400 0401 QMetaObject::invokeMethod(this, "processNextDirectory", Qt::QueuedConnection); 0402 } 0403 0404 void AddCommand::onItemCreated(KJob *job) 0405 { 0406 const QString fileName = job->property("fileName").toString(); 0407 0408 if (checkJobResult(job, i18nc("@info:shell", 0409 "Failed to add ‘%1’, %2", 0410 fileName, job->errorString()))) 0411 { 0412 ItemCreateJob *createJob = qobject_cast<ItemCreateJob *>(job); 0413 Q_ASSERT(createJob != nullptr); 0414 0415 ErrorReporter::progress(i18n("Added file '%2' as item %1", 0416 QString::number(createJob->item().id()), 0417 QDir(mBasePath).relativeFilePath(fileName))); 0418 } 0419 0420 processNextFile(); 0421 }