File indexing completed on 2024-04-28 03:51:50
0001 /* 0002 This file is part of the KDE Baloo Project 0003 SPDX-FileCopyrightText: 2012-2014 Vishesh Handa <me@vhanda.in> 0004 SPDX-FileCopyrightText: 2017-2018 James D. Smith <smithjd15@gmail.com> 0005 SPDX-FileCopyrightText: 2020 Stefan BrĂ¼ns <bruns@kde.org> 0006 0007 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0008 */ 0009 0010 #include "kio_tags.h" 0011 #include "kio_tags_debug.h" 0012 0013 #include <QUrl> 0014 0015 #include <KLocalizedString> 0016 #include <KUser> 0017 #include <KIO/Job> 0018 0019 #include <QCoreApplication> 0020 #include <QDir> 0021 #include <QRegularExpression> 0022 0023 #include "file.h" 0024 #include "taglistjob.h" 0025 #include "../common/udstools.h" 0026 0027 #include "term.h" 0028 0029 using namespace Baloo; 0030 0031 // Pseudo plugin class to embed meta data 0032 class KIOPluginForMetaData : public QObject 0033 { 0034 Q_OBJECT 0035 Q_PLUGIN_METADATA(IID "org.kde.kio.worker.tags" FILE "tags.json") 0036 }; 0037 0038 TagsProtocol::TagsProtocol(const QByteArray& pool_socket, const QByteArray& app_socket) 0039 : KIO::ForwardingWorkerBase("tags", pool_socket, app_socket) 0040 { 0041 } 0042 0043 TagsProtocol::~TagsProtocol() 0044 { 0045 } 0046 0047 KIO::WorkerResult TagsProtocol::listDir(const QUrl& url) 0048 { 0049 ParseResult result = parseUrl(url); 0050 0051 switch(result.urlType) { 0052 case InvalidUrl: 0053 case FileUrl: 0054 qCWarning(KIO_TAGS) << result.decodedUrl << "list() invalid url"; 0055 return KIO::WorkerResult::fail(KIO::ERR_CANNOT_ENTER_DIRECTORY, result.decodedUrl); 0056 case TagUrl: 0057 listEntries(result.pathUDSResults); 0058 } 0059 0060 return KIO::WorkerResult::pass(); 0061 } 0062 0063 KIO::WorkerResult TagsProtocol::stat(const QUrl& url) 0064 { 0065 ParseResult result = parseUrl(url); 0066 0067 switch(result.urlType) { 0068 case InvalidUrl: 0069 qCWarning(KIO_TAGS) << result.decodedUrl << "stat() invalid url"; 0070 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl); 0071 case FileUrl: 0072 return ForwardingWorkerBase::stat(result.fileUrl); 0073 case TagUrl: 0074 for (const KIO::UDSEntry& entry : std::as_const(result.pathUDSResults)) { 0075 if (entry.stringValue(KIO::UDSEntry::UDS_EXTRA) == result.tag) { 0076 statEntry(entry); 0077 break; 0078 } 0079 } 0080 } 0081 0082 return KIO::WorkerResult::pass(); 0083 } 0084 0085 KIO::WorkerResult TagsProtocol::copy(const QUrl& src, const QUrl& dest, int permissions, KIO::JobFlags flags) 0086 { 0087 Q_UNUSED(permissions); 0088 Q_UNUSED(flags); 0089 0090 ParseResult srcResult = parseUrl(src); 0091 ParseResult dstResult = parseUrl(dest, QList<ParseFlags>() << ChopLastSection << LazyValidation); 0092 0093 if (srcResult.urlType == InvalidUrl) { 0094 qCWarning(KIO_TAGS) << srcResult.decodedUrl << "copy() invalid src url"; 0095 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, srcResult.decodedUrl); 0096 } else if (dstResult.urlType == InvalidUrl) { 0097 qCWarning(KIO_TAGS) << dstResult.decodedUrl << "copy() invalid dest url"; 0098 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, dstResult.decodedUrl); 0099 } 0100 0101 auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& newTag) { 0102 qCDebug(KIO_TAGS) << md.filePath() << "adding tag" << newTag; 0103 QStringList tags = md.tags(); 0104 tags.append(newTag); 0105 md.setTags(tags); 0106 }; 0107 0108 if (srcResult.metaData.tags().contains(dstResult.tag)) { 0109 qCWarning(KIO_TAGS) << srcResult.fileUrl.toLocalFile() << "file already has tag" << dstResult.tag; 0110 infoMessage(i18n("File %1 already has tag %2", srcResult.fileUrl.toLocalFile(), dstResult.tag)); 0111 } else if (dstResult.urlType == TagUrl) { 0112 rewriteTags(srcResult.metaData, dstResult.tag); 0113 } 0114 0115 return KIO::WorkerResult::pass(); 0116 } 0117 0118 KIO::WorkerResult TagsProtocol::get(const QUrl& url) 0119 { 0120 ParseResult result = parseUrl(url); 0121 0122 switch(result.urlType) { 0123 case InvalidUrl: 0124 qCWarning(KIO_TAGS) << result.decodedUrl << "get() invalid url"; 0125 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl); 0126 case FileUrl: 0127 return ForwardingWorkerBase::get(result.fileUrl); 0128 case TagUrl: 0129 return KIO::WorkerResult::fail(KIO::ERR_UNSUPPORTED_ACTION, result.decodedUrl); 0130 } 0131 Q_UNREACHABLE(); 0132 return KIO::WorkerResult::pass(); 0133 } 0134 0135 KIO::WorkerResult TagsProtocol::rename(const QUrl& src, const QUrl& dest, KIO::JobFlags flags) 0136 { 0137 Q_UNUSED(flags); 0138 0139 ParseResult srcResult = parseUrl(src); 0140 ParseResult dstResult; 0141 0142 if (srcResult.urlType == FileUrl) { 0143 dstResult = parseUrl(dest, QList<ParseFlags>() << ChopLastSection); 0144 } else if (srcResult.urlType == TagUrl) { 0145 dstResult = parseUrl(dest, QList<ParseFlags>() << LazyValidation); 0146 } 0147 0148 if (srcResult.urlType == InvalidUrl) { 0149 qCWarning(KIO_TAGS) << srcResult.decodedUrl << "rename() invalid src url"; 0150 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, srcResult.decodedUrl); 0151 } else if (dstResult.urlType == InvalidUrl) { 0152 qCWarning(KIO_TAGS) << dstResult.decodedUrl << "rename() invalid dest url"; 0153 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, dstResult.decodedUrl); 0154 } 0155 0156 auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& oldTag, const QString& newTag) { 0157 qCDebug(KIO_TAGS) << md.filePath() << "swapping tag" << oldTag << "with" << newTag; 0158 QStringList tags = md.tags(); 0159 tags.removeAll(oldTag); 0160 tags.append(newTag); 0161 md.setTags(tags); 0162 }; 0163 0164 if (srcResult.metaData.tags().contains(dstResult.tag)) { 0165 qCWarning(KIO_TAGS) << srcResult.fileUrl.toLocalFile() << "file already has tag" << dstResult.tag; 0166 infoMessage(i18n("File %1 already has tag %2", srcResult.fileUrl.toLocalFile(), dstResult.tag)); 0167 } else if (srcResult.urlType == FileUrl) { 0168 rewriteTags(srcResult.metaData, srcResult.tag, dstResult.tag); 0169 } else if (srcResult.urlType == TagUrl) { 0170 ResultIterator it = srcResult.query.exec(); 0171 while (it.next()) { 0172 KFileMetaData::UserMetaData md(it.filePath()); 0173 if (it.filePath() == srcResult.fileUrl.toLocalFile()) { 0174 rewriteTags(md, srcResult.tag, dstResult.tag); 0175 } else if (srcResult.fileUrl.isEmpty()) { 0176 const auto tags = md.tags(); 0177 for (const QString& tag : tags) { 0178 if (tag == srcResult.tag || (tag.startsWith(srcResult.tag + QLatin1Char('/')))) { 0179 QString newTag = tag; 0180 newTag.replace(srcResult.tag, dstResult.tag, Qt::CaseInsensitive); 0181 rewriteTags(md, tag, newTag); 0182 } 0183 } 0184 } 0185 } 0186 } 0187 0188 return KIO::WorkerResult::pass(); 0189 } 0190 0191 KIO::WorkerResult TagsProtocol::del(const QUrl& url, bool isfile) 0192 { 0193 Q_UNUSED(isfile); 0194 0195 ParseResult result = parseUrl(url); 0196 0197 auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& tag) { 0198 qCDebug(KIO_TAGS) << md.filePath() << "removing tag" << tag; 0199 QStringList tags = md.tags(); 0200 tags.removeAll(tag); 0201 md.setTags(tags); 0202 }; 0203 0204 switch(result.urlType) { 0205 case InvalidUrl: 0206 qCWarning(KIO_TAGS) << result.decodedUrl << "del() invalid url"; 0207 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl); 0208 case FileUrl: 0209 case TagUrl: 0210 ResultIterator it = result.query.exec(); 0211 while (it.next()) { 0212 KFileMetaData::UserMetaData md(it.filePath()); 0213 if (it.filePath() == result.fileUrl.toLocalFile()) { 0214 rewriteTags(md, result.tag); 0215 } else if (result.fileUrl.isEmpty()) { 0216 const auto tags = md.tags(); 0217 for (const QString &tag : tags) { 0218 if ((tag == result.tag) || (tag.startsWith(result.tag + QLatin1Char('/'), Qt::CaseInsensitive))) { 0219 rewriteTags(md, tag); 0220 } 0221 } 0222 } 0223 } 0224 } 0225 0226 return KIO::WorkerResult::pass(); 0227 } 0228 0229 KIO::WorkerResult TagsProtocol::mimetype(const QUrl& url) 0230 { 0231 ParseResult result = parseUrl(url); 0232 0233 switch(result.urlType) { 0234 case InvalidUrl: 0235 qCWarning(KIO_TAGS) << result.decodedUrl << "mimetype() invalid url"; 0236 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl); 0237 case FileUrl: 0238 return ForwardingWorkerBase::mimetype(result.fileUrl); 0239 case TagUrl: 0240 mimeType(QStringLiteral("inode/directory")); 0241 } 0242 0243 return KIO::WorkerResult::pass(); 0244 } 0245 0246 KIO::WorkerResult TagsProtocol::mkdir(const QUrl& url, int permissions) 0247 { 0248 Q_UNUSED(permissions); 0249 0250 ParseResult result = parseUrl(url, QList<ParseFlags>() << LazyValidation); 0251 0252 switch(result.urlType) { 0253 case InvalidUrl: 0254 case FileUrl: 0255 qCWarning(KIO_TAGS) << result.decodedUrl << "mkdir() invalid url"; 0256 return KIO::WorkerResult::fail(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl); 0257 case TagUrl: 0258 m_unassignedTags << result.tag; 0259 } 0260 0261 return KIO::WorkerResult::pass(); 0262 } 0263 0264 bool TagsProtocol::rewriteUrl(const QUrl& url, QUrl& newURL) 0265 { 0266 Q_UNUSED(url); 0267 Q_UNUSED(newURL); 0268 0269 return false; 0270 } 0271 0272 TagsProtocol::ParseResult TagsProtocol::parseUrl(const QUrl& url, const QList<ParseFlags> &flags) 0273 { 0274 TagsProtocol::ParseResult result; 0275 result.decodedUrl = QUrl::fromPercentEncoding(url.toString().toUtf8()); 0276 0277 if ((url.scheme() == QLatin1String("tags")) && result.decodedUrl.length()>6 && result.decodedUrl.at(6) == QLatin1Char('/')) { 0278 result.urlType = InvalidUrl; 0279 return result; 0280 } 0281 0282 auto createUDSEntryForTag = [] (const QString& tagSection, const QString& tag) { 0283 KIO::UDSEntry uds; 0284 uds.reserve(9); 0285 uds.fastInsert(KIO::UDSEntry::UDS_NAME, tagSection); 0286 uds.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); 0287 uds.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); 0288 uds.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0700); 0289 uds.fastInsert(KIO::UDSEntry::UDS_USER, KUser().loginName()); 0290 uds.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("tag")); 0291 uds.fastInsert(KIO::UDSEntry::UDS_EXTRA, tag); 0292 0293 QString displayType; 0294 QString displayName; 0295 0296 // a tag/folder 0297 if (tagSection == tag) { 0298 displayType = i18nc("This is a noun", "Tag"); 0299 displayName = tag.section(QLatin1Char('/'), -1); 0300 } 0301 0302 // a tagged file 0303 else if (!tag.isEmpty()) { 0304 displayType = i18nc("This is a noun", "Tag Fragment"); 0305 if (tagSection == QStringLiteral("..")) { 0306 displayName = tag.section(QLatin1Char('/'), -2); 0307 } else if (tagSection == QStringLiteral(".")) { 0308 displayName = tag.section(QLatin1Char('/'), -1); 0309 } else { 0310 displayName = tagSection; 0311 } 0312 } 0313 0314 // The root folder 0315 else { 0316 displayType = i18n("All Tags"); 0317 displayName = i18n("All Tags"); 0318 } 0319 0320 uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_TYPE, displayType); 0321 uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, displayName); 0322 0323 return uds; 0324 }; 0325 0326 TagListJob* tagJob = new TagListJob(); 0327 if (!tagJob->exec()) { 0328 qCWarning(KIO_TAGS) << "tag fetch failed:" << tagJob->errorString(); 0329 return result; 0330 } 0331 0332 if (url.isLocalFile()) { 0333 result.urlType = FileUrl; 0334 result.fileUrl = url; 0335 result.metaData = KFileMetaData::UserMetaData(url.toLocalFile()); 0336 } else if (url.scheme() == QLatin1String("tags")) { 0337 bool validTag = flags.contains(LazyValidation); 0338 0339 // Determine the tag from the URL. 0340 result.tag = result.decodedUrl; 0341 result.tag.remove(url.scheme() + QLatin1Char(':')); 0342 result.tag = QDir::cleanPath(result.tag); 0343 while (result.tag.startsWith(QLatin1Char('/'))) { 0344 result.tag.remove(0, 1); 0345 } 0346 0347 // Extract any local file path from the URL. 0348 QString tag = result.tag.section(QDir::separator(), 0, -2); 0349 QString fileName = result.tag.section(QDir::separator(), -1, -1); 0350 int pos = 0; 0351 0352 // Extract and remove any multiple filename suffix from the file name. 0353 QRegularExpression regexp(QStringLiteral("\\s\\((\\d+)\\)$")); 0354 QRegularExpressionMatch regMatch = regexp.match(fileName); 0355 if (regMatch.hasMatch()) { 0356 pos = regMatch.captured(1).toInt(); 0357 0358 fileName.remove(regexp); 0359 } 0360 0361 Query q; 0362 q.setSearchString(QStringLiteral("tag=\"%1\" AND filename=\"%2\"").arg(tag, fileName)); 0363 ResultIterator it = q.exec(); 0364 0365 int i = 0; 0366 while (it.next()) { 0367 result.fileUrl = QUrl::fromLocalFile(it.filePath()); 0368 result.metaData = KFileMetaData::UserMetaData(it.filePath()); 0369 0370 if (i == pos) { 0371 break; 0372 } else { 0373 i++; 0374 } 0375 } 0376 0377 if (!result.fileUrl.isEmpty() || flags.contains(ChopLastSection)) { 0378 result.tag = result.tag.section(QDir::separator(), 0, -2); 0379 } 0380 0381 validTag = validTag || result.tag.isEmpty(); 0382 0383 if (!result.tag.isEmpty()) { 0384 // Create a query to find files that may be in the operation's scope. 0385 QString query = result.tag; 0386 query.prepend(QStringLiteral("tag:")); 0387 query.replace(QLatin1Char(' '), QStringLiteral(" AND tag:")); 0388 query.replace(QLatin1Char('/'), QStringLiteral(" AND tag:")); 0389 result.query.setSearchString(query); 0390 0391 qCDebug(KIO_TAGS) << result.decodedUrl << "url query:" << query; 0392 } 0393 0394 // Create the tag directory entries. 0395 int index = result.tag.count(QLatin1Char('/')) + (result.tag.isEmpty() ? 0 : 1); 0396 QStringList tagPaths; 0397 0398 const QStringList tags = QStringList() << tagJob->tags() << m_unassignedTags; 0399 for (const QString& tag : tags) { 0400 if (result.tag.isEmpty() || (tag.startsWith(result.tag, Qt::CaseInsensitive))) { 0401 QString tagSection = tag.section(QLatin1Char('/'), index, index, QString::SectionSkipEmpty); 0402 if (!tagPaths.contains(tagSection, Qt::CaseInsensitive) && !tagSection.isEmpty()) { 0403 result.pathUDSResults << createUDSEntryForTag(tagSection, tag); 0404 tagPaths << tagSection; 0405 } 0406 } 0407 0408 validTag = validTag || tag.startsWith(result.tag, Qt::CaseInsensitive); 0409 } 0410 0411 if (validTag && result.fileUrl.isEmpty()) { 0412 result.urlType = TagUrl; 0413 } else if (validTag && !result.fileUrl.isEmpty()) { 0414 result.urlType = FileUrl; 0415 } 0416 } 0417 0418 if (result.urlType == FileUrl) { 0419 return result; 0420 } else { 0421 result.pathUDSResults << createUDSEntryForTag(QStringLiteral("."), result.tag); 0422 } 0423 0424 // The root tag url has no file entries. 0425 if (result.tag.isEmpty()) { 0426 return result; 0427 } else { 0428 result.pathUDSResults << createUDSEntryForTag(QStringLiteral(".."), result.tag); 0429 } 0430 0431 // Query for any files associated with the tag. 0432 Query q; 0433 q.setSearchString(QStringLiteral("tag=\"%1\"").arg(result.tag)); 0434 ResultIterator it = q.exec(); 0435 QList<QString> resultNames; 0436 UdsFactory udsf; 0437 0438 while (it.next()) { 0439 KIO::UDSEntry uds = udsf.createUdsEntry(it.filePath()); 0440 if (uds.count() == 0) { 0441 continue; 0442 } 0443 0444 const QUrl url(uds.stringValue(KIO::UDSEntry::UDS_URL)); 0445 auto dupCount = resultNames.count(url.fileName()); 0446 if (dupCount > 0) { 0447 uds.replace(KIO::UDSEntry::UDS_NAME, url.fileName() + QStringLiteral(" (%1)").arg(dupCount)); 0448 } 0449 0450 qCDebug(KIO_TAGS) << result.tag << "adding file:" << uds.stringValue(KIO::UDSEntry::UDS_NAME); 0451 0452 resultNames << url.fileName(); 0453 result.pathUDSResults << uds; 0454 } 0455 0456 return result; 0457 } 0458 0459 extern "C" 0460 { 0461 Q_DECL_EXPORT int kdemain(int argc, char** argv) 0462 { 0463 QCoreApplication app(argc, argv); 0464 app.setApplicationName(QStringLiteral("kio_tags")); 0465 Baloo::TagsProtocol worker(argv[2], argv[3]); 0466 worker.dispatchLoop(); 0467 return 0; 0468 } 0469 } 0470 0471 #include "kio_tags.moc" 0472 #include "moc_kio_tags.cpp"