File indexing completed on 2024-04-28 05:47:34

0001 /*
0002     SPDX-FileCopyrightText: 2008 Harald Hvaal <haraldhv@stud.ntnu.no>
0003     SPDX-FileCopyrightText: 2009-2010 Raphael Kubo da Costa <rakuco@FreeBSD.org>
0004 
0005     SPDX-License-Identifier: BSD-2-Clause
0006 */
0007 
0008 #include "batchextract.h"
0009 #include "ark_debug.h"
0010 #include "extractiondialog.h"
0011 #include "jobs.h"
0012 #include "queries.h"
0013 
0014 #include <KIO/JobTracker>
0015 #include <KIO/JobUiDelegate>
0016 #include <KIO/JobUiDelegateFactory>
0017 #include <KIO/OpenUrlJob>
0018 #include <KLocalizedString>
0019 #include <KMessageBox>
0020 #include <KWidgetJobTracker>
0021 
0022 #include <QDir>
0023 #include <QFileInfo>
0024 #include <QPointer>
0025 #include <QTimer>
0026 
0027 BatchExtract::BatchExtract(QObject *parent)
0028     : KCompositeJob(parent)
0029     , m_autoSubfolder(false)
0030     , m_preservePaths(true)
0031     , m_openDestinationAfterExtraction(false)
0032 {
0033     setCapabilities(KJob::Killable);
0034 
0035     connect(this, &KJob::result, this, &BatchExtract::showFailedFiles);
0036 }
0037 
0038 BatchExtract::~BatchExtract()
0039 {
0040 }
0041 
0042 void BatchExtract::addExtraction(const QUrl &url)
0043 {
0044     QString destination = destinationFolder();
0045 
0046     auto job = Kerfuffle::Archive::batchExtract(url.toLocalFile(), destination, autoSubfolder(), preservePaths());
0047 
0048     qCDebug(ARK) << QString(QStringLiteral("Registering job from archive %1, to %2, preservePaths %3"))
0049                         .arg(url.toLocalFile(), destination, QString::number(preservePaths()));
0050 
0051     addSubjob(job);
0052 
0053     m_fileNames[job] = qMakePair(url.toLocalFile(), destination);
0054 
0055     connect(job, &KJob::percentChanged, this, &BatchExtract::forwardProgress);
0056 
0057     connect(job, &Kerfuffle::BatchExtractJob::userQuery, this, &BatchExtract::slotUserQuery);
0058 }
0059 
0060 bool BatchExtract::doKill()
0061 {
0062     if (subjobs().isEmpty()) {
0063         return false;
0064     }
0065 
0066     return subjobs().first()->kill();
0067 }
0068 
0069 void BatchExtract::slotUserQuery(Kerfuffle::Query *query)
0070 {
0071     query->execute();
0072 }
0073 
0074 bool BatchExtract::autoSubfolder() const
0075 {
0076     return m_autoSubfolder;
0077 }
0078 
0079 void BatchExtract::setAutoSubfolder(bool value)
0080 {
0081     m_autoSubfolder = value;
0082 }
0083 
0084 void BatchExtract::start()
0085 {
0086     QTimer::singleShot(0, this, &BatchExtract::slotStartJob);
0087 }
0088 
0089 void BatchExtract::slotStartJob()
0090 {
0091     if (m_inputs.isEmpty()) {
0092         emitResult();
0093         return;
0094     }
0095 
0096     for (const auto &url : std::as_const(m_inputs)) {
0097         addExtraction(url);
0098     }
0099 
0100     KIO::getJobTracker()->registerJob(this);
0101 
0102     Q_EMIT description(this,
0103                        i18n("Extracting Files"),
0104                        qMakePair(i18n("Source archive"), m_fileNames.value(subjobs().at(0)).first),
0105                        qMakePair(i18n("Destination"), m_fileNames.value(subjobs().at(0)).second));
0106 
0107     m_initialJobCount = subjobs().size();
0108 
0109     qCDebug(ARK) << "Starting first job";
0110 
0111     subjobs().at(0)->start();
0112 }
0113 
0114 void BatchExtract::showFailedFiles()
0115 {
0116     if (!m_failedFiles.isEmpty()) {
0117         KMessageBox::informationList(nullptr, i18n("The following files could not be extracted:"), m_failedFiles);
0118     }
0119 }
0120 
0121 void BatchExtract::slotResult(KJob *job)
0122 {
0123     if (job->error()) {
0124         qCDebug(ARK) << "There was en error:" << job->error() << ", errorText:" << job->errorString();
0125 
0126         setErrorText(job->errorString());
0127         setError(job->error());
0128 
0129         removeSubjob(job);
0130 
0131         if (job->error() != KJob::KilledJobError) {
0132             const QString filename = m_fileNames.value(job).first;
0133             if (hasSubjobs()) {
0134                 KMessageBox::error(
0135                     nullptr,
0136                     job->errorString().isEmpty()
0137                         ? xi18nc("@info", "There was an error while extracting <filename>%1</filename>. Any further archive will not be extracted.", filename)
0138                         : xi18nc("@info",
0139                                  "There was an error while extracting <filename>%1</filename>:<nl/><message>%2</message><nl/>Any further archive will not be "
0140                                  "extracted.",
0141                                  filename,
0142                                  job->errorString()));
0143             } else {
0144                 KMessageBox::error(nullptr,
0145                                    job->errorString().isEmpty()
0146                                        ? xi18nc("@info", "There was an error while extracting <filename>%1</filename>.", filename)
0147                                        : xi18nc("@info",
0148                                                 "There was an error while extracting <filename>%1</filename>:<nl/><message>%2</message>",
0149                                                 filename,
0150                                                 job->errorString()));
0151             }
0152         }
0153 
0154         emitResult();
0155         return;
0156     }
0157 
0158     removeSubjob(job);
0159 
0160     if (!hasSubjobs()) {
0161         if (openDestinationAfterExtraction()) {
0162             const QString path = QDir::cleanPath(destinationFolder());
0163             const QUrl destination(QUrl::fromLocalFile(path));
0164             KIO::OpenUrlJob *job = new KIO::OpenUrlJob(destination, QStringLiteral("inode/directory"));
0165             job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
0166             job->start();
0167         }
0168 
0169         qCDebug(ARK) << "Finished, emitting the result";
0170         emitResult();
0171     } else {
0172         qCDebug(ARK) << "Starting the next job";
0173         Q_EMIT description(this,
0174                            i18n("Extracting Files"),
0175                            qMakePair(i18n("Source archive"), m_fileNames.value(subjobs().at(0)).first),
0176                            qMakePair(i18n("Destination"), m_fileNames.value(subjobs().at(0)).second));
0177         subjobs().at(0)->start();
0178     }
0179 }
0180 
0181 void BatchExtract::forwardProgress(KJob *job, unsigned long percent)
0182 {
0183     Q_UNUSED(job)
0184     auto jobPart = static_cast<ulong>(100 / m_initialJobCount);
0185     auto remainingJobs = static_cast<ulong>(m_initialJobCount - subjobs().size());
0186     setPercent(jobPart * remainingJobs + percent / static_cast<ulong>(m_initialJobCount));
0187 }
0188 
0189 void BatchExtract::addInput(const QUrl &url)
0190 {
0191     qCDebug(ARK) << "Adding archive" << url.toLocalFile();
0192 
0193     if (!QFileInfo::exists(url.toLocalFile())) {
0194         m_failedFiles.append(url.fileName());
0195         return;
0196     }
0197 
0198     m_inputs.append(url);
0199 }
0200 
0201 bool BatchExtract::openDestinationAfterExtraction() const
0202 {
0203     return m_openDestinationAfterExtraction;
0204 }
0205 
0206 bool BatchExtract::preservePaths() const
0207 {
0208     return m_preservePaths;
0209 }
0210 
0211 QString BatchExtract::destinationFolder() const
0212 {
0213     if (m_destinationFolder.isEmpty()) {
0214         return QDir::currentPath();
0215     } else {
0216         return m_destinationFolder;
0217     }
0218 }
0219 
0220 void BatchExtract::setDestinationFolder(const QString &folder)
0221 {
0222     if (QFileInfo(folder).isDir()) {
0223         m_destinationFolder = folder;
0224         // Magic property that tells the job tracker the job's destination
0225         setProperty("destUrl", QUrl::fromLocalFile(folder).toString());
0226     }
0227 }
0228 
0229 void BatchExtract::setOpenDestinationAfterExtraction(bool value)
0230 {
0231     m_openDestinationAfterExtraction = value;
0232 }
0233 
0234 void BatchExtract::setPreservePaths(bool value)
0235 {
0236     m_preservePaths = value;
0237 }
0238 
0239 bool BatchExtract::showExtractDialog()
0240 {
0241     QPointer<Kerfuffle::ExtractionDialog> dialog = new Kerfuffle::ExtractionDialog;
0242 
0243     if (m_inputs.size() > 1) {
0244         dialog.data()->batchModeOption();
0245     }
0246 
0247     dialog.data()->setModal(true);
0248     dialog.data()->setAutoSubfolder(autoSubfolder());
0249     dialog.data()->setCurrentUrl(QUrl::fromUserInput(destinationFolder(), QDir::currentPath(), QUrl::AssumeLocalFile));
0250     dialog.data()->setPreservePaths(preservePaths());
0251 
0252     // Only one archive, we need a LoadJob to get the single-folder and subfolder properties.
0253     // TODO: find a better way (e.g. let the dialog handle everything), otherwise we list
0254     // the archive twice (once here and once in the following BatchExtractJob).
0255     Kerfuffle::LoadJob *loadJob = nullptr;
0256     if (m_inputs.size() == 1) {
0257         loadJob = Kerfuffle::Archive::load(m_inputs.at(0).toLocalFile(), this);
0258         // We need to access the job after result has been emitted, if the user rejects the dialog.
0259         loadJob->setAutoDelete(false);
0260 
0261         connect(loadJob, &KJob::result, this, [=](KJob *job) {
0262             if (job->error()) {
0263                 return;
0264             }
0265 
0266             auto archive = qobject_cast<Kerfuffle::LoadJob *>(job)->archive();
0267             dialog->setExtractToSubfolder(archive->hasMultipleTopLevelEntries());
0268             dialog->setSubfolder(archive->subfolderName());
0269         });
0270 
0271         connect(loadJob, &KJob::result, dialog.data(), &Kerfuffle::ExtractionDialog::setReadyGui);
0272         dialog->setBusyGui();
0273         // NOTE: we exploit the dialog->exec() below to run this job.
0274         loadJob->start();
0275     }
0276 
0277     QUrl destinationDirectory;
0278     if (dialog.data()->exec()) {
0279         destinationDirectory = dialog.data()->destinationDirectory();
0280         if (destinationDirectory.isLocalFile()) {
0281             setAutoSubfolder(false && dialog.data()->autoSubfolders());
0282             setDestinationFolder(destinationDirectory.toLocalFile());
0283             setOpenDestinationAfterExtraction(dialog.data()->openDestinationAfterExtraction());
0284             setPreservePaths(dialog.data()->preservePaths());
0285 
0286             delete dialog.data();
0287             return true;
0288         }
0289     }
0290 
0291     // we get here if no directory was chosen, or the chosen directory is not local
0292     if (loadJob) {
0293         loadJob->kill();
0294         loadJob->deleteLater();
0295     }
0296 
0297     if (!destinationDirectory.isEmpty() && !destinationDirectory.isLocalFile()) {
0298         KMessageBox::error(nullptr,
0299                            xi18nc("@info",
0300                                   "The archive could not be extracted to <filename>%1</filename> because Ark can only extract to local destinations.",
0301                                   destinationDirectory.toDisplayString()));
0302     }
0303 
0304     delete dialog.data();
0305 
0306     return false;
0307 }
0308 
0309 #include "moc_batchextract.cpp"