jobs.cpp 16.8 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 <QFileInfo>
35
#include <QRegularExpression>
36
#include <QThread>
37
#include <QTimer>
38

39
#include <KLocalizedString>
40

41 42
//#define DEBUG_RACECONDITION

43 44
namespace Kerfuffle
{
45

46 47 48
class Job::Private : public QThread
{
public:
49 50 51 52
    Private(Job *job, QObject *parent = 0)
        : QThread(parent)
        , q(job)
    {
Laurent Montel's avatar
Laurent Montel committed
53
        connect(q, &KJob::result, this, &QThread::quit);
54 55
    }

56
    virtual void run() Q_DECL_OVERRIDE;
57

58
private:
59 60 61 62 63
    Job *q;
};

void Job::Private::run()
{
64
    q->doWork();
65

66 67 68
    if (q->isRunning()) {
        exec();
    }
69 70 71 72 73 74

#ifdef DEBUG_RACECONDITION
    QThread::sleep(2);
#endif
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
75 76
Job::Job(ReadOnlyArchiveInterface *interface)
    : KJob()
77
    , m_archiveInterface(interface)
78 79
    , m_isRunning(false)
    , d(new Private(this))
80 81 82 83 84 85 86 87 88 89 90 91
{
    static bool onlyOnce = false;
    if (!onlyOnce) {
        qRegisterMetaType<QPair<QString, QString> >("QPair<QString,QString>");
        onlyOnce = true;
    }

    setCapabilities(KJob::Killable);
}

Job::~Job()
{
92 93 94
    qDeleteAll(m_archiveEntries);
    m_archiveEntries.clear();

95 96
    if (d->isRunning()) {
        d->wait();
97 98
    }

99
    delete d;
100
}
101

102 103 104 105 106
ReadOnlyArchiveInterface *Job::archiveInterface()
{
    return m_archiveInterface;
}

107 108 109 110 111
bool Job::isRunning() const
{
    return m_isRunning;
}

112 113
void Job::start()
{
114
    jobTimer.start();
115
    m_isRunning = true;
116

117
    if (archiveInterface()->waitForFinishedSignal()) {
118 119 120 121 122 123
        // 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();
    }
124 125
}

126 127 128 129 130 131
void Job::emitResult()
{
    m_isRunning = false;
    KJob::emitResult();
}

132 133
void Job::connectToArchiveInterfaceSignals()
{
Laurent Montel's avatar
Laurent Montel committed
134 135 136 137 138 139 140 141
    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);
    connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished, Qt::DirectConnection);
    connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery);
142 143
}

144 145
void Job::onCancelled()
{
146
    qCDebug(ARK) << "Cancelled emitted";
147 148 149
    setError(KJob::KilledJobError);
}

150 151
void Job::onError(const QString & message, const QString & details)
{
152
    Q_UNUSED(details)
153

154
    qCDebug(ARK) << "Error emitted:" << message;
155
    setError(KJob::UserDefinedError);
156 157 158
    setErrorText(message);
}

159
void Job::onEntry(Archive::Entry *entry)
160
{
161
    emit newEntry(entry);
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
}

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)
{
181
    qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms";
182 183 184 185 186 187 188 189 190 191 192

    emitResult();
}

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

bool Job::doKill()
{
193
    bool ret = archiveInterface()->doKill();
194
    if (!ret) {
195
        qCWarning(ARK) << "Killing does not seem to be supported here.";
196
    }
197 198 199
    return ret;
}

Elvis Angelaccio's avatar
Elvis Angelaccio committed
200 201
ListJob::ListJob(ReadOnlyArchiveInterface *interface)
    : Job(interface)
202 203 204
    , m_isSingleFolderArchive(true)
    , m_isPasswordProtected(false)
    , m_extractedFilesSize(0)
205 206
    , m_dirCount(0)
    , m_filesCount(0)
