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 }