Warning, file /libraries/baloo-widgets/src/filemetadataprovider.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz@gmx.at>
0003     SPDX-FileCopyrightText: 2012 Vishesh Handa <me@vhanda.in>
0004     SPDX-FileCopyrightText: 2021 Kai Uwe Broulik <kde@broulik.de>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "filemetadataprovider.h"
0010 #include "filemetadatautil_p.h"
0011 #include "filefetchjob.h"
0012 
0013 #include <KFileMetaData/PropertyInfo>
0014 #include <KFormat>
0015 #include <KLocalizedString>
0016 #include <KProtocolInfo>
0017 #include <KShell>
0018 
0019 #include <QPair>
0020 
0021 // Required includes for subDirectoriesCount():
0022 #ifdef Q_OS_WIN
0023 #include <QDir>
0024 #else
0025 #include <QFile>
0026 #include <dirent.h>
0027 #endif
0028 
0029 using namespace Baloo;
0030 
0031 namespace
0032 {
0033 /**
0034  * The standard QMap::unite will contain the key multiple times if both \p v1 and \p v2
0035  * contain the same key.
0036  *
0037  * This will only take the key from \p v2 into account
0038  */
0039 QVariantMap unite(const QVariantMap &v1, const QVariantMap &v2)
0040 {
0041     QVariantMap v(v1);
0042     QMapIterator<QString, QVariant> it(v2);
0043     while (it.hasNext()) {
0044         it.next();
0045 
0046         v[it.key()] = it.value();
0047     }
0048 
0049     return v;
0050 }
0051 
0052 /**
0053 * @return The number of files and hidden files for the directory path.
0054 */
0055 QPair<int, int> subDirectoriesCount(const QString &path)
0056 {
0057 #ifdef Q_OS_WIN
0058     QDir dir(path);
0059     int count = dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System).count();
0060     int hiddenCount = dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::Hidden).count();
0061     return QPair<int, int>(count, hiddenCount);
0062 #else
0063     // Taken from kdelibs/kio/kio/kdirmodel.cpp
0064     // SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org>
0065 
0066     int count = -1;
0067     int hiddenCount = -1;
0068     DIR *dir = ::opendir(QFile::encodeName(path).constData());
0069     if (dir) {
0070         count = 0;
0071         hiddenCount = 0;
0072         struct dirent *dirEntry = nullptr;
0073         while ((dirEntry = ::readdir(dir))) { // krazy:exclude=syscalls
0074             if (dirEntry->d_name[0] == '.') {
0075                 if (dirEntry->d_name[1] == '\0') {
0076                     // Skip "."
0077                     continue;
0078                 }
0079                 if (dirEntry->d_name[1] == '.' && dirEntry->d_name[2] == '\0') {
0080                     // Skip ".."
0081                     continue;
0082                 }
0083                 // hidden files
0084                 hiddenCount++;
0085             } else {
0086                 ++count;
0087             }
0088         }
0089         ::closedir(dir);
0090     }
0091     return QPair<int, int>(count, hiddenCount);
0092 #endif
0093 }
0094 
0095 /**
0096  * Fill \p data with properties can be derived from others
0097  */
0098 void extractDerivedProperties(QVariantMap &data)
0099 {
0100     const auto width = data.value(QStringLiteral("width"));
0101     const auto height = data.value(QStringLiteral("height"));
0102     if ((width.type() == QVariant::Double || width.type() == QVariant::Int) && (height.type() == QVariant::Double || height.type() == QVariant::Int)) {
0103         data.insert(QStringLiteral("dimensions"), i18nc("width × height", "%1 × %2", width.toInt(), height.toInt()));
0104     }
0105 
0106     bool okLatitude;
0107     const auto gpsLatitude = data.value(QStringLiteral("photoGpsLatitude")).toFloat(&okLatitude);
0108     bool okLongitude;
0109     const auto gpsLongitude = data.value(QStringLiteral("photoGpsLongitude")).toFloat(&okLongitude);
0110 
0111     if (okLatitude && okLongitude) {
0112         data.insert(QStringLiteral("gpsLocation"), QVariant::fromValue(QPair<float, float>(gpsLatitude, gpsLongitude)));
0113     }
0114 }
0115 } // anonymous namespace
0116 
0117 void FileMetaDataProvider::slotFileFetchFinished(KJob *job)
0118 {
0119     auto fetchJob = static_cast<FileFetchJob *>(job);
0120     QList<QVariantMap> files = fetchJob->data();
0121 
0122     Q_ASSERT(!files.isEmpty());
0123 
0124     if (files.size() > 1) {
0125         Baloo::Private::mergeCommonData(m_data, files);
0126     } else {
0127         m_data = unite(m_data, files.first());
0128     }
0129     extractDerivedProperties(m_data);
0130     m_readOnly = !fetchJob->canEditAll();
0131 
0132     insertEditableData();
0133     Q_EMIT loadingFinished();
0134 }
0135 
0136 void FileMetaDataProvider::insertSingleFileBasicData()
0137 {
0138     // TODO: Handle case if remote URLs are used properly. isDir() does
0139     // not work, the modification date needs also to be adjusted...
0140     Q_ASSERT(m_fileItems.count() == 1);
0141     {
0142         const KFileItem &item = m_fileItems.first();
0143 
0144         KFormat format;
0145         if (item.isDir()) {
0146             if (item.isLocalFile() && !item.isSlow()) {
0147                 const QPair<int, int> counts = subDirectoriesCount(item.url().path());
0148                 const int count = counts.first;
0149                 if (count != -1) {
0150                     QString itemCountString = i18ncp("@item:intable", "%1 item", "%1 items", count);
0151                     m_data.insert(QStringLiteral("kfileitem#size"), itemCountString);
0152 
0153                     const int hiddenCount = counts.second;
0154                     if (hiddenCount > 0) {
0155                         // add hidden items count
0156                         QString hiddenCountString = i18ncp("@item:intable", "%1 item", "%1 items", hiddenCount);
0157                         m_data.insert(QStringLiteral("kfileitem#hiddenItems"), hiddenCountString);
0158                     }
0159                 }
0160             } else if (item.entry().contains(KIO::UDSEntry::UDS_SIZE)) {
0161                 m_data.insert(QStringLiteral("kfileitem#size"), format.formatByteSize(item.size()));
0162             }
0163             if (item.entry().contains(KIO::UDSEntry::UDS_RECURSIVE_SIZE)) {
0164                 m_data.insert(QStringLiteral("kfileitem#totalSize"), format.formatByteSize(item.recursiveSize()));
0165             }
0166         } else {
0167             if (item.entry().contains(KIO::UDSEntry::UDS_SIZE)) {
0168                 m_data.insert(QStringLiteral("kfileitem#size"), format.formatByteSize(item.size()));
0169             }
0170         }
0171 
0172         m_data.insert(QStringLiteral("kfileitem#type"), item.mimeComment());
0173         if (item.isLink()) {
0174             m_data.insert(QStringLiteral("kfileitem#linkDest"), item.linkDest());
0175         }
0176         if (item.entry().contains(KIO::UDSEntry::UDS_TARGET_URL)) {
0177             m_data.insert(QStringLiteral("kfileitem#targetUrl"), KShell::tildeCollapse(item.targetUrl().toDisplayString(QUrl::PreferLocalFile)));
0178         }
0179         QDateTime modificationTime = item.time(KFileItem::ModificationTime);
0180         if (modificationTime.isValid()) {
0181             m_data.insert(QStringLiteral("kfileitem#modified"), modificationTime);
0182         }
0183         QDateTime creationTime = item.time(KFileItem::CreationTime);
0184         if (creationTime.isValid()) {
0185             m_data.insert(QStringLiteral("kfileitem#created"), creationTime);
0186         }
0187         QDateTime accessTime = item.time(KFileItem::AccessTime);
0188         if (accessTime.isValid()) {
0189             m_data.insert(QStringLiteral("kfileitem#accessed"), accessTime);
0190         }
0191 
0192         m_data.insert(QStringLiteral("kfileitem#owner"), item.user());
0193         m_data.insert(QStringLiteral("kfileitem#group"), item.group());
0194         m_data.insert(QStringLiteral("kfileitem#permissions"), item.permissionsString());
0195 
0196         const auto extraFields = KProtocolInfo::extraFields(item.url());
0197         for (int i = 0; i < extraFields.count(); ++i) {
0198             const auto &field = extraFields.at(i);
0199             if (field.type == KProtocolInfo::ExtraField::Invalid) {
0200                 continue;
0201             }
0202 
0203             const QString text = item.entry().stringValue(KIO::UDSEntry::UDS_EXTRA + i);
0204             if (text.isEmpty()) {
0205                 continue;
0206             }
0207 
0208             const QString key = QStringLiteral("kfileitem#extra_%1_%2").arg(item.url().scheme(), QString::number(i + 1));
0209 
0210             if (field.type == KProtocolInfo::ExtraField::DateTime) {
0211                 const QDateTime date = QDateTime::fromString(text, Qt::ISODate);
0212                 if (!date.isValid()) {
0213                     continue;
0214                 }
0215 
0216                 m_data.insert(key, date);
0217             } else {
0218                 m_data.insert(key, text);
0219             }
0220         }
0221     }
0222 }
0223 
0224 void FileMetaDataProvider::insertFilesListBasicData()
0225 {
0226     // If all directories
0227     Q_ASSERT(m_fileItems.count() > 1);
0228     bool allDirectories = true;
0229     for (const KFileItem &item : std::as_const(m_fileItems)) {
0230         allDirectories &= item.isDir();
0231         if (!allDirectories) {
0232             break;
0233         }
0234     }
0235 
0236     if (allDirectories) {
0237         int count = 0;
0238         int hiddenCount = 0;
0239         for (const KFileItem &item : std::as_const(m_fileItems)) {
0240             if (!item.isLocalFile() || item.isSlow()) {
0241                 return;
0242             }
0243             const QPair<int, int> counts = subDirectoriesCount(item.url().path());
0244             const int subcount = counts.first;
0245             if (subcount == -1) {
0246                 return;
0247             }
0248             count += subcount;
0249             hiddenCount += counts.second;
0250         }
0251         QString itemCountString = i18ncp("@item:intable", "%1 item", "%1 items", count);
0252         if (hiddenCount > 0) {
0253             // add hidden items count
0254             QString hiddenCountString = i18ncp("@item:intable", "%1 item", "%1 items", hiddenCount);
0255             m_data.insert(QStringLiteral("kfileitem#hiddenItems"), hiddenCountString);
0256         }
0257         m_data.insert(QStringLiteral("kfileitem#totalSize"), itemCountString);
0258 
0259     } else {
0260         // Calculate the size of all items
0261         quint64 totalSize = 0;
0262         for (const KFileItem &item : std::as_const(m_fileItems)) {
0263             if (!item.isDir() && !item.isLink()) {
0264                 totalSize += item.size();
0265             }
0266         }
0267         KFormat format;
0268         m_data.insert(QStringLiteral("kfileitem#totalSize"), format.formatByteSize(totalSize));
0269     }
0270 }
0271 
0272 void FileMetaDataProvider::insertEditableData()
0273 {
0274     if (!m_readOnly) {
0275         if (!m_data.contains(QStringLiteral("tags"))) {
0276             m_data.insert(QStringLiteral("tags"), QVariant());
0277         }
0278         if (!m_data.contains(QStringLiteral("rating"))) {
0279             m_data.insert(QStringLiteral("rating"), 0);
0280         }
0281         if (!m_data.contains(QStringLiteral("userComment"))) {
0282             m_data.insert(QStringLiteral("userComment"), QVariant());
0283         }
0284     }
0285 }
0286 
0287 FileMetaDataProvider::FileMetaDataProvider(QObject *parent)
0288     : QObject(parent)
0289     , m_readOnly(false)
0290 {
0291 }
0292 
0293 FileMetaDataProvider::~FileMetaDataProvider() = default;
0294 
0295 void FileMetaDataProvider::setFileItem()
0296 {
0297     // There are 3 code paths -
0298     // Remote file
0299     // Single local file -
0300     //   * Not Indexed
0301     //   * Indexed
0302     //
0303     insertSingleFileBasicData();
0304     const QUrl url = m_fileItems.first().targetUrl();
0305     if (!url.isLocalFile() || m_fileItems.first().isSlow()) {
0306         // FIXME - are extended attributes supported for remote files?
0307         m_readOnly = true;
0308         Q_EMIT loadingFinished();
0309         return;
0310     }
0311 
0312     const QString filePath = url.toLocalFile();
0313     FileFetchJob *job;
0314 
0315     // Not indexed or only basic file indexing (no content)
0316     if (!m_config.fileIndexingEnabled() || !m_config.shouldBeIndexed(filePath) || m_config.onlyBasicIndexing()) {
0317         job = new FileFetchJob(QStringList{filePath}, true, FileFetchJob::UseRealtimeIndexing::Only, this);
0318 
0319         // Fully indexed by Baloo
0320     } else {
0321         job = new FileFetchJob(QStringList{filePath}, true, FileFetchJob::UseRealtimeIndexing::Fallback, this);
0322     }
0323     connect(job, &FileFetchJob::finished, this, &FileMetaDataProvider::slotFileFetchFinished);
0324     job->start();
0325 }
0326 
0327 void FileMetaDataProvider::setFileItems()
0328 {
0329     // Multiple Files -
0330     //   * Not Indexed
0331     //   * Indexed
0332 
0333     QStringList urls;
0334     urls.reserve(m_fileItems.size());
0335     // Only extract data from indexed files,
0336     // it would be too expensive otherwise.
0337     for (const KFileItem &item : std::as_const(m_fileItems)) {
0338         const QUrl url = item.targetUrl();
0339         if (url.isLocalFile() && !item.isSlow()) {
0340             urls << url.toLocalFile();
0341         }
0342     }
0343 
0344     insertFilesListBasicData();
0345     if (!urls.isEmpty()) {
0346         // Editing only if all URLs are local
0347         bool canEdit = (urls.size() == m_fileItems.size());
0348 
0349         auto job = new FileFetchJob(urls, canEdit, FileFetchJob::UseRealtimeIndexing::Disabled, this);
0350         connect(job, &FileFetchJob::finished, this, &FileMetaDataProvider::slotFileFetchFinished);
0351         job->start();
0352 
0353     } else {
0354         // FIXME - are extended attributes supported for remote files?
0355         m_readOnly = true;
0356         Q_EMIT loadingFinished();
0357     }
0358 }
0359 
0360 void FileMetaDataProvider::setItems(const KFileItemList &items)
0361 {
0362     m_fileItems = items;
0363     m_data.clear();
0364 
0365     if (items.isEmpty()) {
0366         Q_EMIT loadingFinished();
0367     } else if (items.size() == 1) {
0368         setFileItem();
0369     } else {
0370         setFileItems();
0371     }
0372 }
0373 
0374 QString FileMetaDataProvider::label(const QString &metaDataLabel) const
0375 {
0376     static QHash<QString, QString> hash = {
0377         {QStringLiteral("kfileitem#comment"), i18nc("@label", "Comment")},
0378         {QStringLiteral("kfileitem#created"), i18nc("@label", "Created")},
0379         {QStringLiteral("kfileitem#accessed"), i18nc("@label", "Accessed")},
0380         {QStringLiteral("kfileitem#modified"), i18nc("@label", "Modified")},
0381         {QStringLiteral("kfileitem#owner"), i18nc("@label", "Owner")},
0382         {QStringLiteral("kfileitem#group"), i18nc("@label", "Group")},
0383         {QStringLiteral("kfileitem#permissions"), i18nc("@label", "Permissions")},
0384         {QStringLiteral("kfileitem#rating"), i18nc("@label", "Rating")},
0385         {QStringLiteral("kfileitem#size"), i18nc("@label", "Size")},
0386         {QStringLiteral("kfileitem#tags"), i18nc("@label", "Tags")},
0387         {QStringLiteral("kfileitem#totalSize"), i18nc("@label", "Total Size")},
0388         {QStringLiteral("kfileitem#hiddenItems"), i18nc("@label", "Hidden items")},
0389         {QStringLiteral("kfileitem#type"), i18nc("@label", "Type")},
0390         {QStringLiteral("kfileitem#linkDest"), i18nc("@label", "Link to")},
0391         {QStringLiteral("kfileitem#targetUrl"), i18nc("@label", "Points to")},
0392         {QStringLiteral("tags"), i18nc("@label", "Tags")},
0393         {QStringLiteral("rating"), i18nc("@label", "Rating")},
0394         {QStringLiteral("userComment"), i18nc("@label", "Comment")},
0395         {QStringLiteral("originUrl"), i18nc("@label", "Downloaded From")},
0396         {QStringLiteral("dimensions"), i18nc("@label", "Dimensions")},
0397         {QStringLiteral("gpsLocation"), i18nc("@label", "GPS Location")},
0398     };
0399 
0400     QString value = hash.value(metaDataLabel);
0401     if (value.isEmpty()) {
0402         static const auto extraPrefix = QStringLiteral("kfileitem#extra_");
0403         if (metaDataLabel.startsWith(extraPrefix)) {
0404 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0405             const auto parts = metaDataLabel.splitRef(QLatin1Char('_'));
0406 #else
0407             const auto parts = metaDataLabel.split(QLatin1Char('_'));
0408 #endif
0409             Q_ASSERT(parts.count() == 3);
0410             const auto protocol = parts.at(1);
0411             const int extraNumber = parts.at(2).toInt() - 1;
0412 
0413             // Have to construct a dummy URL for KProtocolInfo::extraFields...
0414             QUrl url;
0415 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0416             url.setScheme(protocol.toString());
0417 #else
0418             url.setScheme(protocol);
0419 #endif
0420             const auto extraFields = KProtocolInfo::extraFields(url);
0421             auto field = extraFields.value(extraNumber);
0422             if (field.type != KProtocolInfo::ExtraField::Invalid) {
0423                 value = field.name;
0424             }
0425         }
0426     }
0427 
0428     if (value.isEmpty()) {
0429         value = KFileMetaData::PropertyInfo::fromName(metaDataLabel).displayName();
0430     }
0431 
0432     return value;
0433 }
0434 
0435 QString FileMetaDataProvider::group(const QString &label) const
0436 {
0437     static QHash<QString, QString> uriGrouper = {
0438 
0439         // KFileItem Data
0440         {QStringLiteral("kfileitem#type"), QStringLiteral("0FileItemA")},
0441         {QStringLiteral("kfileitem#linkDest"), QStringLiteral("0FileItemB")},
0442         {QStringLiteral("kfileitem#size"), QStringLiteral("0FileItemC")},
0443         {QStringLiteral("kfileitem#totalSize"), QStringLiteral("0FileItemC")},
0444         {QStringLiteral("kfileitem#hiddenItems"), QStringLiteral("0FileItemD")},
0445         {QStringLiteral("kfileitem#modified"), QStringLiteral("0FileItemE")},
0446         {QStringLiteral("kfileitem#accessed"), QStringLiteral("0FileItemF")},
0447         {QStringLiteral("kfileitem#created"), QStringLiteral("0FileItemG")},
0448         {QStringLiteral("kfileitem#owner"), QStringLiteral("0FileItemH")},
0449         {QStringLiteral("kfileitem#group"), QStringLiteral("0FileItemI")},
0450         {QStringLiteral("kfileitem#permissions"), QStringLiteral("0FileItemJ")},
0451 
0452         // Editable Data
0453         {QStringLiteral("tags"), QStringLiteral("1EditableDataA")},
0454         {QStringLiteral("rating"), QStringLiteral("1EditableDataB")},
0455         {QStringLiteral("userComment"), QStringLiteral("1EditableDataC")},
0456 
0457         // Image Data
0458         {QStringLiteral("width"), QStringLiteral("2ImageA")},
0459         {QStringLiteral("height"), QStringLiteral("2ImageB")},
0460         {QStringLiteral("dimensions"), QStringLiteral("2ImageCA")},
0461         {QStringLiteral("photoFNumber"), QStringLiteral("2ImageC")},
0462         {QStringLiteral("photoExposureTime"), QStringLiteral("2ImageD")},
0463         {QStringLiteral("photoExposureBiasValue"), QStringLiteral("2ImageE")},
0464         {QStringLiteral("photoISOSpeedRatings"), QStringLiteral("2ImageF")},
0465         {QStringLiteral("photoFocalLength"), QStringLiteral("2ImageG")},
0466         {QStringLiteral("photoFocalLengthIn35mmFilm"), QStringLiteral("2ImageH")},
0467         {QStringLiteral("photoFlash"), QStringLiteral("2ImageI")},
0468         {QStringLiteral("imageOrientation"), QStringLiteral("2ImageJ")},
0469         {QStringLiteral("photoGpsLocation"), QStringLiteral("2ImageK")},
0470         {QStringLiteral("photoGpsLatitude"), QStringLiteral("2ImageL")},
0471         {QStringLiteral("photoGpsLongitude"), QStringLiteral("2ImageM")},
0472         {QStringLiteral("photoGpsAltitude"), QStringLiteral("2ImageN")},
0473         {QStringLiteral("manufacturer"), QStringLiteral("2ImageO")},
0474         {QStringLiteral("model"), QStringLiteral("2ImageP")},
0475 
0476         // Media Data
0477         {QStringLiteral("title"), QStringLiteral("3MediaA")},
0478         {QStringLiteral("artist"), QStringLiteral("3MediaB")},
0479         {QStringLiteral("album"), QStringLiteral("3MediaC")},
0480         {QStringLiteral("albumArtist"), QStringLiteral("3MediaD")},
0481         {QStringLiteral("genre"), QStringLiteral("3MediaE")},
0482         {QStringLiteral("trackNumber"), QStringLiteral("3MediaF")},
0483         {QStringLiteral("discNumber"), QStringLiteral("3MediaG")},
0484         {QStringLiteral("releaseYear"), QStringLiteral("3MediaH")},
0485         {QStringLiteral("duration"), QStringLiteral("3MediaI")},
0486         {QStringLiteral("sampleRate"), QStringLiteral("3MediaJ")},
0487         {QStringLiteral("bitRate"), QStringLiteral("3MediaK")},
0488 
0489         // Miscellaneous Data
0490         {QStringLiteral("originUrl"), QStringLiteral("4MiscA")},
0491     };
0492 
0493     const QString val = uriGrouper.value(label);
0494     if (val.isEmpty()) {
0495         return QStringLiteral("lastGroup");
0496     }
0497     return val;
0498 }
0499 
0500 KFileItemList FileMetaDataProvider::items() const
0501 {
0502     return m_fileItems;
0503 }
0504 
0505 void FileMetaDataProvider::setReadOnly(bool readOnly)
0506 {
0507     m_readOnly = readOnly;
0508 }
0509 
0510 bool FileMetaDataProvider::isReadOnly() const
0511 {
0512     return m_readOnly;
0513 }
0514 
0515 QVariantMap FileMetaDataProvider::data() const
0516 {
0517     return m_data;
0518 }