207
{
208
    qCDebug(ARK) << "ListJob started";
Laurent Montel's avatar
Laurent Montel committed
209
    connect(this, &ListJob::newEntry, this, &ListJob::onNewEntry);
210 211 212 213 214
}

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

218
    if (!archiveInterface()->waitForFinishedSignal()) {
219
        onFinished(ret);
220
    }
221 222
}

223
qlonglong ListJob::extractedFilesSize() const
224 225 226 227
{
    return m_extractedFilesSize;
}

228
bool ListJob::isPasswordProtected() const
229 230 231 232
{
    return m_isPasswordProtected;
}

233
bool ListJob::isSingleFolderArchive() const
234
{
235 236 237 238
    if (m_filesCount == 1 && m_dirCount == 0) {
        return false;
    }

239 240 241
    return m_isSingleFolderArchive;
}

242
void ListJob::onNewEntry(const Archive::Entry *entry)
243
{
244 245
    m_extractedFilesSize += entry->property("size").toLongLong();
    m_isPasswordProtected |= entry->property("isPasswordProtected").toBool();
246

247
    if (entry->isDir()) {
248 249 250 251 252
        m_dirCount++;
    } else {
        m_filesCount++;
    }

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

258 259 260
        if (m_basePath.isEmpty()) {
            m_basePath = basePath;
            m_subfolderName = basePath;
261
        } else {
262 263 264 265
            if (m_basePath != basePath) {
                m_isSingleFolderArchive = false;
                m_subfolderName.clear();
            }
266 267 268 269
        }
    }
}

270
QString ListJob::subfolderName() const
271
{
272 273 274 275
    if (!isSingleFolderArchive()) {
        return QString();
    }

276 277 278
    return m_subfolderName;
}

279
ExtractJob::ExtractJob(const QList<Archive::Entry*> &entries, const QString &destinationDir, const ExtractionOptions &options, ReadOnlyArchiveInterface *interface)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
280
    : Job(interface)
281
    , m_entries(entries)
282 283
    , m_destinationDir(destinationDir)
    , m_options(options)
284
{
285
    qCDebug(ARK) << "ExtractJob created";
286
    setDefaultOptions();
287 288 289 290 291
}

void ExtractJob::doWork()
{
    QString desc;
292
    if (m_entries.count() == 0) {
293 294
        desc = i18n("Extracting all files");
    } else {
295
        desc = i18np("Extracting one file", "Extracting %1 files", m_entries.count());
296 297 298
    }
    emit description(this, desc);

299 300 301 302 303 304 305
    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;
    }

306
    connectToArchiveInterfaceSignals();
307

308
    qCDebug(ARK) << "Starting extraction with selected files:"
309
             << m_entries
310 311
             << "Destination dir:" << m_destinationDir
             << "Options:" << m_options;
312

313
    bool ret = archiveInterface()->extractFiles(m_entries, m_destinationDir, m_options);
314

315
    if (!archiveInterface()->waitForFinishedSignal()) {
316
        onFinished(ret);
317
    }
318 319
}

