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

0001 /*
0002     Copyright (C) 2014-2021  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 "tagscommand.h"
0020 
0021 #include <iostream>
0022 
0023 #include <Akonadi/TagFetchJob>
0024 #include <Akonadi/TagCreateJob>
0025 #include <Akonadi/TagDeleteJob>
0026 
0027 #include "commandfactory.h"
0028 
0029 using namespace Akonadi;
0030 
0031 DEFINE_COMMAND("tags", TagsCommand, I18N_NOOP("List or modify tags"));
0032 
0033 TagsCommand::TagsCommand(QObject *parent)
0034     : AbstractCommand(parent),
0035       mBriefOutput(false),
0036       mUrlsOutput(false),
0037       mOperationMode(ModeList),
0038       mAddForceId(0)
0039 {
0040 }
0041 
0042 void TagsCommand::setupCommandOptions(QCommandLineParser *parser)
0043 {
0044     addOptionsOption(parser);
0045     parser->addOption(QCommandLineOption((QStringList() << "b" << "brief"), i18n("Brief output, tag names or IDs only")));
0046     parser->addOption(QCommandLineOption((QStringList() << "u" << "urls"), i18n("Brief output, tag URLs only")));
0047 
0048     parser->addOption(QCommandLineOption((QStringList() << "l" << "list"), i18n("List all known tags (the default operation)")));
0049     parser->addOption(QCommandLineOption((QStringList() << "a" << "add"), i18n("Add named tags")));
0050     parser->addOption(QCommandLineOption((QStringList() << "d" << "delete"), i18n("Delete tags")));
0051     // This option would be nice to have but will not work, an ID is
0052     // automatically assigned by the Akonadi server when a tag is created.
0053     //parser->addOption(QCommandLineOption((QStringList() << "i" << "id"), i18n("ID for a tag to be added (default automatic)"), i18n("id")));
0054 
0055     parser->addPositionalArgument(i18n("TAG"), i18n("The name of a tag to add, or the name, ID or URL of a tag to delete"), i18n("[TAG...]"));
0056 }
0057 
0058 int TagsCommand::initCommand(QCommandLineParser *parser)
0059 {
0060     mBriefOutput = parser->isSet("brief");
0061     mUrlsOutput = parser->isSet("urls");
0062 
0063     //if (parser->isSet("id"))
0064     //{
0065     //    bool ok;
0066     //    mAddForceId = parser->value("id").toUInt(&ok);
0067     //    if (!ok || mAddForceId==0)
0068     //    {
0069     //        emitErrorSeeHelp(i18nc("@info:shell", "Invalid value for the 'id' option"));
0070     //        return (InvalidUsage);
0071     //    }
0072     //}
0073 
0074     if (mBriefOutput && mUrlsOutput) {
0075         emitErrorSeeHelp(i18nc("@info:shell", "The 'brief' and 'urls' options cannot both be specified"));
0076         return InvalidUsage;
0077     }
0078 
0079     int modeCount = 0;
0080     if (parser->isSet("list")) {
0081         ++modeCount;
0082     }
0083     if (parser->isSet("add")) {
0084         ++modeCount;
0085     }
0086     if (parser->isSet("delete")) {
0087         ++modeCount;
0088     }
0089     if (modeCount > 1) {
0090         emitErrorSeeHelp(i18nc("@info:shell", "Only one of the 'list', 'add' or 'delete' options may be specified"));
0091         return (InvalidUsage);
0092     }
0093 
0094     const QStringList tagArgs = parser->positionalArguments();
0095 
0096     if (parser->isSet("list")) {            // see if "List" mode
0097         // expand
0098         mOperationMode = ModeList;
0099     } else if (parser->isSet("add")) {          // see if "Add" mode
0100         // add [-i ID] NAME
0101         // add NAME...
0102         mOperationMode = ModeAdd;
0103 
0104         if (tagArgs.isEmpty())
0105         {
0106             emitErrorSeeHelp(i18nc("@info:shell", "No tags specified to add"));
0107             return (InvalidUsage);
0108         }
0109 
0110         if (tagArgs.count()>1 && mAddForceId!=0)
0111         {
0112             emitErrorSeeHelp(i18nc("@info:shell", "Multiple tags cannot be specified to add with 'id'"));
0113             return (InvalidUsage);
0114         }
0115     }
0116     else if (parser->isSet("delete"))           // see if "Delete" mode
0117     {
0118         // delete NAME|ID|URL...
0119         mOperationMode = ModeDelete;
0120 
0121         if (tagArgs.isEmpty())
0122         {
0123             emitErrorSeeHelp(i18nc("@info:shell", "No tags specified to delete"));
0124             return (InvalidUsage);
0125         }
0126     }
0127 
0128     initProcessLoop(tagArgs);
0129     return NoError;
0130 }
0131 
0132 void TagsCommand::start()
0133 {
0134     if (mOperationMode == ModeDelete) {
0135         if (!isDryRun()) {              // allow if not doing anything
0136             if (!allowDangerousOperation()) {
0137                 emit finished(RuntimeError);
0138                 return;
0139             }
0140         }
0141     }
0142 
0143     TagFetchJob *job = new TagFetchJob(this);       // always need tag list
0144     connect(job, &KJob::result, this, &TagsCommand::onTagsFetched);
0145 }
0146 
0147 void TagsCommand::addNextTag()
0148 {
0149     // See whether a tag with that name or ID already exists
0150     for (const Tag &tag : mFetchedTags)
0151     {
0152         if (tag.name()==currentArg() || (mAddForceId!=0 && tag.id()==mAddForceId))
0153         {
0154             emit error(i18nc("@info:shell", "A tag named '%1' ID %2 already exists",
0155                              tag.name(), QString::number(tag.id())));
0156             processNext();              // ignore the conflicting tag
0157             return;
0158         }
0159     }
0160 
0161     Tag newTag(currentArg());
0162     if (mAddForceId!=0) newTag.setId(mAddForceId);
0163     TagCreateJob *createJob = new TagCreateJob(newTag, this);
0164     connect(createJob, &KJob::result, this, &TagsCommand::onTagAdded);
0165 }
0166 
0167 void TagsCommand::onTagAdded(KJob *job)
0168 {
0169     if (!checkJobResult(job)) return;
0170     TagCreateJob *createJob = qobject_cast<TagCreateJob *>(job);
0171     Q_ASSERT(createJob != nullptr);
0172 
0173     Tag addedTag = createJob->tag();
0174     if (!addedTag.isValid())
0175     {
0176         if (mAddForceId!=0)
0177         {
0178             emit error(i18nc("@info:shell", "Cannot add tag '%1' ID %2",
0179                              currentArg(), QString::number(mAddForceId)));
0180         }
0181         else
0182         {
0183             emit error(i18nc("@info:shell", "Cannot add tag '%1'", currentArg()));
0184         }
0185     }
0186     else
0187     {
0188         if (mBriefOutput) std::cout << addedTag.id() << std::endl;
0189         else if (mUrlsOutput) std::cout << qPrintable(addedTag.url().toDisplayString()) << std::endl;
0190         else std::cout << "Added tag '" << qPrintable(addedTag.name()) << "' ID " << addedTag.id() << std::endl;
0191     }
0192 
0193     processNext();                  // continue to do next
0194 }
0195 
0196 void TagsCommand::deleteNextTag()
0197 {
0198     Tag delTag;
0199     // See if user input is a valid integer as a tag ID
0200     bool ok;
0201     unsigned int id = currentArg().toUInt(&ok);
0202     if (ok) delTag = Tag(id);               // conversion succeeded
0203     else
0204     {
0205         // Otherwise check if we have an Akonadi URL
0206         const QUrl url = QUrl::fromUserInput(currentArg());
0207         if (url.isValid() && url.scheme() == QLatin1String("akonadi"))
0208         {                       // valid Akonadi URL
0209             delTag = Tag::fromUrl(url);
0210         }
0211         else
0212         {
0213             // Otherwise assume a tag name.  Unfortunately this means that
0214             // a tag with a name that looks like an integer or a URL cannot be
0215             // deleted.  This is not very likely.
0216             //
0217             // A tag can only be deleted by ID, so find the corresponding
0218             // tag with that name in the fetched list and use its ID.
0219             for (const Tag &tag : mFetchedTags)
0220             {
0221                 if (tag.name() == currentArg())
0222                 {
0223                     delTag = Tag(tag.id());
0224                     break;
0225                 }
0226             }
0227 
0228             // Check now whether the named tag currently exists.
0229             if (delTag.id()==-1)            // no tag found by loop above
0230             {
0231                 emit error(i18nc("@info:shell", "Tag to delete '%1' does not exist", currentArg()));
0232                 processNext();              // ignore the missing tag
0233                 return;
0234             }
0235         }
0236     }
0237 
0238     // Need to verify that a tag with the specified ID currently exists
0239     // in the Akonadi database.  Otherwise attempting to delete a tag
0240     // with a nonexistent ID crashes the server with the assert:
0241     //
0242     //   qt_assert_x(where="QueryBuilder::buildWhereCondition()", what="No values given for IN condition.",
0243     //               file="akonadi/src/server/storage/querybuilder.cpp"
0244     //   Akonadi::Server::QueryBuilder::buildWhereCondition() at akonadi/src/server/storage/querybuilder.cpp:536
0245     //   Akonadi::Server::QueryBuilder::buildWhereCondition() at akonadi/src/server/storage/querybuilder.cpp:522
0246     //   Akonadi::Server::QueryBuilder::buildQuery() at akonadi/src/server/storage/querybuilder.cpp:319
0247     //   Akonadi::Server::QueryBuilder::exec() at akonadi/src/server/storage/querybuilder.cpp:366
0248     //   Akonadi::Server::DataStore::removeTags() at akonadi/src/server/storage/datastore.cpp:670
0249     //   Akonadi::Server::TagDeleteHandler::parseStream() at akonadi/src/server/handler/tagdeletehandler.cpp:40
0250     //   Akonadi::Server::Connection::parseStream() at akonadi/src/server/connection.cpp:162
0251     //
0252     // Look for a matching tag by ID, if a name has been specified then
0253     // it will have been resolved to an existing ID above.
0254     Q_ASSERT(delTag.isValid());
0255     for (const Tag &tag : mFetchedTags)
0256     {
0257         if (delTag.id()==tag.id())          // tag specified by ID
0258         {
0259             // It is now safe to delete the tag.  Use the fetched tag,
0260             // since that will have both its ID and name available to
0261             // be reported later.
0262             TagDeleteJob *deleteJob = new TagDeleteJob(tag, this);
0263             connect(deleteJob, &KJob::result, this, &TagsCommand::onTagDeleted);
0264             return;
0265         }
0266     }
0267 
0268     emit error(i18nc("@info:shell", "Tag to delete ID %1 does not exist", QString::number(delTag.id())));
0269     processNext();                  // ignore the missing tag
0270 }
0271 
0272 void TagsCommand::onTagDeleted(KJob *job)
0273 {
0274     if (!checkJobResult(job)) return;
0275     TagDeleteJob *deleteJob = qobject_cast<TagDeleteJob *>(job);
0276     Q_ASSERT(deleteJob != nullptr);
0277 
0278     Tag deletedTag = deleteJob->tags().first();     // must be one and only one
0279     if (mBriefOutput) std::cout << deletedTag.id() << std::endl;
0280     else if (mUrlsOutput) std::cout << qPrintable(deletedTag.url().toDisplayString()) << std::endl;
0281     else std::cout << "Deleted tag '" << qPrintable(deletedTag.name()) << "' ID " << deletedTag.id() << std::endl;
0282     processNext();                  // continue to do next
0283 }
0284 
0285 static void writeColumn(const QString &data, int width = 0)
0286 {
0287     std::cout << qPrintable(data.leftJustified(width)) << "  ";
0288 }
0289 
0290 static void writeColumn(quint64 data, int width = 0)
0291 {
0292     writeColumn(QString::number(data), width);
0293 }
0294 
0295 void TagsCommand::onTagsFetched(KJob *job)
0296 {
0297     if (!checkJobResult(job)) return;
0298     TagFetchJob *fetchJob = qobject_cast<TagFetchJob *>(job);
0299     Q_ASSERT(fetchJob != nullptr);
0300     mFetchedTags = fetchJob->tags();
0301 
0302     // Now that the current tag list has been fetched,
0303     // look at what to do.
0304     if (mOperationMode == ModeList)
0305     {
0306         if (mFetchedTags.isEmpty())
0307         {
0308             emit error(i18nc("@info:shell", "No tags found"));
0309             emit finished(NoError);
0310         }
0311         else listTags();
0312     }
0313     else if (mOperationMode == ModeAdd) startProcessLoop("addNextTag");
0314     else if (mOperationMode == ModeDelete) startProcessLoop("deleteNextTag");
0315 }
0316 
0317 void TagsCommand::listTags()
0318 {
0319     if (!mBriefOutput && !mUrlsOutput) {
0320         writeColumn(i18nc("@info:shell column header", "ID"), 8);
0321         writeColumn(i18nc("@info:shell column header", "URL"), 25);
0322         writeColumn(i18nc("@info:shell column header", "Name"));
0323         std::cout << std::endl;
0324     }
0325 
0326     for (const Tag &tag : qAsConst(mFetchedTags))
0327     {
0328         if (!mBriefOutput && !mUrlsOutput) {
0329             writeColumn(tag.id(), 8);
0330         }
0331         if (!mBriefOutput || mUrlsOutput) {
0332             writeColumn(tag.url().toDisplayString(), 25);
0333         }
0334         if (!mUrlsOutput) {
0335             writeColumn(tag.name());
0336         }
0337         std::cout << std::endl;
0338     }
0339 
0340     emit finished();
0341 }