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"