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