1 /*
2     SPDX-FileCopyrightText: 2008 Harald Hvaal <haraldhv@stud.ntnu.no>
3     SPDX-FileCopyrightText: 2009-2010 Raphael Kubo da Costa <rakuco@FreeBSD.org>
4 
5     SPDX-License-Identifier: BSD-2-Clause
6 */
7 
8 #include "batchextract.h"
9 #include "ark_debug.h"
10 #include "extractiondialog.h"
11 #include "jobs.h"
12 #include "queries.h"
13 
14 #include <KIO/JobTracker>
15 #include <KIO/JobUiDelegate>
16 #include <KIO/OpenUrlJob>
17 #include <KLocalizedString>
18 #include <KMessageBox>
19 #include <KWidgetJobTracker>
20 
21 #include <QDir>
22 #include <QFileInfo>
23 #include <QPointer>
24 #include <QTimer>
25 
BatchExtract(QObject * parent)26 BatchExtract::BatchExtract(QObject* parent)
27     : KCompositeJob(parent),
28       m_autoSubfolder(false),
29       m_preservePaths(true),
30       m_openDestinationAfterExtraction(false)
31 {
32     setCapabilities(KJob::Killable);
33 
34     connect(this, &KJob::result, this, &BatchExtract::showFailedFiles);
35 }
36 
~BatchExtract()37 BatchExtract::~BatchExtract()
38 {
39 }
40 
addExtraction(const QUrl & url)41 void BatchExtract::addExtraction(const QUrl& url)
42 {
43     QString destination = destinationFolder();
44 
45     auto job = Kerfuffle::Archive::batchExtract(url.toLocalFile(), destination, autoSubfolder(), preservePaths());
46 
47     qCDebug(ARK) << QString(QStringLiteral("Registering job from archive %1, to %2, preservePaths %3")).arg(url.toLocalFile(), destination, QString::number(preservePaths()));
48 
49     addSubjob(job);
50 
51     m_fileNames[job] = qMakePair(url.toLocalFile(), destination);
52 
53     connect(job, &KJob::percentChanged, this, &BatchExtract::forwardProgress);
54 
55     connect(job, &Kerfuffle::BatchExtractJob::userQuery,
56             this, &BatchExtract::slotUserQuery);
57 }
58 
doKill()59 bool BatchExtract::doKill()
60 {
61     if (subjobs().isEmpty()) {
62         return false;
63     }
64 
65     return subjobs().first()->kill();
66 }
67 
slotUserQuery(Kerfuffle::Query * query)68 void BatchExtract::slotUserQuery(Kerfuffle::Query *query)
69 {
70     query->execute();
71 }
72 
autoSubfolder() const73 bool BatchExtract::autoSubfolder() const
74 {
75     return m_autoSubfolder;
76 }
77 
setAutoSubfolder(bool value)78 void BatchExtract::setAutoSubfolder(bool value)
79 {
80     m_autoSubfolder = value;
81 }
82 
start()83 void BatchExtract::start()
84 {
85     QTimer::singleShot(0, this, &BatchExtract::slotStartJob);
86 }
87 
slotStartJob()88 void BatchExtract::slotStartJob()
89 {
90     if (m_inputs.isEmpty()) {
91         emitResult();
92         return;
93     }
94 
95     for (const auto& url : std::as_const(m_inputs)) {
96         addExtraction(url);
97     }
98 
99     KIO::getJobTracker()->registerJob(this);
100 
101     Q_EMIT description(this,
102                      i18n("Extracting Files"),
103                      qMakePair(i18n("Source archive"), m_fileNames.value(subjobs().at(0)).first),
104                      qMakePair(i18n("Destination"), m_fileNames.value(subjobs().at(0)).second)
105                     );
106 
107     m_initialJobCount = subjobs().size();
108 
109     qCDebug(ARK) << "Starting first job";
110 
111     subjobs().at(0)->start();
112 }
113 
showFailedFiles()114 void BatchExtract::showFailedFiles()
115 {
116     if (!m_failedFiles.isEmpty()) {
117         KMessageBox::informationList(nullptr, i18n("The following files could not be extracted:"), m_failedFiles);
118     }
119 }
120 
slotResult(KJob * job)121 void BatchExtract::slotResult(KJob *job)
122 {
123     if (job->error()) {
124         qCDebug(ARK) << "There was en error:" << job->error() << ", errorText:" << job->errorString();
125 
126         setErrorText(job->errorString());
127         setError(job->error());
128 
129         removeSubjob(job);
130 
131         if (job->error() != KJob::KilledJobError) {
132             const QString filename = m_fileNames.value(job).first;
133             if (hasSubjobs()) {
134                 KMessageBox::error(nullptr,
135                                    job->errorString().isEmpty() ?
136                                    xi18nc("@info", "There was an error while extracting <filename>%1</filename>. Any further archive will not be extracted.", filename) :
137                                    xi18nc("@info", "There was an error while extracting <filename>%1</filename>:<nl/><message>%2</message><nl/>Any further archive will not be extracted.", filename, job->errorString()));
138             } else {
139                 KMessageBox::error(nullptr,
140                                    job->errorString().isEmpty() ?
141                                    xi18nc("@info", "There was an error while extracting <filename>%1</filename>.", filename) :
142                                    xi18nc("@info", "There was an error while extracting <filename>%1</filename>:<nl/><message>%2</message>", filename, job->errorString()));
143             }
144         }
145 
146         emitResult();
147         return;
148     }
149 
150     removeSubjob(job);
151 
152     if (!hasSubjobs()) {
153         if (openDestinationAfterExtraction()) {
154             const QString path = QDir::cleanPath(destinationFolder());
155             const QUrl destination(QUrl::fromLocalFile(path));
156             KIO::OpenUrlJob *job = new KIO::OpenUrlJob(destination, QStringLiteral("inode/directory"));
157             job->setUiDelegate(new KIO::JobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, nullptr));
158             job->start();
159         }
160 
161         qCDebug(ARK) << "Finished, emitting the result";
162         emitResult();
163     } else {
164         qCDebug(ARK) << "Starting the next job";
165         Q_EMIT description(this,
166                          i18n("Extracting Files"),
167                          qMakePair(i18n("Source archive"), m_fileNames.value(subjobs().at(0)).first),
168                          qMakePair(i18n("Destination"), m_fileNames.value(subjobs().at(0)).second)
169                         );
170         subjobs().at(0)->start();
171     }
172 }
173 
forwardProgress(KJob * job,unsigned long percent)174 void BatchExtract::forwardProgress(KJob *job, unsigned long percent)
175 {
176     Q_UNUSED(job)
177     auto jobPart = static_cast<ulong>(100 / m_initialJobCount);
178     auto remainingJobs = static_cast<ulong>(m_initialJobCount - subjobs().size());
179     setPercent(jobPart * remainingJobs + percent / static_cast<ulong>(m_initialJobCount));
180 }
181 
addInput(const QUrl & url)182 void BatchExtract::addInput(const QUrl& url)
183 {
184     qCDebug(ARK) << "Adding archive" << url.toLocalFile();
185 
186     if (!QFileInfo::exists(url.toLocalFile())) {
187         m_failedFiles.append(url.fileName());
188         return;
189     }
190 
191     m_inputs.append(url);
192 }
193 
openDestinationAfterExtraction() const194 bool BatchExtract::openDestinationAfterExtraction() const
195 {
196     return m_openDestinationAfterExtraction;
197 }
198 
preservePaths() const199 bool BatchExtract::preservePaths() const
200 {
201     return m_preservePaths;
202 }
203 
destinationFolder() const204 QString BatchExtract::destinationFolder() const
205 {
206     if (m_destinationFolder.isEmpty()) {
207         return QDir::currentPath();
208     } else {
209         return m_destinationFolder;
210     }
211 }
212 
setDestinationFolder(const QString & folder)213 void BatchExtract::setDestinationFolder(const QString& folder)
214 {
215     if (QFileInfo(folder).isDir()) {
216         m_destinationFolder = folder;
217         // Magic property that tells the job tracker the job's destination
218         setProperty("destUrl", QUrl::fromLocalFile(folder).toString());
219     }
220 }
221 
setOpenDestinationAfterExtraction(bool value)222 void BatchExtract::setOpenDestinationAfterExtraction(bool value)
223 {
224     m_openDestinationAfterExtraction = value;
225 }
226 
setPreservePaths(bool value)227 void BatchExtract::setPreservePaths(bool value)
228 {
229     m_preservePaths = value;
230 }
231 
showExtractDialog()232 bool BatchExtract::showExtractDialog()
233 {
234     QPointer<Kerfuffle::ExtractionDialog> dialog =
235         new Kerfuffle::ExtractionDialog;
236 
237     if (m_inputs.size() > 1) {
238         dialog.data()->batchModeOption();
239     }
240 
241     dialog.data()->setModal(true);
242     dialog.data()->setAutoSubfolder(autoSubfolder());
243     dialog.data()->setCurrentUrl(QUrl::fromUserInput(destinationFolder(), QString(), QUrl::AssumeLocalFile));
244     dialog.data()->setPreservePaths(preservePaths());
245 
246     // Only one archive, we need a LoadJob to get the single-folder and subfolder properties.
247     // TODO: find a better way (e.g. let the dialog handle everything), otherwise we list
248     // the archive twice (once here and once in the following BatchExtractJob).
249     Kerfuffle::LoadJob *loadJob = nullptr;
250     if (m_inputs.size() == 1) {
251         loadJob = Kerfuffle::Archive::load(m_inputs.at(0).toLocalFile(), this);
252         // We need to access the job after result has been emitted, if the user rejects the dialog.
253         loadJob->setAutoDelete(false);
254 
255         connect(loadJob, &KJob::result, this, [=](KJob *job) {
256             if (job->error()) {
257                 return;
258             }
259 
260             auto archive = qobject_cast<Kerfuffle::LoadJob*>(job)->archive();
261             dialog->setExtractToSubfolder(archive->hasMultipleTopLevelEntries());
262             dialog->setSubfolder(archive->subfolderName());
263         });
264 
265         connect(loadJob, &KJob::result, dialog.data(), &Kerfuffle::ExtractionDialog::setReadyGui);
266         dialog->setBusyGui();
267         // NOTE: we exploit the dialog->exec() below to run this job.
268         loadJob->start();
269     }
270 
271     QUrl destinationDirectory;
272     if (dialog.data()->exec()) {
273         destinationDirectory = dialog.data()->destinationDirectory();
274         if (destinationDirectory.isLocalFile()) {
275             setAutoSubfolder(false && dialog.data()->autoSubfolders());
276             setDestinationFolder(destinationDirectory.toLocalFile());
277             setOpenDestinationAfterExtraction(dialog.data()->openDestinationAfterExtraction());
278             setPreservePaths(dialog.data()->preservePaths());
279 
280             delete dialog.data();
281             return true;
282         }
283     }
284 
285     // we get here if no directory was chosen, or the chosen directory is not local
286     if (loadJob) {
287         loadJob->kill();
288         loadJob->deleteLater();
289     }
290 
291     if (!destinationDirectory.isEmpty() && !destinationDirectory.isLocalFile()) {
292         KMessageBox::error(nullptr,
293                             xi18nc("@info", "The archive could not be extracted to <filename>%1</filename> because Ark can only extract to local destinations.", destinationDirectory.toDisplayString()));
294     }
295 
296     delete dialog.data();
297 
298     return false;
299 }
300 
301