File indexing completed on 2024-04-21 12:15:18

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