diff --git a/src/kioslaves/tags/kio_tags.cpp b/src/kioslaves/tags/kio_tags.cpp index 99dbe946..b81647e2 100644 --- a/src/kioslaves/tags/kio_tags.cpp +++ b/src/kioslaves/tags/kio_tags.cpp @@ -1,489 +1,489 @@ /* * This file is part of the KDE Baloo Project * Copyright (C) 2012-2014 Vishesh Handa * Copyright (C) 2017-2018 James D. Smith * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) version 3, or any * later version accepted by the membership of KDE e.V. (or its * successor approved by the membership of KDE e.V.), which shall * act as a proxy defined in Section 6 of version 3 of the license. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see . * */ #include "kio_tags.h" #include "kio_tags_debug.h" #include #include #include #include #include #include #include #include #include #include #include "file.h" #include "taglistjob.h" #include "query.h" #include "term.h" using namespace Baloo; TagsProtocol::TagsProtocol(const QByteArray& pool_socket, const QByteArray& app_socket) : KIO::ForwardingSlaveBase("tags", pool_socket, app_socket) { } TagsProtocol::~TagsProtocol() { } void TagsProtocol::listDir(const QUrl& url) { ParseResult result = parseUrl(url); switch(result.urlType) { case InvalidUrl: case FileUrl: qCWarning(KIO_TAGS) << result.decodedUrl << "list() invalid url"; error(KIO::ERR_CANNOT_ENTER_DIRECTORY, result.decodedUrl); return; case TagUrl: listEntries(result.pathUDSResults); } finished(); } void TagsProtocol::stat(const QUrl& url) { ParseResult result = parseUrl(url); switch(result.urlType) { case InvalidUrl: qCWarning(KIO_TAGS) << result.decodedUrl << "stat() invalid url"; error(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl); return; case FileUrl: ForwardingSlaveBase::stat(result.fileUrl); return; case TagUrl: for (const KIO::UDSEntry& entry : qAsConst(result.pathUDSResults)) { if (entry.stringValue(KIO::UDSEntry::UDS_EXTRA) == result.tag) { statEntry(entry); } } } finished(); } void TagsProtocol::copy(const QUrl& src, const QUrl& dest, int permissions, KIO::JobFlags flags) { Q_UNUSED(permissions); Q_UNUSED(flags); ParseResult srcResult = parseUrl(src); ParseResult dstResult = parseUrl(dest, QList() << ChopLastSection << LazyValidation); if (srcResult.urlType == InvalidUrl) { qCWarning(KIO_TAGS) << srcResult.decodedUrl << "copy() invalid src url"; error(KIO::ERR_DOES_NOT_EXIST, srcResult.decodedUrl); return; } else if (dstResult.urlType == InvalidUrl) { qCWarning(KIO_TAGS) << dstResult.decodedUrl << "copy() invalid dest url"; error(KIO::ERR_DOES_NOT_EXIST, dstResult.decodedUrl); return; } auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& newTag) { qCDebug(KIO_TAGS) << md.filePath() << "adding tag" << newTag; QStringList tags = md.tags(); tags.append(newTag); md.setTags(tags); }; if (srcResult.metaData.tags().contains(dstResult.tag)) { qCWarning(KIO_TAGS) << srcResult.fileUrl.toLocalFile() << "file already has tag" << dstResult.tag; infoMessage(i18n("File %1 already has tag %2", srcResult.fileUrl.toLocalFile(), dstResult.tag)); } else if (dstResult.urlType == TagUrl) { rewriteTags(srcResult.metaData, dstResult.tag); } finished(); } void TagsProtocol::get(const QUrl& url) { ParseResult result = parseUrl(url); switch(result.urlType) { case InvalidUrl: qCWarning(KIO_TAGS) << result.decodedUrl << "get() invalid url"; error(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl); return; case FileUrl: ForwardingSlaveBase::get(result.fileUrl); return; case TagUrl: error(KIO::ERR_UNSUPPORTED_ACTION, result.decodedUrl); return; } } void TagsProtocol::rename(const QUrl& src, const QUrl& dest, KIO::JobFlags flags) { Q_UNUSED(flags); ParseResult srcResult = parseUrl(src); ParseResult dstResult; if (srcResult.urlType == FileUrl) { dstResult = parseUrl(dest, QList() << ChopLastSection); } else if (srcResult.urlType == TagUrl) { dstResult = parseUrl(dest, QList() << LazyValidation); } if (srcResult.urlType == InvalidUrl) { qCWarning(KIO_TAGS) << srcResult.decodedUrl << "rename() invalid src url"; error(KIO::ERR_DOES_NOT_EXIST, srcResult.decodedUrl); return; } else if (dstResult.urlType == InvalidUrl) { qCWarning(KIO_TAGS) << dstResult.decodedUrl << "rename() invalid dest url"; error(KIO::ERR_DOES_NOT_EXIST, dstResult.decodedUrl); return; } auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& oldTag, const QString& newTag) { qCDebug(KIO_TAGS) << md.filePath() << "swapping tag" << oldTag << "with" << newTag; QStringList tags = md.tags(); tags.removeAll(oldTag); tags.append(newTag); md.setTags(tags); }; if (srcResult.metaData.tags().contains(dstResult.tag)) { qCWarning(KIO_TAGS) << srcResult.fileUrl.toLocalFile() << "file already has tag" << dstResult.tag; infoMessage(i18n("File %1 already has tag %2", srcResult.fileUrl.toLocalFile(), dstResult.tag)); } else if (srcResult.urlType == FileUrl) { rewriteTags(srcResult.metaData, srcResult.tag, dstResult.tag); } else if (srcResult.urlType == TagUrl) { ResultIterator it = srcResult.query.exec(); while (it.next()) { KFileMetaData::UserMetaData md(it.filePath()); if (it.filePath() == srcResult.fileUrl.toLocalFile()) { rewriteTags(md, srcResult.tag, dstResult.tag); } else if (srcResult.fileUrl.isEmpty()) { const auto tags = md.tags(); for (const QString& tag : tags) { if (tag == srcResult.tag || (tag.startsWith(srcResult.tag + QLatin1Char('/')))) { QString newTag = tag; newTag.replace(srcResult.tag, dstResult.tag, Qt::CaseInsensitive); rewriteTags(md, tag, newTag); } } } } } finished(); } void TagsProtocol::del(const QUrl& url, bool isfile) { Q_UNUSED(isfile); ParseResult result = parseUrl(url); auto rewriteTags = [] (KFileMetaData::UserMetaData& md, const QString& tag) { qCDebug(KIO_TAGS) << md.filePath() << "removing tag" << tag; QStringList tags = md.tags(); tags.removeAll(tag); md.setTags(tags); }; switch(result.urlType) { case InvalidUrl: qCWarning(KIO_TAGS) << result.decodedUrl << "del() invalid url"; error(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl); return; case FileUrl: case TagUrl: ResultIterator it = result.query.exec(); while (it.next()) { KFileMetaData::UserMetaData md(it.filePath()); if (it.filePath() == result.fileUrl.toLocalFile()) { rewriteTags(md, result.tag); } else if (result.fileUrl.isEmpty()) { const auto tags = md.tags(); for (const QString &tag : tags) { if ((tag == result.tag) || (tag.startsWith(result.tag + QLatin1Char('/'), Qt::CaseInsensitive))) { rewriteTags(md, tag); } } } } } finished(); } void TagsProtocol::mimetype(const QUrl& url) { ParseResult result = parseUrl(url); switch(result.urlType) { case InvalidUrl: qCWarning(KIO_TAGS) << result.decodedUrl << "mimetype() invalid url"; error(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl); return; case FileUrl: ForwardingSlaveBase::mimetype(result.fileUrl); return; case TagUrl: mimeType(QStringLiteral("inode/directory")); } finished(); } void TagsProtocol::mkdir(const QUrl& url, int permissions) { Q_UNUSED(permissions); ParseResult result = parseUrl(url, QList() << LazyValidation); switch(result.urlType) { case InvalidUrl: case FileUrl: qCWarning(KIO_TAGS) << result.decodedUrl << "mkdir() invalid url"; error(KIO::ERR_DOES_NOT_EXIST, result.decodedUrl); return; case TagUrl: m_unassignedTags << result.tag; } finished(); } bool TagsProtocol::rewriteUrl(const QUrl& url, QUrl& newURL) { Q_UNUSED(url); Q_UNUSED(newURL); return false; } TagsProtocol::ParseResult TagsProtocol::parseUrl(const QUrl& url, const QList &flags) { TagsProtocol::ParseResult result; result.decodedUrl = QUrl::fromPercentEncoding(url.toString().toUtf8()); - if (result.decodedUrl.at(6) == QChar('/')) { + if ((url.scheme() == QLatin1String("tags")) && result.decodedUrl.at(6) == QChar('/')) { result.urlType = InvalidUrl; return result; } auto createUDSEntryForTag = [] (const QString& tagSection, const QString& tag) { KIO::UDSEntry uds; uds.reserve(9); uds.fastInsert(KIO::UDSEntry::UDS_NAME, tagSection); uds.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR); uds.fastInsert(KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/directory")); uds.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0700); uds.fastInsert(KIO::UDSEntry::UDS_USER, KUser().loginName()); uds.fastInsert(KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("tag")); uds.fastInsert(KIO::UDSEntry::UDS_EXTRA, tag); QString displayType; if (tagSection == tag) { displayType = i18n("Tag"); } else if (!tag.isEmpty()) { displayType = i18n("Tag Fragment"); } else { displayType = i18n("All Tags"); } uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_TYPE, displayType); QString displayName = i18n("All Tags"); if (!tag.isEmpty() && ((tagSection == QStringLiteral(".")) || (tagSection == QStringLiteral("..")))) { displayName = tag.section(QLatin1Char('/'), -1); } else { displayName = tagSection; } uds.fastInsert(KIO::UDSEntry::UDS_DISPLAY_NAME, displayName); return uds; }; TagListJob* tagJob = new TagListJob(); if (!tagJob->exec()) { qCWarning(KIO_TAGS) << "tag fetch failed:" << tagJob->errorString(); return result; } if (url.isLocalFile()) { result.urlType = FileUrl; result.fileUrl = url; result.metaData = KFileMetaData::UserMetaData(url.toLocalFile()); } else if (url.scheme() == QLatin1String("tags")) { bool validTag = flags.contains(LazyValidation); // Determine the tag from the URL. result.tag = result.decodedUrl; result.tag.remove(url.scheme() + QLatin1Char(':')); result.tag = QDir::cleanPath(result.tag); while (result.tag.startsWith(QLatin1Char('/'))) { result.tag.remove(0, 1); } // Extract any local file path from the URL. QString tag = result.tag.section(QDir::separator(), 0, -2); QString fileName = result.tag.section(QDir::separator(), -1, -1); int pos = 0; // Extract and remove any multiple filename suffix from the file name. QRegularExpression regexp(QStringLiteral("\\s\\((\\d+)\\)$")); QRegularExpressionMatch regMatch = regexp.match(fileName); if (regMatch.hasMatch()) { pos = regMatch.captured(1).toInt(); fileName.remove(regexp); } Query q; q.setSearchString(QStringLiteral("tag=\"%1\" AND filename=\"%2\"").arg(tag, fileName)); ResultIterator it = q.exec(); int i = 0; while (it.next()) { result.fileUrl = QUrl::fromLocalFile(it.filePath()); result.metaData = KFileMetaData::UserMetaData(it.filePath()); if (i == pos) { break; } else { i++; } } if (!result.fileUrl.isEmpty() || flags.contains(ChopLastSection)) { result.tag = result.tag.section(QDir::separator(), 0, -2); } validTag = validTag || result.tag.isEmpty(); if (!result.tag.isEmpty()) { // Create a query to find files that may be in the operation's scope. QString query = result.tag; query.prepend("tag:"); query.replace(' ', " AND tag:"); query.replace('/', " AND tag:"); result.query.setSearchString(query); qCDebug(KIO_TAGS) << result.decodedUrl << "url query:" << query; } // Create the tag directory entries. int index = result.tag.count(QLatin1Char('/')) + (result.tag.isEmpty() ? 0 : 1); QStringList tagPaths; const QStringList tags = QStringList() << tagJob->tags() << m_unassignedTags; for (const QString& tag : tags) { if (result.tag.isEmpty() || (tag.startsWith(result.tag, Qt::CaseInsensitive))) { QString tagSection = tag.section(QLatin1Char('/'), index, index, QString::SectionSkipEmpty); if (!tagPaths.contains(tagSection, Qt::CaseInsensitive) && !tagSection.isEmpty()) { result.pathUDSResults << createUDSEntryForTag(tagSection, tag); tagPaths << tagSection; } } validTag = validTag || tag.startsWith(result.tag, Qt::CaseInsensitive); } if (validTag && result.fileUrl.isEmpty()) { result.urlType = TagUrl; } else if (validTag && !result.fileUrl.isEmpty()) { result.urlType = FileUrl; } } if (result.urlType == FileUrl) { return result; } else { result.pathUDSResults << createUDSEntryForTag(QStringLiteral("."), result.tag); } // The root tag url has no file entries. if (result.tag.isEmpty()) { return result; } else { result.pathUDSResults << createUDSEntryForTag(QStringLiteral(".."), result.tag); } // Query for any files associated with the tag. Query q; q.setSearchString(QStringLiteral("tag=\"%1\"").arg(result.tag)); ResultIterator it = q.exec(); QList resultNames; while (it.next()) { const QUrl& match = QUrl::fromLocalFile(it.filePath()); // Somehow stat the file. KIO::UDSEntry uds; KIO::StatJob* job = KIO::stat(match, KIO::HideProgressInfo); // We do not want to wait for the event loop to delete the job. QScopedPointer sp(job); job->setAutoDelete(false); if (job->exec()) { uds = job->statResult(); } else { continue; } uds.fastInsert(KIO::UDSEntry::UDS_TARGET_URL, match.toString()); uds.fastInsert(KIO::UDSEntry::UDS_ICON_OVERLAY_NAMES, QStringLiteral("tag")); if (resultNames.contains(match.fileName())) { uds.replace(KIO::UDSEntry::UDS_NAME, match.fileName() + QStringLiteral(" (%1)").arg(resultNames.count(match.fileName()))); } qCDebug(KIO_TAGS) << result.tag << "adding file:" << uds.stringValue(KIO::UDSEntry::UDS_NAME); resultNames << match.fileName(); result.pathUDSResults << uds; } return result; } extern "C" { Q_DECL_EXPORT int kdemain(int argc, char** argv) { QCoreApplication app(argc, argv); app.setApplicationName(QStringLiteral("kio_tags")); Baloo::TagsProtocol slave(argv[2], argv[3]); slave.dispatchLoop(); return 0; } }