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 }