jobs.cpp 17 KB
Newer Older
1 2
/*
 * Copyright (c) 2007 Henrique Pinto <henrique.pinto@kdemail.net>
3
 * Copyright (c) 2008-2009 Harald Hvaal <haraldhv@stud.ntnu.no>
4
 * Copyright (c) 2009-2012 Raphael Kubo da Costa <rakuco@FreeBSD.org>
5
 * Copyright (c) 2016 Vladyslav Batyrenko <mvlabat@gmail.com>
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
28

29
#include "jobs.h"
30
#include "archiveentry.h"
31
#include "ark_debug.h"
32

33
#include <QDir>
34
#include <QDirIterator>
35
#include <QFileInfo>
36
#include <QRegularExpression>
37
#include <QThread>
38
#include <QTimer>
39

40
#include <KLocalizedString>
41

42 43
namespace Kerfuffle
{
44

45 46 47
class Job::Private : public QThread
{
public:
48 49 50 51 52 53
    Private(Job *job, QObject *parent = 0)
        : QThread(parent)
        , q(job)
    {
    }

54
    virtual void run() Q_DECL_OVERRIDE;
55

56
private:
57 58 59 60 61
    Job *q;
};

void Job::Private::run()
{
62
    q->doWork();
63 64
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
65 66
Job::Job(ReadOnlyArchiveInterface *interface)
    : KJob()
67
    , m_archiveInterface(interface)
68
    , d(new Private(this))
69 70 71 72 73 74
{
    setCapabilities(KJob::Killable);
}

Job::~Job()
{
75 76 77
    qDeleteAll(m_archiveEntries);
    m_archiveEntries.clear();

78 79
    if (d->isRunning()) {
        d->wait();
80 81
    }

82
    delete d;
83
}
84

85 86 87 88 89
ReadOnlyArchiveInterface *Job::archiveInterface()
{
    return m_archiveInterface;
}

90 91
void Job::start()
{
92
    jobTimer.start();
93

94
    if (archiveInterface()->waitForFinishedSignal()) {
95 96 97 98 99 100
        // CLI-based interfaces run a QProcess, no need to use threads.
        QTimer::singleShot(0, this, &Job::doWork);
    } else {
        // Run the job in another thread.
        d->start();
    }
101 102
}

103 104
void Job::connectToArchiveInterfaceSignals()
{
Laurent Montel's avatar
Laurent Montel committed
105 106 107 108 109 110
    connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &Job::onCancelled);
    connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &Job::onError);
    connect(archiveInterface(), &ReadOnlyArchiveInterface::entry, this, &Job::onEntry);
    connect(archiveInterface(), &ReadOnlyArchiveInterface::entryRemoved, this, &Job::onEntryRemoved);
    connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &Job::onProgress);
    connect(archiveInterface(), &ReadOnlyArchiveInterface::info, this, &Job::onInfo);
111
    connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished);
Laurent Montel's avatar
Laurent Montel committed
112
    connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery);
113 114
}

115 116
void Job::onCancelled()
{
117
    qCDebug(ARK) << "Cancelled emitted";
118 119 120
    setError(KJob::KilledJobError);
}

121 122
void Job::onError(const QString & message, const QString & details)
{
123
    Q_UNUSED(details)
124

125
    qCDebug(ARK) << "Error emitted:" << message;
126
    setError(KJob::UserDefinedError);
127 128 129
    setErrorText(message);
}

130
void Job::onEntry(Archive::Entry *entry)
131
{
132
    emit newEntry(entry);
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
}

void Job::onProgress(double value)
{
    setPercent(static_cast<unsigned long>(100.0*value));
}

void Job::onInfo(const QString& info)
{
    emit infoMessage(this, info);
}

void Job::onEntryRemoved(const QString & path)
{
    emit entryRemoved(path);
}

void Job::onFinished(bool result)
{
152
    qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms";
153 154 155 156 157 158 159 160 161 162 163

    emitResult();
}

void Job::onUserQuery(Query *query)
{
    emit userQuery(query);
}

bool Job::doKill()
{
164 165 166 167 168
    if (d->isRunning()) {
        d->requestInterruption();
        d->wait();
    }

169
    bool ret = archiveInterface()->doKill();
170
    if (!ret) {
171
        qCWarning(ARK) << "Killing does not seem to be supported here.";
172
    }
173 174 175
    return ret;
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
176 177
ListJob::ListJob(ReadOnlyArchiveInterface *interface)
    : Job(interface)
178 179 180
    , m_isSingleFolderArchive(true)
    , m_isPasswordProtected(false)
    , m_extractedFilesSize(0)
181 182
    , m_dirCount(0)
    , m_filesCount(0)
183
{
184
    qCDebug(ARK) << "ListJob started";
Laurent Montel's avatar
Laurent Montel committed
185
    connect(this, &ListJob::newEntry, this, &ListJob::onNewEntry);
186 187 188 189 190
}

void ListJob::doWork()
{
    emit description(this, i18n("Loading archive..."));
191
    connectToArchiveInterfaceSignals();
192
    bool ret = archiveInterface()->list();
193

194
    if (!archiveInterface()->waitForFinishedSignal()) {
195
        onFinished(ret);
196
    }
197 198
}

199
qlonglong ListJob::extractedFilesSize() const
200 201 202 203
{
    return m_extractedFilesSize;
}

204
bool ListJob::isPasswordProtected() const
205 206 207 208
{
    return m_isPasswordProtected;
}

209
bool ListJob::isSingleFolderArchive() const
210
{
211 212 213 214
    if (m_filesCount == 1 && m_dirCount == 0) {
        return false;
    }

215 216 217
    return m_isSingleFolderArchive;
}

218
void ListJob::onNewEntry(const Archive::Entry *entry)
219
{
220 221
    m_extractedFilesSize += entry->property("size").toLongLong();
    m_isPasswordProtected |= entry->property("isPasswordProtected").toBool();
222

223
    if (entry->isDir()) {
224 225 226 227 228
        m_dirCount++;
    } else {
        m_filesCount++;
    }

229
    if (m_isSingleFolderArchive) {
230
        // RPM filenames have the ./ prefix, and "." would be detected as the subfolder name, so we remove it.
231
        const QString fullPath = entry->fullPath().replace(QRegularExpression(QStringLiteral("^\\./")), QString());
232
        const QString basePath = fullPath.split(QLatin1Char('/')).at(0);
233

234 235 236
        if (m_basePath.isEmpty()) {
            m_basePath = basePath;
            m_subfolderName = basePath;
237
        } else {
238 239 240 241
            if (m_basePath != basePath) {
                m_isSingleFolderArchive = false;
                m_subfolderName.clear();
            }
242 243 244 245
        }
    }
}

246
QString ListJob::subfolderName() const
247
{
248 249 250 251
    if (!isSingleFolderArchive()) {
        return QString();
    }

252 253 254
    return m_subfolderName;
}

255
ExtractJob::ExtractJob(const QList<Archive::Entry*> &entries, const QString &destinationDir, const ExtractionOptions &options, ReadOnlyArchiveInterface *interface)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
256
    : Job(interface)
257
    , m_entries(entries)
258 259
    , m_destinationDir(destinationDir)
    , m_options(options)
260
{
261
    qCDebug(ARK) << "ExtractJob created";
262
    setDefaultOptions();
263 264 265 266 267
}

void ExtractJob::doWork()
{
    QString desc;
268
    if (m_entries.count() == 0) {
269 270
        desc = i18n("Extracting all files");
    } else {
271
        desc = i18np("Extracting one file", "Extracting %1 files", m_entries.count());
272 273 274
    }
    emit description(this, desc);

275 276 277 278 279 280 281
    QFileInfo destDirInfo(m_destinationDir);
    if (destDirInfo.isDir() && (!destDirInfo.isWritable() || !destDirInfo.isExecutable())) {
        onError(xi18n("Could not write to destination <filename>%1</filename>.<nl/>Check whether you have sufficient permissions.", m_destinationDir), QString());
        onFinished(false);
        return;
    }

282
    connectToArchiveInterfaceSignals();
283

284
    qCDebug(ARK) << "Starting extraction with" << m_entries.count() << "selected files."
285
             << m_entries
286 287
             << "Destination dir:" << m_destinationDir
             << "Options:" << m_options;
288

289
    bool ret = archiveInterface()->extractFiles(m_entries, m_destinationDir, m_options);
290

291
    if (!archiveInterface()->waitForFinishedSignal()) {
292
        onFinished(ret);
293
    }
294 295
}

296
void ExtractJob::setDefaultOptions()
297
{
298 299
    ExtractionOptions defaultOptions;

300
    defaultOptions[QStringLiteral("PreservePaths")] = false;
301 302 303 304 305 306

    ExtractionOptions::const_iterator it = defaultOptions.constBegin();
    for (; it != defaultOptions.constEnd(); ++it) {
        if (!m_options.contains(it.key())) {
            m_options[it.key()] = it.value();
        }
307 308 309
    }
}

310 311 312 313 314
QString ExtractJob::destinationDirectory() const
{
    return m_destinationDir;
}

315 316 317 318 319
ExtractionOptions ExtractJob::extractionOptions() const
{
    return m_options;
}

320
TempExtractJob::TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
321
    : Job(interface)
322
    , m_entry(entry)
323 324
    , m_passwordProtectedHint(passwordProtectedHint)
{
325
    m_tmpExtractDir = new QTemporaryDir();
326 327 328 329
}

QString TempExtractJob::validatedFilePath() const
{
330
    QString path = extractionDir() + QLatin1Char('/') + m_entry->fullPath();
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351

    // Make sure a maliciously crafted archive with parent folders named ".." do
    // not cause the previewed file path to be located outside the temporary
    // directory, resulting in a directory traversal issue.
    path.remove(QStringLiteral("../"));

    return path;
}

ExtractionOptions TempExtractJob::extractionOptions() const
{
    ExtractionOptions options;
    options[QStringLiteral("PreservePaths")] = true;

    if (m_passwordProtectedHint) {
        options[QStringLiteral("PasswordProtectedHint")] = true;
    }

    return options;
}

352 353 354 355 356
QTemporaryDir *TempExtractJob::tempDir() const
{
    return m_tmpExtractDir;
}

357 358 359 360 361 362
void TempExtractJob::doWork()
{
    emit description(this, i18n("Extracting one file"));

    connectToArchiveInterfaceSignals();

363
    qCDebug(ARK) << "Extracting:" << m_entry;
364

365
    bool ret = archiveInterface()->extractFiles({m_entry}, extractionDir(), extractionOptions());
366 367 368 369 370 371

    if (!archiveInterface()->waitForFinishedSignal()) {
        onFinished(ret);
    }
}

372
QString TempExtractJob::extractionDir() const
373
{
374
    return m_tmpExtractDir->path();
375 376
}

377 378
PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
    : TempExtractJob(entry, passwordProtectedHint, interface)
379
{
380
    qCDebug(ARK) << "PreviewJob started";
381 382
}

383 384
OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
    : TempExtractJob(entry, passwordProtectedHint, interface)
385 386 387 388
{
    qCDebug(ARK) << "OpenJob started";
}

389 390
OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
    : OpenJob(entry, passwordProtectedHint, interface)
391 392 393 394
{
    qCDebug(ARK) << "OpenWithJob started";
}

395
AddJob::AddJob(const QList<Archive::Entry*> &entries, const Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
396
    : Job(interface)
397
    , m_entries(entries)
398
    , m_destination(destination)
399
    , m_options(options)
400
{
401
    qCDebug(ARK) << "AddJob started";
402 403 404 405
}

void AddJob::doWork()
{
406
    // Set current dir.
407 408 409 410 411 412 413 414
    const QString globalWorkDir = m_options.value(QStringLiteral("GlobalWorkDir")).toString();
    const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir);
    if (!globalWorkDir.isEmpty()) {
        qCDebug(ARK) << "GlobalWorkDir is set, changing dir to " << globalWorkDir;
        m_oldWorkingDir = QDir::currentPath();
        QDir::setCurrent(globalWorkDir);
    }

415 416 417 418
    // Count total number of entries to be added.
    qulonglong totalCount = 0;
    QElapsedTimer timer;
    timer.start();
419
    foreach (const Archive::Entry* entry, m_entries) {
420
        totalCount++;
421
        if (QFileInfo(entry->fullPath()).isDir()) {
422
            QDirIterator it(entry->fullPath(), QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
            while (it.hasNext()) {
                it.next();
                totalCount++;
            }
        }
    }
    qCDebug(ARK) << "Counted" << totalCount << "entries in" << timer.elapsed() << "ms";

    m_options[QStringLiteral("NumberOfEntries")] = totalCount;

    qCDebug(ARK) << "AddJob: going to add" << totalCount << "entries";
    emit description(this, i18np("Adding a file", "Adding %1 files", totalCount));

    ReadWriteArchiveInterface *m_writeInterface =
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());

    Q_ASSERT(m_writeInterface);

441
    // The file paths must be relative to GlobalWorkDir.
442
    foreach (Archive::Entry *entry, m_entries) {
443 444
        // #191821: workDir must be used instead of QDir::current()
        //          so that symlinks aren't resolved automatically
445
        const QString &fullPath = entry->fullPath();
446
        QString relativePath = workDir.relativeFilePath(fullPath);
447

448
        if (fullPath.endsWith(QLatin1Char('/'))) {
449 450 451
            relativePath += QLatin1Char('/');
        }

452
        entry->setFullPath(relativePath);
453 454
    }

455
    connectToArchiveInterfaceSignals();
456
    bool ret = m_writeInterface->addFiles(m_entries, m_destination, m_options);
457

458
    if (!archiveInterface()->waitForFinishedSignal()) {
459
        onFinished(ret);
460
    }
461 462
}

463 464 465 466 467 468 469 470 471
void AddJob::onFinished(bool result)
{
    if (!m_oldWorkingDir.isEmpty()) {
        QDir::setCurrent(m_oldWorkingDir);
    }

    Job::onFinished(result);
}

472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546
MoveJob::MoveJob(const QList<Archive::Entry*> &entries, Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface)
    : Job(interface)
    , m_finishedSignalsCount(0)
    , m_entries(entries)
    , m_destination(destination)
    , m_options(options)
{
    qCDebug(ARK) << "MoveJob started";
}

void MoveJob::doWork()
{
    qCDebug(ARK) << "MoveJob: going to move" << m_entries.count() << "file(s)";

    emit description(this, i18np("Moving a file", "Moving %1 files", m_entries.count()));

    ReadWriteArchiveInterface *m_writeInterface =
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());

    Q_ASSERT(m_writeInterface);

    connectToArchiveInterfaceSignals();
    bool ret = m_writeInterface->moveFiles(m_entries, m_destination, m_options);

    if (!archiveInterface()->waitForFinishedSignal()) {
        onFinished(ret);
    }
}

void MoveJob::onFinished(bool result)
{
    m_finishedSignalsCount++;
    if (m_finishedSignalsCount == archiveInterface()->moveRequiredSignals()) {
        Job::onFinished(result);
    }
}

CopyJob::CopyJob(const QList<Archive::Entry*> &entries, Archive::Entry *destination, const CompressionOptions &options, ReadWriteArchiveInterface *interface)
    : Job(interface)
    , m_finishedSignalsCount(0)
    , m_entries(entries)
    , m_destination(destination)
    , m_options(options)
{
    qCDebug(ARK) << "CopyJob started";
}

void CopyJob::doWork()
{
    qCDebug(ARK) << "CopyJob: going to copy" << m_entries.count() << "file(s)";

    emit description(this, i18np("Copying a file", "Copying %1 files", m_entries.count()));

    ReadWriteArchiveInterface *m_writeInterface =
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());

    Q_ASSERT(m_writeInterface);

    connectToArchiveInterfaceSignals();
    bool ret = m_writeInterface->copyFiles(m_entries, m_destination, m_options);

    if (!archiveInterface()->waitForFinishedSignal()) {
        onFinished(ret);
    }
}

void CopyJob::onFinished(bool result)
{
    m_finishedSignalsCount++;
    if (m_finishedSignalsCount == archiveInterface()->copyRequiredSignals()) {
        Job::onFinished(result);
    }
}

DeleteJob::DeleteJob(const QList<Archive::Entry*> &entries, ReadWriteArchiveInterface *interface)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
547
    : Job(interface)
548
    , m_entries(entries)
549 550 551 552 553
{
}

void DeleteJob::doWork()
{
554
    emit description(this, i18np("Deleting a file from the archive", "Deleting %1 files", m_entries.count()));
555 556

    ReadWriteArchiveInterface *m_writeInterface =
557
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());
558 559 560

    Q_ASSERT(m_writeInterface);

561
    connectToArchiveInterfaceSignals();
562
    bool ret = m_writeInterface->deleteFiles(m_entries);
563

564
    if (!archiveInterface()->waitForFinishedSignal()) {
565
        onFinished(ret);
566
    }
567
}
568

Elvis Angelaccio's avatar
Elvis Angelaccio committed
569 570
CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface)
    : Job(interface)
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591
    , m_comment(comment)
{
}

void CommentJob::doWork()
{
    emit description(this, i18n("Adding comment"));

    ReadWriteArchiveInterface *m_writeInterface =
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());

    Q_ASSERT(m_writeInterface);

    connectToArchiveInterfaceSignals();
    bool ret = m_writeInterface->addComment(m_comment);

    if (!archiveInterface()->waitForFinishedSignal()) {
        onFinished(ret);
    }
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
592 593
TestJob::TestJob(ReadOnlyArchiveInterface *interface)
    : Job(interface)
Ragnar Thomsen's avatar
Ragnar Thomsen committed
594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
{
    m_testSuccess = false;
}

void TestJob::doWork()
{
    qCDebug(ARK) << "TestJob started";

    emit description(this, i18n("Testing archive"));
    connectToArchiveInterfaceSignals();
    connect(archiveInterface(), &ReadOnlyArchiveInterface::testSuccess, this, &TestJob::onTestSuccess);

    bool ret = archiveInterface()->testArchive();

    if (!archiveInterface()->waitForFinishedSignal()) {
        onFinished(ret);
    }
}

void TestJob::onTestSuccess()
{
    m_testSuccess = true;
}

bool TestJob::testSucceeded()
{
    return m_testSuccess;
}

623
} // namespace Kerfuffle
624

625