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"