320
void ExtractJob::setDefaultOptions()
321
{
322 323
    ExtractionOptions defaultOptions;

324
    defaultOptions[QStringLiteral("PreservePaths")] = false;
325 326 327 328 329 330

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

334 335 336 337 338
QString ExtractJob::destinationDirectory() const
{
    return m_destinationDir;
}

339 340 341 342 343
ExtractionOptions ExtractJob::extractionOptions() const
{
    return m_options;
}

344
TempExtractJob::TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
345
    : Job(interface)
346
    , m_entry(entry)
347 348 349 350 351 352 353
    , m_passwordProtectedHint(passwordProtectedHint)
{
}


QString TempExtractJob::validatedFilePath() const
{
354
    QString path = extractionDir() + QLatin1Char('/') + m_entry->fullPath();
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381

    // 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;
}

void TempExtractJob::doWork()
{
    emit description(this, i18n("Extracting one file"));

    connectToArchiveInterfaceSignals();

382
    qCDebug(ARK) << "Extracting:" << m_entry;
383

384
    bool ret = archiveInterface()->extractFiles({m_entry}, extractionDir(), extractionOptions());
385 386 387 388 389 390

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

391 392
PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
    : TempExtractJob(entry, passwordProtectedHint, interface)
393 394 395 396 397 398 399 400 401
{
    qCDebug(ARK) << "PreviewJob started";
}

QString PreviewJob::extractionDir() const
{
    return m_tmpExtractDir.path();
}

402 403
OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
    : TempExtractJob(entry, passwordProtectedHint, interface)
404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
{
    qCDebug(ARK) << "OpenJob started";

    m_tmpExtractDir = new QTemporaryDir();
}

QTemporaryDir *OpenJob::tempDir() const
{
    return m_tmpExtractDir;
}

QString OpenJob::extractionDir() const
{
    return m_tmpExtractDir->path();
}

420 421
OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface)
    : OpenJob(entry, passwordProtectedHint, interface)
422 423 424 425
{
    qCDebug(ARK) << "OpenWithJob started";
}

426
AddJob::AddJob(const QList<Archive::Entry*> &entries, const Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
427
    : Job(interface)
428
    , m_entries(entries)
429
    , m_destination(destination)
430
    , m_options(options)
431
{
432
    qCDebug(ARK) << "AddJob started";
433 434 435 436
}

void AddJob::doWork()
{
437
    qCDebug(ARK) << "AddJob: going to add" << m_entries.count() << "file(s)";
438

439
    emit description(this, i18np("Adding a file", "Adding %1 files", m_entries.count()));
440 441

    ReadWriteArchiveInterface *m_writeInterface =
442
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());
443 444 445

    Q_ASSERT(m_writeInterface);

446 447 448 449 450 451 452 453 454
    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);
    }

    // The file paths must be relative to GlobalWorkDir.
455
    foreach (Archive::Entry *entry, m_entries) {
456 457
        // #191821: workDir must be used instead of QDir::current()
        //          so that symlinks aren't resolved automatically
458
        const QString &fullPath = entry->fullPath();
459
        QString relativePath = workDir.relativeFilePath(fullPath);
460

461
        if (fullPath.endsWith(QLatin1Char('/'))) {
462 463 464
            relativePath += QLatin1Char('/');
        }

465
        entry->setFullPath(relativePath);
466 467
    }

468
    connectToArchiveInterfaceSignals();
469
    bool ret = m_writeInterface->addFiles(m_entries, m_destination, m_options);
470

471
    if (!archiveInterface()->waitForFinishedSignal()) {
472
        onFinished(ret);
473
    }
474 475
}

476 477 478 479 480 481 482 483 484
void AddJob::onFinished(bool result)
{
    if (!m_oldWorkingDir.isEmpty()) {
        QDir::setCurrent(m_oldWorkingDir);
    }

    Job::onFinished(result);
}

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 547 548 549 550 551 552 553 554 555 556 557 558 559
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
560
    : Job(interface)
561
    , m_entries(entries)
562 563 564 565 566
{
}

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

    ReadWriteArchiveInterface *m_writeInterface =
570
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());
571 572 573

    Q_ASSERT(m_writeInterface);

574
    connectToArchiveInterfaceSignals();
575
    bool ret = m_writeInterface->deleteFiles(m_entries);
576

577
    if (!archiveInterface()->waitForFinishedSignal()) {
578
        onFinished(ret);
579
    }
580
}
581

Elvis Angelaccio's avatar
Elvis Angelaccio committed
582 583
CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface)
    : Job(interface)
584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604
    , 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
605 606
TestJob::TestJob(ReadOnlyArchiveInterface *interface)
    : Job(interface)
Ragnar Thomsen's avatar
Ragnar Thomsen committed
607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
{
    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;
}

636
} // namespace Kerfuffle
637

638