File indexing completed on 2024-05-05 05:50:41
0001 /* 0002 SPDX-FileCopyrightText: 2007 Henrique Pinto <henrique.pinto@kdemail.net> 0003 SPDX-FileCopyrightText: 2008-2009 Harald Hvaal <haraldhv@stud.ntnu.no> 0004 SPDX-FileCopyrightText: 2009-2012 Raphael Kubo da Costa <rakuco@FreeBSD.org> 0005 SPDX-FileCopyrightText: 2016 Vladyslav Batyrenko <mvlabat@gmail.com> 0006 0007 SPDX-License-Identifier: BSD-2-Clause 0008 */ 0009 0010 #include "jobs.h" 0011 #include "ark_debug.h" 0012 0013 #include <QDir> 0014 #include <QDirIterator> 0015 #include <QFileInfo> 0016 #include <QStorageInfo> 0017 #include <QThread> 0018 #include <QTimer> 0019 #include <QUrl> 0020 0021 #include <KFileUtils> 0022 #include <KLocalizedString> 0023 0024 namespace Kerfuffle 0025 { 0026 class Job::Private : public QThread 0027 { 0028 Q_OBJECT 0029 0030 public: 0031 Private(Job *job, QObject *parent = nullptr) 0032 : QThread(parent) 0033 , q(job) 0034 { 0035 } 0036 0037 void run() override; 0038 0039 private: 0040 Job *q; 0041 }; 0042 0043 void Job::Private::run() 0044 { 0045 q->doWork(); 0046 } 0047 0048 Job::Job(Archive *archive, ReadOnlyArchiveInterface *interface) 0049 : KJob() 0050 , m_archive(archive) 0051 , m_archiveInterface(interface) 0052 , d(new Private(this)) 0053 { 0054 setCapabilities(KJob::Killable); 0055 } 0056 0057 Job::Job(Archive *archive) 0058 : Job(archive, nullptr) 0059 { 0060 } 0061 0062 Job::Job(ReadOnlyArchiveInterface *interface) 0063 : Job(nullptr, interface) 0064 { 0065 } 0066 0067 Job::~Job() 0068 { 0069 if (d->isRunning()) { 0070 d->wait(); 0071 } 0072 0073 delete d; 0074 } 0075 0076 ReadOnlyArchiveInterface *Job::archiveInterface() 0077 { 0078 // Use the archive interface. 0079 if (archive()) { 0080 return archive()->interface(); 0081 } 0082 0083 // Use the interface passed to this job (e.g. JSONArchiveInterface in jobstest.cpp). 0084 return m_archiveInterface; 0085 } 0086 0087 Archive *Job::archive() const 0088 { 0089 return m_archive; 0090 } 0091 0092 QString Job::errorString() const 0093 { 0094 if (!errorText().isEmpty()) { 0095 return errorText(); 0096 } 0097 0098 if (archive()) { 0099 if (archive()->error() == NoPlugin) { 0100 return i18n("No suitable plugin found. Ark does not seem to support this file type."); 0101 } 0102 0103 if (archive()->error() == FailedPlugin) { 0104 return i18n("Failed to load a suitable plugin. Make sure any executables needed to handle the archive type are installed."); 0105 } 0106 } 0107 0108 return QString(); 0109 } 0110 0111 void Job::start() 0112 { 0113 jobTimer.start(); 0114 0115 // We have an archive but it's not valid, nothing to do. 0116 if (archive() && !archive()->isValid()) { 0117 QTimer::singleShot(0, this, [=]() { 0118 onFinished(false); 0119 }); 0120 return; 0121 } 0122 0123 if (archiveInterface()->waitForFinishedSignal()) { 0124 // CLI-based interfaces run a QProcess, no need to use threads. 0125 QTimer::singleShot(0, this, &Job::doWork); 0126 } else { 0127 // Run the job in another thread. 0128 d->start(); 0129 } 0130 } 0131 0132 void Job::connectToArchiveInterfaceSignals() 0133 { 0134 connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &Job::onCancelled); 0135 connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &Job::onError); 0136 connect(archiveInterface(), &ReadOnlyArchiveInterface::entry, this, &Job::onEntry); 0137 connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &Job::onProgress); 0138 connect(archiveInterface(), &ReadOnlyArchiveInterface::info, this, &Job::onInfo); 0139 connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished); 0140 connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery); 0141 0142 auto readWriteInterface = qobject_cast<ReadWriteArchiveInterface *>(archiveInterface()); 0143 if (readWriteInterface) { 0144 connect(readWriteInterface, &ReadWriteArchiveInterface::entryRemoved, this, &Job::onEntryRemoved); 0145 } 0146 } 0147 0148 void Job::onCancelled() 0149 { 0150 qCDebug(ARK) << "Cancelled emitted"; 0151 setError(KJob::KilledJobError); 0152 } 0153 0154 void Job::onError(const QString &message, const QString &details, int errorCode) 0155 { 0156 Q_UNUSED(details) 0157 0158 qCDebug(ARK) << "Error emitted:" << errorCode << "-" << message; 0159 setError(errorCode); 0160 setErrorText(message); 0161 } 0162 0163 void Job::onEntry(Archive::Entry *entry) 0164 { 0165 const QString entryFullPath = entry->fullPath(); 0166 const QString cleanEntryFullPath = QDir::cleanPath(entryFullPath); 0167 if (cleanEntryFullPath.startsWith(QLatin1String("../")) || cleanEntryFullPath.contains(QLatin1String("/../"))) { 0168 qCWarning(ARK) << "Possibly malicious archive. Detected entry that could lead to a directory traversal attack:" << entryFullPath; 0169 onError(i18n("Could not load the archive because it contains ill-formed entries and might be a malicious archive."), 0170 QString(), 0171 Kerfuffle::PossiblyMaliciousArchiveError); 0172 onFinished(false); 0173 return; 0174 } 0175 0176 Q_EMIT newEntry(entry); 0177 } 0178 0179 void Job::onProgress(double value) 0180 { 0181 setPercent(static_cast<unsigned long>(100.0 * value)); 0182 } 0183 0184 void Job::onInfo(const QString &info) 0185 { 0186 Q_EMIT infoMessage(this, info); 0187 } 0188 0189 void Job::onEntryRemoved(const QString &path) 0190 { 0191 Q_EMIT entryRemoved(path); 0192 } 0193 0194 void Job::onFinished(bool result) 0195 { 0196 qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms"; 0197 0198 if (archive() && !archive()->isValid()) { 0199 setError(KJob::UserDefinedError); 0200 } 0201 0202 if (!d->isInterruptionRequested()) { 0203 emitResult(); 0204 } 0205 } 0206 0207 void Job::onUserQuery(Query *query) 0208 { 0209 if (archiveInterface()->waitForFinishedSignal()) { 0210 qCWarning(ARK) << "Plugins run from the main thread should call directly query->execute()"; 0211 } 0212 0213 Q_EMIT userQuery(query); 0214 } 0215 0216 bool Job::doKill() 0217 { 0218 const bool killed = archiveInterface()->doKill(); 0219 if (killed) { 0220 return true; 0221 } 0222 0223 if (d->isRunning()) { 0224 qCDebug(ARK) << "Requesting graceful thread interruption, will abort in one second otherwise."; 0225 d->requestInterruption(); 0226 d->wait(1000); 0227 } 0228 0229 return true; 0230 } 0231 0232 LoadJob::LoadJob(Archive *archive, ReadOnlyArchiveInterface *interface) 0233 : Job(archive, interface) 0234 , m_isSingleFolderArchive(true) 0235 , m_isPasswordProtected(false) 0236 , m_extractedFilesSize(0) 0237 , m_dirCount(0) 0238 , m_filesCount(0) 0239 { 0240 // Don't show "finished" notification when finished 0241 setProperty("transientProgressReporting", true); 0242 qCDebug(ARK) << "Created job instance"; 0243 connect(this, &LoadJob::newEntry, this, &LoadJob::onNewEntry); 0244 } 0245 0246 LoadJob::LoadJob(Archive *archive) 0247 : LoadJob(archive, nullptr) 0248 { 0249 } 0250 0251 LoadJob::LoadJob(ReadOnlyArchiveInterface *interface) 0252 : LoadJob(nullptr, interface) 0253 { 0254 } 0255 0256 void LoadJob::doWork() 0257 { 0258 Q_EMIT description(this, i18n("Loading archive"), qMakePair(i18n("Archive"), archiveInterface()->filename())); 0259 connectToArchiveInterfaceSignals(); 0260 0261 bool ret = archiveInterface()->list(); 0262 0263 if (!archiveInterface()->waitForFinishedSignal()) { 0264 // onFinished() needs to be called after onNewEntry(), because the former reads members set in the latter. 0265 // So we need to put it in the event queue, just like the single-thread case does by emitting finished(). 0266 QTimer::singleShot(0, this, [=]() { 0267 onFinished(ret); 0268 }); 0269 } 0270 } 0271 0272 void LoadJob::onFinished(bool result) 0273 { 0274 if (archive() && result) { 0275 archive()->setProperty("unpackedSize", extractedFilesSize()); 0276 archive()->setProperty("isSingleFolder", isSingleFolderArchive()); 0277 const auto name = subfolderName().isEmpty() ? archive()->completeBaseName() : subfolderName(); 0278 archive()->setProperty("subfolderName", name); 0279 if (isPasswordProtected()) { 0280 archive()->setProperty("encryptionType", archive()->password().isEmpty() ? Archive::Encrypted : Archive::HeaderEncrypted); 0281 } 0282 } 0283 0284 Job::onFinished(result); 0285 } 0286 0287 qlonglong LoadJob::extractedFilesSize() const 0288 { 0289 return m_extractedFilesSize; 0290 } 0291 0292 bool LoadJob::isPasswordProtected() const 0293 { 0294 return m_isPasswordProtected; 0295 } 0296 0297 bool LoadJob::isSingleFolderArchive() const 0298 { 0299 if (m_filesCount == 1 && m_dirCount == 0) { 0300 return false; 0301 } 0302 0303 return m_isSingleFolderArchive; 0304 } 0305 0306 void LoadJob::onNewEntry(const Archive::Entry *entry) 0307 { 0308 m_extractedFilesSize += entry->isSparse() ? entry->sparseSize() : entry->property("size").toLongLong(); 0309 m_isPasswordProtected |= entry->property("isPasswordProtected").toBool(); 0310 0311 if (entry->isDir()) { 0312 m_dirCount++; 0313 } else { 0314 m_filesCount++; 0315 } 0316 0317 if (m_isSingleFolderArchive) { 0318 QString fullPath = entry->fullPath(); 0319 // RPM filenames have the ./ prefix, and "." would be detected as the subfolder name, so we remove it. 0320 if (fullPath.startsWith(QLatin1String("./"))) { 0321 fullPath = fullPath.remove(0, 2); 0322 } 0323 0324 const int index = fullPath.indexOf(QLatin1Char('/')); 0325 const QString basePath = fullPath.left(index); 0326 0327 if (m_basePath.isEmpty()) { 0328 m_basePath = basePath; 0329 m_subfolderName = basePath; 0330 } else { 0331 if (m_basePath != basePath) { 0332 m_isSingleFolderArchive = false; 0333 m_subfolderName.clear(); 0334 } 0335 } 0336 } 0337 } 0338 0339 QString LoadJob::subfolderName() const 0340 { 0341 if (!isSingleFolderArchive()) { 0342 return QString(); 0343 } 0344 0345 return m_subfolderName; 0346 } 0347 0348 BatchExtractJob::BatchExtractJob(LoadJob *loadJob, const QString &destination, bool autoSubfolder, bool preservePaths) 0349 : Job(loadJob->archive()) 0350 , m_loadJob(loadJob) 0351 , m_destination(destination) 0352 , m_autoSubfolder(autoSubfolder) 0353 , m_preservePaths(preservePaths) 0354 { 0355 qCDebug(ARK) << "Created job instance"; 0356 } 0357 0358 void BatchExtractJob::doWork() 0359 { 0360 connect(m_loadJob, &KJob::result, this, &BatchExtractJob::slotLoadingFinished); 0361 connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &BatchExtractJob::onCancelled); 0362 0363 if (archiveInterface()->hasBatchExtractionProgress()) { 0364 // progress() will be actually emitted by the LoadJob, but the archiveInterface() is the same. 0365 connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotLoadingProgress); 0366 } 0367 0368 // Forward LoadJob's signals. 0369 connect(m_loadJob, &Kerfuffle::Job::newEntry, this, &BatchExtractJob::newEntry); 0370 connect(m_loadJob, &Kerfuffle::Job::userQuery, this, &BatchExtractJob::userQuery); 0371 m_loadJob->start(); 0372 } 0373 0374 bool BatchExtractJob::doKill() 0375 { 0376 if (m_step == Loading) { 0377 return m_loadJob->kill(); 0378 } 0379 0380 return m_extractJob->kill(); 0381 } 0382 0383 void BatchExtractJob::slotLoadingProgress(double progress) 0384 { 0385 // Progress from LoadJob counts only for 50% of the BatchExtractJob's duration. 0386 m_lastPercentage = static_cast<unsigned long>(50.0 * progress); 0387 setPercent(m_lastPercentage); 0388 } 0389 0390 void BatchExtractJob::slotExtractProgress(double progress) 0391 { 0392 // The 2nd 50% of the BatchExtractJob's duration comes from the ExtractJob. 0393 setPercent(m_lastPercentage + static_cast<unsigned long>(50.0 * progress)); 0394 } 0395 0396 void BatchExtractJob::slotLoadingFinished(KJob *job) 0397 { 0398 if (job->error()) { 0399 // Forward errors as well. 0400 onError(job->errorString(), QString(), job->error()); 0401 onFinished(false); 0402 return; 0403 } 0404 0405 // Block extraction if there's no space on the device. 0406 // Probably we need to take into account a small delta too, 0407 // so, free space + 1% just for the sake of it. 0408 QStorageInfo destinationStorage(m_destination); 0409 if (m_loadJob->extractedFilesSize() * 1.01 > destinationStorage.bytesAvailable()) { 0410 onError(xi18n("No space available on device <filename>%1</filename>", m_destination), QString(), Kerfuffle::DestinationNotWritableError); 0411 onFinished(false); 0412 return; 0413 } 0414 0415 // Now we can start extraction. 0416 setupDestination(); 0417 0418 Kerfuffle::ExtractionOptions options; 0419 options.setPreservePaths(m_preservePaths); 0420 0421 m_extractJob = archive()->extractFiles({}, m_destination, options); 0422 if (m_extractJob) { 0423 connect(m_extractJob, &KJob::result, this, &BatchExtractJob::emitResult); 0424 connect(m_extractJob, &Kerfuffle::Job::userQuery, this, &BatchExtractJob::userQuery); 0425 connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &BatchExtractJob::onError); 0426 if (archiveInterface()->hasBatchExtractionProgress()) { 0427 // The LoadJob is done, change slot and start setting the percentage from m_lastPercentage on. 0428 disconnect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotLoadingProgress); 0429 connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &BatchExtractJob::slotExtractProgress); 0430 } 0431 m_step = Extracting; 0432 m_extractJob->start(); 0433 } else { 0434 emitResult(); 0435 } 0436 } 0437 0438 void BatchExtractJob::setupDestination() 0439 { 0440 const bool isSingleFolderRPM = (archive()->isSingleFolder() && (archive()->mimeType().name() == QLatin1String("application/x-rpm"))); 0441 0442 if (m_autoSubfolder && (archive()->hasMultipleTopLevelEntries() || isSingleFolderRPM)) { 0443 const QDir d(m_destination); 0444 QString subfolderName = archive()->subfolderName(); 0445 0446 // Special case for single folder RPM archives. 0447 // We don't want the autodetected folder to have a meaningless "usr" name. 0448 if (isSingleFolderRPM && subfolderName == QLatin1String("usr")) { 0449 qCDebug(ARK) << "Detected single folder RPM archive. Using archive basename as subfolder name"; 0450 subfolderName = QFileInfo(archive()->fileName()).completeBaseName(); 0451 } 0452 0453 if (d.exists(subfolderName)) { 0454 subfolderName = KFileUtils::suggestName(QUrl::fromUserInput(m_destination, QDir::currentPath(), QUrl::AssumeLocalFile), subfolderName); 0455 } 0456 0457 d.mkdir(subfolderName); 0458 0459 m_destination += QLatin1Char('/') + subfolderName; 0460 } 0461 } 0462 0463 CreateJob::CreateJob(Archive *archive, const QVector<Archive::Entry *> &entries, const CompressionOptions &options) 0464 : Job(archive) 0465 , m_entries(entries) 0466 , m_options(options) 0467 { 0468 qCDebug(ARK) << "Created job instance"; 0469 setTotalAmount(Files, 1); 0470 } 0471 0472 CreateJob::~CreateJob() 0473 { 0474 delete m_addJob; 0475 } 0476 0477 void CreateJob::enableEncryption(const QString &password, bool encryptHeader) 0478 { 0479 archive()->encrypt(password, encryptHeader); 0480 } 0481 0482 void CreateJob::setMultiVolume(bool isMultiVolume) 0483 { 0484 archive()->setMultiVolume(isMultiVolume); 0485 } 0486 0487 void CreateJob::doWork() 0488 { 0489 connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &CreateJob::onProgress); 0490 0491 m_addJob = archive()->addFiles(m_entries, nullptr, m_options); 0492 0493 if (m_addJob) { 0494 connect(m_addJob, &KJob::result, this, &CreateJob::emitResult); 0495 // Forward description signal from AddJob, we need to change the first 0496 // argument ('this' needs to be a CreateJob). 0497 connect(m_addJob, &KJob::description, this, [=](KJob *, const QString &title, const QPair<QString, QString> &field1, const QPair<QString, QString> &) { 0498 Q_EMIT description(this, title, field1); 0499 }); 0500 0501 m_addJob->start(); 0502 } else { 0503 emitResult(); 0504 } 0505 } 0506 0507 bool CreateJob::doKill() 0508 { 0509 bool killed = false; 0510 if (m_addJob) { 0511 killed = m_addJob->kill(); 0512 0513 if (killed) { 0514 // remove leftover archive if needed 0515 auto archiveFile = QFile(archive()->fileName()); 0516 if (archiveFile.exists()) { 0517 archiveFile.remove(); 0518 } 0519 } 0520 } 0521 0522 return killed; 0523 } 0524 0525 ExtractJob::ExtractJob(const QVector<Archive::Entry *> &entries, const QString &destinationDir, ExtractionOptions options, ReadOnlyArchiveInterface *interface) 0526 : Job(interface) 0527 , m_entries(entries) 0528 , m_destinationDir(destinationDir) 0529 , m_options(options) 0530 { 0531 qCDebug(ARK) << "Created job instance"; 0532 // Magic property that tells the job tracker the job's destination 0533 setProperty("destUrl", QUrl::fromLocalFile(destinationDir).toString()); 0534 } 0535 0536 void ExtractJob::doWork() 0537 { 0538 const bool extractingAll = m_entries.empty(); 0539 0540 QString desc; 0541 if (extractingAll) { 0542 desc = i18n("Extracting all files"); 0543 } else { 0544 desc = i18np("Extracting one file", "Extracting %1 files", m_entries.count()); 0545 } 0546 Q_EMIT description(this, 0547 desc, 0548 qMakePair(i18n("Archive"), archiveInterface()->filename()), 0549 qMakePair(i18nc("extraction folder", "Destination"), m_destinationDir)); 0550 0551 QFileInfo destDirInfo(m_destinationDir); 0552 if (destDirInfo.isDir() && (!destDirInfo.isWritable() || !destDirInfo.isExecutable())) { 0553 onError(xi18n("Could not write to destination <filename>%1</filename>.<nl/>Check whether you have sufficient permissions.", m_destinationDir), 0554 QString(), 0555 Kerfuffle::DestinationNotWritableError); 0556 onFinished(false); 0557 return; 0558 } 0559 0560 connectToArchiveInterfaceSignals(); 0561 0562 qCDebug(ARK) << "Starting extraction with" << m_entries.count() << "selected files." << m_entries << "Destination dir:" << m_destinationDir 0563 << "Options:" << m_options; 0564 0565 qulonglong totalUncompressedSize = 0; 0566 if (extractingAll) { 0567 totalUncompressedSize = archiveInterface()->unpackedSize(); 0568 } else { 0569 for (Archive::Entry *entry : qAsConst(m_entries)) { 0570 if (!entry->isDir()) { 0571 totalUncompressedSize += entry->isSparse() ? entry->sparseSize() : entry->size(); 0572 } 0573 } 0574 } 0575 0576 QStorageInfo destinationStorage(m_destinationDir); 0577 0578 if (totalUncompressedSize > static_cast<qulonglong>(destinationStorage.bytesAvailable())) { 0579 onError(xi18n("No space available on device <filename>%1</filename>", m_destinationDir), QString(), Kerfuffle::DestinationNotWritableError); 0580 onFinished(false); 0581 return; 0582 } 0583 0584 bool ret = archiveInterface()->extractFiles(m_entries, m_destinationDir, m_options); 0585 0586 if (!archiveInterface()->waitForFinishedSignal()) { 0587 onFinished(ret); 0588 } 0589 } 0590 0591 QString ExtractJob::destinationDirectory() const 0592 { 0593 return m_destinationDir; 0594 } 0595 0596 ExtractionOptions ExtractJob::extractionOptions() const 0597 { 0598 return m_options; 0599 } 0600 0601 TempExtractJob::TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) 0602 : Job(interface) 0603 , m_entry(entry) 0604 , m_passwordProtectedHint(passwordProtectedHint) 0605 { 0606 m_tmpExtractDir = new QTemporaryDir(); 0607 } 0608 0609 Archive::Entry *TempExtractJob::entry() const 0610 { 0611 return m_entry; 0612 } 0613 0614 QString TempExtractJob::validatedFilePath() const 0615 { 0616 QString path; 0617 // For single-file archives the filepath of the extracted entry is the displayName and not the fullpath. 0618 // TODO: find a better way to handle this. 0619 // Should the ReadOnlyArchiveInterface tell us which is the actual filepath of the entry that it has extracted? 0620 if (m_entry->displayName() != m_entry->name()) { 0621 path = extractionDir() + QLatin1Char('/') + m_entry->displayName(); 0622 } else { 0623 path = extractionDir() + QLatin1Char('/') + m_entry->fullPath(); 0624 } 0625 0626 // Make sure a maliciously crafted archive with parent folders named ".." do 0627 // not cause the previewed file path to be located outside the temporary 0628 // directory, resulting in a directory traversal issue. 0629 path.remove(QStringLiteral("../")); 0630 0631 return path; 0632 } 0633 0634 ExtractionOptions TempExtractJob::extractionOptions() const 0635 { 0636 ExtractionOptions options; 0637 0638 if (m_passwordProtectedHint) { 0639 options.setEncryptedArchiveHint(true); 0640 } 0641 0642 return options; 0643 } 0644 0645 QTemporaryDir *TempExtractJob::tempDir() const 0646 { 0647 return m_tmpExtractDir; 0648 } 0649 0650 void TempExtractJob::doWork() 0651 { 0652 // pass 1 to i18np on purpose so this translation may properly be reused. 0653 Q_EMIT description(this, i18np("Extracting one file", "Extracting %1 files", 1)); 0654 0655 connectToArchiveInterfaceSignals(); 0656 0657 qCDebug(ARK) << "Extracting:" << m_entry; 0658 0659 bool ret = archiveInterface()->extractFiles({m_entry}, extractionDir(), extractionOptions()); 0660 0661 if (!archiveInterface()->waitForFinishedSignal()) { 0662 onFinished(ret); 0663 } 0664 } 0665 0666 QString TempExtractJob::extractionDir() const 0667 { 0668 return m_tmpExtractDir->path(); 0669 } 0670 0671 PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) 0672 : TempExtractJob(entry, passwordProtectedHint, interface) 0673 { 0674 qCDebug(ARK) << "Created job instance"; 0675 } 0676 0677 OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) 0678 : TempExtractJob(entry, passwordProtectedHint, interface) 0679 { 0680 qCDebug(ARK) << "Created job instance"; 0681 } 0682 0683 OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) 0684 : OpenJob(entry, passwordProtectedHint, interface) 0685 { 0686 qCDebug(ARK) << "Created job instance"; 0687 } 0688 0689 AddJob::AddJob(const QVector<Archive::Entry *> &entries, 0690 const Archive::Entry *destination, 0691 const CompressionOptions &options, 0692 ReadWriteArchiveInterface *interface) 0693 : Job(interface) 0694 , m_entries(entries) 0695 , m_destination(destination) 0696 , m_options(options) 0697 { 0698 qCDebug(ARK) << "Created job instance"; 0699 } 0700 0701 void AddJob::doWork() 0702 { 0703 // Set current dir. 0704 const QString globalWorkDir = m_options.globalWorkDir(); 0705 const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir); 0706 if (!globalWorkDir.isEmpty()) { 0707 qCDebug(ARK) << "GlobalWorkDir is set, changing dir to " << globalWorkDir; 0708 m_oldWorkingDir = QDir::currentPath(); 0709 QDir::setCurrent(globalWorkDir); 0710 } 0711 0712 // Count total number of entries to be added. 0713 uint totalCount = 0; 0714 QElapsedTimer timer; 0715 timer.start(); 0716 for (const Archive::Entry *entry : std::as_const(m_entries)) { 0717 totalCount++; 0718 if (QFileInfo(entry->fullPath()).isDir()) { 0719 QDirIterator it(entry->fullPath(), QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); 0720 while (it.hasNext()) { 0721 it.next(); 0722 totalCount++; 0723 } 0724 } 0725 } 0726 0727 qCDebug(ARK) << "Going to add" << totalCount << "entries, counted in" << timer.elapsed() << "ms"; 0728 0729 const QString desc = i18np("Compressing a file", "Compressing %1 files", totalCount); 0730 Q_EMIT description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); 0731 0732 ReadWriteArchiveInterface *m_writeInterface = qobject_cast<ReadWriteArchiveInterface *>(archiveInterface()); 0733 0734 Q_ASSERT(m_writeInterface); 0735 0736 // The file paths must be relative to GlobalWorkDir. 0737 for (Archive::Entry *entry : std::as_const(m_entries)) { 0738 // #191821: workDir must be used instead of QDir::current() 0739 // so that symlinks aren't resolved automatically 0740 const QString &fullPath = entry->fullPath(); 0741 QString relativePath = workDir.relativeFilePath(fullPath); 0742 0743 if (fullPath.endsWith(QLatin1Char('/'))) { 0744 relativePath += QLatin1Char('/'); 0745 } 0746 0747 entry->setFullPath(relativePath); 0748 } 0749 0750 connectToArchiveInterfaceSignals(); 0751 bool ret = m_writeInterface->addFiles(m_entries, m_destination, m_options, totalCount); 0752 0753 if (!archiveInterface()->waitForFinishedSignal()) { 0754 onFinished(ret); 0755 } 0756 } 0757 0758 void AddJob::onFinished(bool result) 0759 { 0760 if (!m_oldWorkingDir.isEmpty()) { 0761 QDir::setCurrent(m_oldWorkingDir); 0762 } 0763 0764 Job::onFinished(result); 0765 } 0766 0767 MoveJob::MoveJob(const QVector<Archive::Entry *> &entries, Archive::Entry *destination, const CompressionOptions &options, ReadWriteArchiveInterface *interface) 0768 : Job(interface) 0769 , m_finishedSignalsCount(0) 0770 , m_entries(entries) 0771 , m_destination(destination) 0772 , m_options(options) 0773 { 0774 qCDebug(ARK) << "Created job instance"; 0775 } 0776 0777 void MoveJob::doWork() 0778 { 0779 qCDebug(ARK) << "Going to move" << m_entries.count() << "file(s)"; 0780 0781 QString desc = i18np("Moving a file", "Moving %1 files", m_entries.count()); 0782 Q_EMIT description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); 0783 0784 ReadWriteArchiveInterface *m_writeInterface = qobject_cast<ReadWriteArchiveInterface *>(archiveInterface()); 0785 0786 Q_ASSERT(m_writeInterface); 0787 0788 connectToArchiveInterfaceSignals(); 0789 bool ret = m_writeInterface->moveFiles(m_entries, m_destination, m_options); 0790 0791 if (!archiveInterface()->waitForFinishedSignal()) { 0792 onFinished(ret); 0793 } 0794 } 0795 0796 void MoveJob::onFinished(bool result) 0797 { 0798 m_finishedSignalsCount++; 0799 if (m_finishedSignalsCount == archiveInterface()->moveRequiredSignals()) { 0800 Job::onFinished(result); 0801 } 0802 } 0803 0804 CopyJob::CopyJob(const QVector<Archive::Entry *> &entries, Archive::Entry *destination, const CompressionOptions &options, ReadWriteArchiveInterface *interface) 0805 : Job(interface) 0806 , m_finishedSignalsCount(0) 0807 , m_entries(entries) 0808 , m_destination(destination) 0809 , m_options(options) 0810 { 0811 qCDebug(ARK) << "Created job instance"; 0812 } 0813 0814 void CopyJob::doWork() 0815 { 0816 qCDebug(ARK) << "Going to copy" << m_entries.count() << "file(s)"; 0817 0818 QString desc = i18np("Copying a file", "Copying %1 files", m_entries.count()); 0819 Q_EMIT description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); 0820 0821 ReadWriteArchiveInterface *m_writeInterface = qobject_cast<ReadWriteArchiveInterface *>(archiveInterface()); 0822 0823 Q_ASSERT(m_writeInterface); 0824 0825 connectToArchiveInterfaceSignals(); 0826 bool ret = m_writeInterface->copyFiles(m_entries, m_destination, m_options); 0827 0828 if (!archiveInterface()->waitForFinishedSignal()) { 0829 onFinished(ret); 0830 } 0831 } 0832 0833 void CopyJob::onFinished(bool result) 0834 { 0835 m_finishedSignalsCount++; 0836 if (m_finishedSignalsCount == archiveInterface()->copyRequiredSignals()) { 0837 Job::onFinished(result); 0838 } 0839 } 0840 0841 DeleteJob::DeleteJob(const QVector<Archive::Entry *> &entries, ReadWriteArchiveInterface *interface) 0842 : Job(interface) 0843 , m_entries(entries) 0844 { 0845 } 0846 0847 void DeleteJob::doWork() 0848 { 0849 QString desc = i18np("Deleting a file from the archive", "Deleting %1 files", m_entries.count()); 0850 Q_EMIT description(this, desc, qMakePair(i18n("Archive"), archiveInterface()->filename())); 0851 0852 ReadWriteArchiveInterface *m_writeInterface = qobject_cast<ReadWriteArchiveInterface *>(archiveInterface()); 0853 0854 Q_ASSERT(m_writeInterface); 0855 0856 connectToArchiveInterfaceSignals(); 0857 bool ret = m_writeInterface->deleteFiles(m_entries); 0858 0859 if (!archiveInterface()->waitForFinishedSignal()) { 0860 onFinished(ret); 0861 } 0862 } 0863 0864 CommentJob::CommentJob(const QString &comment, ReadWriteArchiveInterface *interface) 0865 : Job(interface) 0866 , m_comment(comment) 0867 { 0868 } 0869 0870 void CommentJob::doWork() 0871 { 0872 Q_EMIT description(this, i18n("Adding comment")); 0873 0874 ReadWriteArchiveInterface *m_writeInterface = qobject_cast<ReadWriteArchiveInterface *>(archiveInterface()); 0875 0876 Q_ASSERT(m_writeInterface); 0877 0878 connectToArchiveInterfaceSignals(); 0879 bool ret = m_writeInterface->addComment(m_comment); 0880 0881 if (!archiveInterface()->waitForFinishedSignal()) { 0882 onFinished(ret); 0883 } 0884 } 0885 0886 TestJob::TestJob(ReadOnlyArchiveInterface *interface) 0887 : Job(interface) 0888 { 0889 m_testSuccess = false; 0890 } 0891 0892 void TestJob::doWork() 0893 { 0894 qCDebug(ARK) << "Job started"; 0895 0896 Q_EMIT description(this, i18n("Testing archive"), qMakePair(i18n("Archive"), archiveInterface()->filename())); 0897 0898 connectToArchiveInterfaceSignals(); 0899 connect(archiveInterface(), &ReadOnlyArchiveInterface::testSuccess, this, &TestJob::onTestSuccess); 0900 0901 bool ret = archiveInterface()->testArchive(); 0902 0903 if (!archiveInterface()->waitForFinishedSignal()) { 0904 onFinished(ret); 0905 } 0906 } 0907 0908 void TestJob::onTestSuccess() 0909 { 0910 m_testSuccess = true; 0911 } 0912 0913 bool TestJob::testSucceeded() 0914 { 0915 return m_testSuccess; 0916 } 0917 0918 } // namespace Kerfuffle 0919 0920 #include "jobs.moc" 0921 #include "moc_jobs.cpp"