File indexing completed on 2024-05-12 16:46:30

0001 /***************************************************************************
0002     Copyright (C) 2005-2009 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include <config.h>
0026 
0027 #include "filelistingimporter.h"
0028 #include "../collections/filecatalog.h"
0029 #include "../entry.h"
0030 #include "../field.h"
0031 #include "../fieldformat.h"
0032 #include "../images/imagefactory.h"
0033 #include "../utils/tellico_utils.h"
0034 #include "../utils/guiproxy.h"
0035 #include "../progressmanager.h"
0036 #include "../core/netaccess.h"
0037 #include "../tellico_debug.h"
0038 
0039 #include <KLocalizedString>
0040 #include <KJobWidgets/KJobWidgets>
0041 #include <KIO/Job>
0042 #include <Solid/Device>
0043 #include <Solid/StorageVolume>
0044 #include <Solid/StorageAccess>
0045 
0046 #ifdef HAVE_KFILEMETADATA
0047 #include <KFileMetaData/Extractor>
0048 #include <KFileMetaData/ExtractorCollection>
0049 #include <KFileMetaData/SimpleExtractionResult>
0050 #include <KFileMetaData/PropertyInfo>
0051 #endif
0052 
0053 #include <QDate>
0054 #include <QDir>
0055 #include <QCheckBox>
0056 #include <QGroupBox>
0057 #include <QFile>
0058 #include <QFileInfo>
0059 #include <QVBoxLayout>
0060 #include <QApplication>
0061 
0062 namespace {
0063   static const int FILE_PREVIEW_SIZE = 128;
0064 }
0065 
0066 using Tellico::Import::FileListingImporter;
0067 
0068 FileListingImporter::FileListingImporter(const QUrl& url_) : Importer(url_), m_coll(nullptr), m_widget(nullptr),
0069     m_recursive(nullptr), m_filePreview(nullptr), m_job(nullptr), m_cancelled(false) {
0070 }
0071 
0072 bool FileListingImporter::canImport(int type) const {
0073   return type == Data::Collection::File;
0074 }
0075 
0076 Tellico::Data::CollPtr FileListingImporter::collection() {
0077   if(m_coll) {
0078     return m_coll;
0079   }
0080 
0081   ProgressItem& item = ProgressManager::self()->newProgressItem(this, i18n("Scanning files..."), true);
0082   item.setTotalSteps(100);
0083   connect(&item, &Tellico::ProgressItem::signalCancelled, this, &FileListingImporter::slotCancel);
0084   ProgressItem::Done done(this);
0085 
0086   // going to assume only one volume will ever be imported
0087   const QString volume = volumeName();
0088 
0089   // the importer might be running without a gui/widget
0090   m_job = (m_widget && m_recursive->isChecked())
0091           ? KIO::listRecursive(url(), KIO::DefaultFlags, false /* include hidden */)
0092           : KIO::listDir(url(), KIO::DefaultFlags, false /* include hidden */);
0093   KJobWidgets::setWindow(m_job, GUI::Proxy::widget());
0094   void (KIO::ListJob::* jobEntries)(KIO::Job*, const KIO::UDSEntryList&) = &KIO::ListJob::entries;
0095   connect(static_cast<KIO::ListJob*>(m_job.data()), jobEntries, this, &FileListingImporter::slotEntries);
0096 
0097   if(!m_job->exec() || m_cancelled) {
0098     myDebug() << "did not run job:" << m_job->errorString();
0099     return Data::CollPtr();
0100   }
0101 
0102 #ifdef HAVE_KFILEMETADATA
0103   KFileMetaData::ExtractorCollection extractors;
0104   QHash<KFileMetaData::Property::Property, QString> propertyNameHash;
0105 #endif
0106 
0107   QStringList metaIgnore = QStringList()
0108                          << QStringLiteral("mimeType")
0109                          << QStringLiteral("url")
0110                          << QStringLiteral("fileName")
0111                          << QStringLiteral("lastModified")
0112                          << QStringLiteral("contentSize")
0113                          << QStringLiteral("type");
0114 
0115   const bool usePreview = m_widget && m_filePreview->isChecked();
0116 
0117   const QString title    = QStringLiteral("title");
0118   const QString url      = QStringLiteral("url");
0119   const QString desc     = QStringLiteral("description");
0120   const QString vol      = QStringLiteral("volume");
0121   const QString folder   = QStringLiteral("folder");
0122   const QString type     = QStringLiteral("mimetype");
0123   const QString size     = QStringLiteral("size");
0124   const QString perm     = QStringLiteral("permissions");
0125   const QString owner    = QStringLiteral("owner");
0126   const QString group    = QStringLiteral("group");
0127   const QString created  = QStringLiteral("created");
0128   const QString modified = QStringLiteral("modified");
0129   const QString metainfo = QStringLiteral("metainfo");
0130   const QString icon     = QStringLiteral("icon");
0131 
0132   // cache the icon image ids to avoid repeated creation of Data::Image objects
0133   QHash<QString, QString> iconImageId;
0134 
0135   m_coll = new Data::FileCatalog(true);
0136   QString tmp;
0137   const uint stepSize = qMax(1, m_files.count()/100);
0138   const bool showProgress = options() & ImportProgress;
0139 
0140   item.setTotalSteps(m_files.count());
0141   uint j = 0;
0142   foreach(const KFileItem& item, m_files) {
0143     if(m_cancelled) {
0144       break;
0145     }
0146 
0147     Data::EntryPtr entry(new Data::Entry(m_coll));
0148 
0149     const QUrl u = item.url();
0150     entry->setField(title,  u.fileName());
0151     entry->setField(url,    u.url());
0152     entry->setField(desc,   item.mimeComment());
0153     entry->setField(vol,    volume);
0154     tmp = QDir(this->url().toLocalFile()).relativeFilePath(u.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path());
0155     // use empty string for root folder instead of "."
0156     entry->setField(folder, tmp == QLatin1String(".") ? QString() : tmp);
0157     entry->setField(type,   item.mimetype());
0158     entry->setField(size,   KIO::convertSize(item.size()));
0159     entry->setField(perm,   item.permissionsString());
0160     entry->setField(owner,  item.user());
0161     entry->setField(group,  item.group());
0162 
0163     QDateTime dt(item.time(KFileItem::CreationTime));
0164     if(!dt.isNull()) {
0165       entry->setField(created, dt.date().toString(Qt::ISODate));
0166     }
0167     dt = QDateTime(item.time(KFileItem::ModificationTime));
0168     if(!dt.isNull()) {
0169       entry->setField(modified, dt.date().toString(Qt::ISODate));
0170     }
0171 
0172 #ifdef HAVE_KFILEMETADATA
0173     KFileMetaData::SimpleExtractionResult result(u.toLocalFile(),
0174                                                  item.mimetype(),
0175                                                  KFileMetaData::ExtractionResult::ExtractMetaData);
0176     QList<KFileMetaData::Extractor*> exList = extractors.fetchExtractors(item.mimetype());
0177     foreach(KFileMetaData::Extractor* ex, exList) {
0178 // initializing exempi can cause a crash in Exiv for files with XMP data
0179 // crude workaround is to avoid using the exivextractor and the only apparent way is to
0180 // match against the mimetypes. Here, we use image/x-exv as the canary in the coal mine
0181 // see https://bugs.kde.org/show_bug.cgi?id=390744
0182 #ifdef HAVE_EXEMPI
0183       if(!ex->mimetypes().contains(QStringLiteral("image/x-exv"))) {
0184 #else
0185       if(true) {
0186 #endif
0187         ex->extract(&result);
0188       }
0189     }
0190     QStringList strings;
0191     KFileMetaData::PropertyMap properties = result.properties();
0192     KFileMetaData::PropertyMap::const_iterator it = properties.constBegin();
0193     for( ; it != properties.constEnd(); ++it) {
0194       const QString value = it.value().toString();
0195       if(!value.isEmpty()) {
0196         QString label;
0197         if(propertyNameHash.contains(it.key())) {
0198           label = propertyNameHash.value(it.key());
0199         } else {
0200           label = KFileMetaData::PropertyInfo(it.key()).displayName();
0201           propertyNameHash.insert(it.key(), label);
0202         }
0203 //        myDebug() << label << value;
0204         if(!metaIgnore.contains(label)) {
0205           strings << label + FieldFormat::columnDelimiterString() + value;
0206         }
0207       }
0208     }
0209     entry->setField(metainfo, strings.join(FieldFormat::rowDelimiterString()));
0210 #endif
0211 
0212     QPixmap pixmap;
0213     if(!m_cancelled && usePreview) {
0214       pixmap = Tellico::NetAccess::filePreview(item, FILE_PREVIEW_SIZE);
0215     }
0216     if(pixmap.isNull()) {
0217       if(iconImageId.contains(item.iconName())) {
0218         entry->setField(icon, iconImageId.value(item.iconName()));
0219       } else {
0220         pixmap = QIcon::fromTheme(item.iconName()).pixmap(QSize(FILE_PREVIEW_SIZE, FILE_PREVIEW_SIZE));
0221         const QString id = ImageFactory::addImage(pixmap, QStringLiteral("PNG"));
0222         if(!id.isEmpty()) {
0223           entry->setField(icon, id);
0224           iconImageId.insert(item.iconName(), id);
0225         }
0226       }
0227     } else {
0228       const QString id = ImageFactory::addImage(pixmap, QStringLiteral("PNG"));
0229       if(!id.isEmpty()) {
0230         entry->setField(icon, id);
0231       }
0232     }
0233 
0234     m_coll->addEntries(entry);
0235 
0236     if(showProgress && j%stepSize == 0) {
0237       ProgressManager::self()->setProgress(this, j);
0238       qApp->processEvents();
0239     }
0240     ++j;
0241   }
0242 
0243   if(m_cancelled) {
0244     m_coll = Data::CollPtr();
0245     return m_coll;
0246   }
0247 
0248   return m_coll;
0249 }
0250 
0251 QWidget* FileListingImporter::widget(QWidget* parent_) {
0252   if(m_widget) {
0253     return m_widget;
0254   }
0255 
0256   m_widget = new QWidget(parent_);
0257   QVBoxLayout* l = new QVBoxLayout(m_widget);
0258 
0259   QGroupBox* gbox = new QGroupBox(i18n("File Listing Options"), m_widget);
0260   QVBoxLayout* vlay = new QVBoxLayout(gbox);
0261 
0262   m_recursive = new QCheckBox(i18n("Recursive folder search"), gbox);
0263   m_recursive->setWhatsThis(i18n("If checked, folders are recursively searched for all files."));
0264   // by default, make it checked
0265   m_recursive->setChecked(true);
0266 
0267   m_filePreview = new QCheckBox(i18n("Generate file previews"), gbox);
0268   m_filePreview->setWhatsThis(i18n("If checked, previews of the file contents are generated, which can slow down "
0269                                       "the folder listing."));
0270   // by default, make it no previews
0271   m_filePreview->setChecked(false);
0272 
0273   vlay->addWidget(m_recursive);
0274   vlay->addWidget(m_filePreview);
0275 
0276   l->addWidget(gbox);
0277   l->addStretch(1);
0278   return m_widget;
0279 }
0280 
0281 void FileListingImporter::slotEntries(KIO::Job* job_, const KIO::UDSEntryList& list_) {
0282   if(m_cancelled) {
0283     job_->kill();
0284     m_job = nullptr;
0285     return;
0286   }
0287 
0288   for(KIO::UDSEntryList::ConstIterator it = list_.begin(); it != list_.end(); ++it) {
0289     KFileItem item(*it, url(), false, true);
0290     if(item.isFile()) {
0291       m_files.append(item);
0292     }
0293   }
0294 }
0295 
0296 QString FileListingImporter::volumeName() const {
0297   const QString filePath = url().toLocalFile();
0298   QString matchingPath, volume;
0299   QList<Solid::Device> devices = Solid::Device::listFromType(Solid::DeviceInterface::StorageVolume, QString());
0300   foreach(const Solid::Device& device, devices) {
0301     const Solid::StorageAccess* acc = device.as<const Solid::StorageAccess>();
0302     if(acc && !acc->filePath().isEmpty() && filePath.startsWith(acc->filePath())) {
0303       // it might be possible for one volume to be mounted at /dir1 and another to be mounted at /dir1/dir2
0304       // so we need to find the longest natching filePath
0305       if(acc->filePath().length() > matchingPath.length()) {
0306         matchingPath = acc->filePath();
0307         const Solid::StorageVolume* vol = device.as<const Solid::StorageVolume>();
0308         if(vol) {
0309           volume = vol->label();
0310         }
0311       }
0312     }
0313   }
0314   return volume;
0315 }
0316 
0317 void FileListingImporter::slotCancel() {
0318   m_cancelled = true;
0319   if(m_job) {
0320     m_job->kill();
0321   }
0322 }