File indexing completed on 2024-05-05 04:22:02

0001 // SPDX-FileCopyrightText: 2003-2020 Jesper K. Pedersen <blackie@kde.org>
0002 // SPDX-FileCopyrightText: 2021-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0003 //
0004 // SPDX-License-Identifier: GPL-2.0-or-later
0005 
0006 #include "ImportHandler.h"
0007 
0008 #include "ImportSettings.h"
0009 #include "KimFileReader.h"
0010 #include "Logging.h"
0011 
0012 #include <Browser/BrowserWidget.h>
0013 #include <DB/Category.h>
0014 #include <DB/CategoryCollection.h>
0015 #include <DB/ImageDB.h>
0016 #include <DB/MD5.h>
0017 #include <DB/MD5Map.h>
0018 #include <MainWindow/Window.h>
0019 #include <Utilities/UniqFilenameMapper.h>
0020 
0021 #include <KConfigGroup>
0022 #include <KIO/StatJob>
0023 #include <KJobUiDelegate>
0024 #include <KJobWidgets>
0025 #include <KLocalizedString>
0026 #include <QApplication>
0027 #include <QFile>
0028 #include <QProgressDialog>
0029 #include <kio/job.h>
0030 #include <kio_version.h>
0031 #include <kmessagebox.h>
0032 #include <kwidgetsaddons_version.h>
0033 #include <memory>
0034 
0035 using namespace ImportExport;
0036 
0037 ImportExport::ImportHandler::ImportHandler()
0038     : m_fileMapper(nullptr)
0039     , m_finishedPressed(false)
0040     , m_progress(nullptr)
0041     , m_totalCopied(0)
0042     , m_job(nullptr)
0043     , m_reportUnreadableFiles(true)
0044     , m_eventLoop(new QEventLoop)
0045     , m_settings()
0046     , m_kimFileReader(nullptr)
0047 
0048 {
0049 }
0050 
0051 ImportHandler::~ImportHandler()
0052 {
0053     delete m_fileMapper;
0054     delete m_eventLoop;
0055 }
0056 
0057 bool ImportExport::ImportHandler::exec(const ImportSettings &settings, KimFileReader *kimFileReader)
0058 {
0059     m_settings = settings;
0060     m_kimFileReader = kimFileReader;
0061     m_finishedPressed = true;
0062     delete m_fileMapper;
0063     m_fileMapper = new Utilities::UniqFilenameMapper(m_settings.destination());
0064     bool ok;
0065     // copy images
0066     if (m_settings.externalSource()) {
0067         copyFromExternal();
0068 
0069         // If none of the images were to be copied, then we flushed the loop before we got started, in that case, don't start the loop.
0070         qCDebug(ImportExportLog) << "Copying" << m_pendingCopies.count() << "files from external source...";
0071         if (m_pendingCopies.count() > 0)
0072             ok = m_eventLoop->exec();
0073         else
0074             ok = false;
0075     } else {
0076         ok = copyFilesFromZipFile();
0077         if (ok)
0078             updateDB();
0079     }
0080     if (m_progress)
0081         delete m_progress;
0082 
0083     return ok;
0084 }
0085 
0086 void ImportExport::ImportHandler::copyFromExternal()
0087 {
0088     m_pendingCopies = m_settings.selectedImages();
0089     m_totalCopied = 0;
0090     m_progress = new QProgressDialog(MainWindow::Window::theMainWindow());
0091     m_progress->setWindowTitle(i18nc("@title:window", "Copying Images"));
0092     m_progress->setMinimum(0);
0093     m_progress->setMaximum(2 * m_pendingCopies.count());
0094     m_progress->show();
0095     connect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages);
0096     if (m_pendingCopies.isEmpty()) {
0097         qCDebug(ImportExportLog) << "No images selected for import!";
0098         m_eventLoop->exit(false);
0099         return;
0100     }
0101     copyNextFromExternal();
0102 }
0103 
0104 void ImportExport::ImportHandler::copyNextFromExternal()
0105 {
0106     // this function must not be called without a next image
0107     Q_ASSERT(!m_pendingCopies.isEmpty());
0108     DB::ImageInfoPtr info = m_pendingCopies[0];
0109 
0110     if (isImageAlreadyInDB(info)) {
0111         qCDebug(ImportExportLog) << info->fileName().relative() << "is already in database.";
0112         aCopyJobCompleted(nullptr);
0113         return;
0114     }
0115 
0116     const DB::FileName fileName = info->fileName();
0117 
0118     bool succeeded = false;
0119     QStringList tried;
0120 
0121     // First search for images next to the .kim file
0122     // Second search for images base on the image root as specified in the .kim file
0123     const QList<QUrl> searchUrls {
0124         m_settings.kimFile().adjusted(QUrl::RemoveFilename), m_settings.baseURL().adjusted(QUrl::RemoveFilename)
0125     };
0126     for (const QUrl &url : searchUrls) {
0127         QUrl src(url);
0128         src.setPath(src.path() + fileName.relative());
0129 
0130 #if KIO_VERSION < QT_VERSION_CHECK(5, 69, 0)
0131         std::unique_ptr<KIO::StatJob> statJob { KIO::stat(src, KIO::StatJob::SourceSide, 0 /* just query for existence */) };
0132 #else
0133         std::unique_ptr<KIO::StatJob> statJob { KIO::statDetails(src, KIO::StatJob::SourceSide, KIO::StatDetail::StatNoDetails) };
0134 #endif
0135         KJobWidgets::setWindow(statJob.get(), MainWindow::Window::theMainWindow());
0136         if (statJob->exec()) {
0137             QUrl dest = QUrl::fromLocalFile(m_fileMapper->uniqNameFor(fileName));
0138             m_job = KIO::file_copy(src, dest, -1, KIO::HideProgressInfo);
0139             connect(m_job, &KIO::FileCopyJob::result, this, &ImportHandler::aCopyJobCompleted);
0140             succeeded = true;
0141             qCDebug(ImportExportLog) << "Copying" << src << "to" << dest;
0142             break;
0143         } else
0144             tried << src.toDisplayString();
0145     }
0146 
0147     if (!succeeded)
0148         aCopyFailed(tried);
0149 }
0150 
0151 bool ImportExport::ImportHandler::copyFilesFromZipFile()
0152 {
0153     DB::ImageInfoList images = m_settings.selectedImages();
0154 
0155     m_totalCopied = 0;
0156     m_progress = new QProgressDialog(MainWindow::Window::theMainWindow());
0157     m_progress->setWindowTitle(i18nc("@title:window", "Copying Images"));
0158     m_progress->setMinimum(0);
0159     m_progress->setMaximum(2 * m_pendingCopies.count());
0160     m_progress->show();
0161 
0162     for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) {
0163         if (!isImageAlreadyInDB(*it)) {
0164             const DB::FileName fileName = (*it)->fileName();
0165             QByteArray data = m_kimFileReader->loadImage(fileName.relative());
0166             if (data.isNull())
0167                 return false;
0168             QString newName = m_fileMapper->uniqNameFor(fileName);
0169 
0170             QFile out(newName);
0171             if (!out.open(QIODevice::WriteOnly)) {
0172                 KMessageBox::error(MainWindow::Window::theMainWindow(), i18n("Error when writing image %1", newName));
0173                 return false;
0174             }
0175             out.write(data.constData(), data.size());
0176             out.close();
0177         }
0178 
0179         qApp->processEvents();
0180         m_progress->setValue(++m_totalCopied);
0181         if (m_progress->wasCanceled()) {
0182             return false;
0183         }
0184     }
0185     return true;
0186 }
0187 
0188 void ImportExport::ImportHandler::updateDB()
0189 {
0190     disconnect(m_progress, &QProgressDialog::canceled, this, &ImportHandler::stopCopyingImages);
0191     m_progress->setLabelText(i18n("Updating Database"));
0192     int len = Settings::SettingsData::instance()->imageDirectory().length();
0193     // image directory is always a prefix of destination
0194     if (len == m_settings.destination().length())
0195         len = 0;
0196     else
0197         qCDebug(ImportExportLog)
0198             << "Re-rooting of ImageInfos from " << Settings::SettingsData::instance()->imageDirectory()
0199             << " to " << m_settings.destination();
0200 
0201     // Run though all images
0202     DB::ImageInfoList images = m_settings.selectedImages();
0203     for (DB::ImageInfoListConstIterator it = images.constBegin(); it != images.constEnd(); ++it) {
0204         DB::ImageInfoPtr info = *it;
0205         if (len != 0) {
0206             // exchange prefix:
0207             QString name = m_settings.destination() + info->fileName().absolute().mid(len);
0208             qCDebug(ImportExportLog) << info->fileName().absolute() << " -> " << name;
0209             info->setFileName(DB::FileName::fromAbsolutePath(name));
0210         }
0211 
0212         if (isImageAlreadyInDB(info)) {
0213             qCDebug(ImportExportLog) << "Updating ImageInfo for " << info->fileName().absolute();
0214             updateInfo(matchingInfoFromDB(info), info);
0215         } else {
0216             qCDebug(ImportExportLog) << "Adding ImageInfo for " << info->fileName().absolute();
0217             addNewRecord(info);
0218         }
0219 
0220         m_progress->setValue(++m_totalCopied);
0221         if (m_progress->wasCanceled())
0222             break;
0223     }
0224 
0225     Browser::BrowserWidget::instance()->home();
0226 }
0227 
0228 void ImportExport::ImportHandler::stopCopyingImages()
0229 {
0230     m_job->kill();
0231 }
0232 
0233 void ImportExport::ImportHandler::aCopyFailed(QStringList files)
0234 {
0235     if (m_reportUnreadableFiles) {
0236         const QString warnMessage = i18n("Cannot copy from any of the following locations:");
0237         const QString title = i18nc("@title", "Copy failed");
0238 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0239         const auto answer = KMessageBox::warningTwoActionsCancelList(m_progress,
0240                                                                      warnMessage,
0241                                                                      files,
0242                                                                      title,
0243                                                                      KStandardGuiItem::cont(),
0244                                                                      KGuiItem(i18nc("@action:button", "Continue without Asking")));
0245         if (answer == KMessageBox::ButtonCode::SecondaryAction) {
0246 #else
0247         const auto answer = KMessageBox::warningYesNoCancelList(m_progress, warnMessage,
0248                                                                 files, title, KStandardGuiItem::cont(), KGuiItem(i18nc("@action:button", "Continue without Asking")));
0249         if (answer == KMessageBox::No) {
0250 #endif
0251             m_reportUnreadableFiles = false;
0252         } else if (answer == KMessageBox::Cancel) {
0253             // This might be late -- if we managed to copy some files, we will
0254             // just throw away any changes to the DB, but some new image files
0255             // might be in the image directory...
0256             m_eventLoop->exit(false);
0257             m_pendingCopies.pop_front();
0258             return;
0259         }
0260     }
0261     aCopyJobCompleted(nullptr);
0262 }
0263 
0264 void ImportExport::ImportHandler::aCopyJobCompleted(KJob *job)
0265 {
0266     qCDebug(ImportExportLog) << "CopyJob" << job << "completed.";
0267     m_pendingCopies.pop_front();
0268     if (job && job->error()) {
0269         job->uiDelegate()->showErrorMessage();
0270         m_eventLoop->exit(false);
0271     } else if (m_pendingCopies.count() == 0) {
0272         updateDB();
0273         m_eventLoop->exit(true);
0274     } else if (m_progress->wasCanceled()) {
0275         m_eventLoop->exit(false);
0276     } else {
0277         m_progress->setValue(++m_totalCopied);
0278         copyNextFromExternal();
0279     }
0280 }
0281 
0282 bool ImportExport::ImportHandler::isImageAlreadyInDB(const DB::ImageInfoPtr &info)
0283 {
0284     return DB::ImageDB::instance()->md5Map()->contains(info->MD5Sum());
0285 }
0286 
0287 DB::ImageInfoPtr ImportExport::ImportHandler::matchingInfoFromDB(const DB::ImageInfoPtr &info)
0288 {
0289     const DB::FileName name = DB::ImageDB::instance()->md5Map()->lookup(info->MD5Sum());
0290     return DB::ImageDB::instance()->info(name);
0291 }
0292 
0293 /**
0294  * Merge the ImageInfo data from the kim file into the existing ImageInfo.
0295  */
0296 void ImportExport::ImportHandler::updateInfo(DB::ImageInfoPtr dbInfo, DB::ImageInfoPtr newInfo)
0297 {
0298     if (dbInfo->label() != newInfo->label() && m_settings.importAction(QString::fromLatin1("*Label*")) == ImportSettings::Replace)
0299         dbInfo->setLabel(newInfo->label());
0300 
0301     if (dbInfo->description().simplified() != newInfo->description().simplified()) {
0302         if (m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Replace)
0303             dbInfo->setDescription(newInfo->description());
0304         else if (m_settings.importAction(QString::fromLatin1("*Description*")) == ImportSettings::Merge)
0305             dbInfo->setDescription(dbInfo->description() + QString::fromLatin1("<br/><br/>") + newInfo->description());
0306     }
0307 
0308     if (dbInfo->angle() != newInfo->angle() && m_settings.importAction(QString::fromLatin1("*Orientation*")) == ImportSettings::Replace)
0309         dbInfo->setAngle(newInfo->angle());
0310 
0311     if (dbInfo->date() != newInfo->date() && m_settings.importAction(QString::fromLatin1("*Date*")) == ImportSettings::Replace)
0312         dbInfo->setDate(newInfo->date());
0313 
0314     updateCategories(newInfo, dbInfo, false);
0315 }
0316 
0317 void ImportExport::ImportHandler::addNewRecord(DB::ImageInfoPtr info)
0318 {
0319     const DB::FileName importName = info->fileName();
0320 
0321     DB::ImageInfoPtr updateInfo(new DB::ImageInfo(importName, info->mediaType(), DB::FileInformation::Ignore));
0322     updateInfo->setLabel(info->label());
0323     updateInfo->setDescription(info->description());
0324     updateInfo->setDate(info->date());
0325     updateInfo->setAngle(info->angle());
0326     updateInfo->setMD5Sum(DB::MD5Sum(updateInfo->fileName()));
0327 
0328     DB::ImageInfoList list;
0329     list.append(updateInfo);
0330     DB::ImageDB::instance()->addImages(list);
0331 
0332     updateCategories(info, updateInfo, true);
0333 }
0334 
0335 void ImportExport::ImportHandler::updateCategories(DB::ImageInfoPtr XMLInfo, DB::ImageInfoPtr DBInfo, bool forceReplace)
0336 {
0337     // Run though the categories
0338     const QList<CategoryMatchSetting> matches = m_settings.categoryMatchSetting();
0339 
0340     for (const CategoryMatchSetting &match : matches) {
0341         QString XMLCategoryName = match.XMLCategoryName();
0342         QString DBCategoryName = match.DBCategoryName();
0343         ImportSettings::ImportAction action = m_settings.importAction(DBCategoryName);
0344 
0345         const Utilities::StringSet items = XMLInfo->itemsOfCategory(XMLCategoryName);
0346         DB::CategoryPtr DBCategoryPtr = DB::ImageDB::instance()->categoryCollection()->categoryForName(DBCategoryName);
0347 
0348         if (!forceReplace && action == ImportSettings::Replace)
0349             DBInfo->setCategoryInfo(DBCategoryName, Utilities::StringSet());
0350 
0351         if (action == ImportSettings::Merge || action == ImportSettings::Replace || forceReplace) {
0352             for (const QString &item : items) {
0353                 if (match.XMLtoDB().contains(item)) {
0354                     DBInfo->addCategoryInfo(DBCategoryName, match.XMLtoDB()[item]);
0355                     DBCategoryPtr->addItem(match.XMLtoDB()[item]);
0356                 }
0357             }
0358         }
0359     }
0360 }
0361 // vi:expandtab:tabstop=4 shiftwidth=4:
0362 
0363 #include "moc_ImportHandler.cpp"