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 75 76 77 78 79 80
{
    static bool onlyOnce = false;
    if (!onlyOnce) {
        qRegisterMetaType<QPair<QString, QString> >("QPair<QString,QString>");
        onlyOnce = true;
    }

    setCapabilities(KJob::Killable);
}

Job::~Job()
{
81 82 83
    qDeleteAll(m_archiveEntries);
    m_archiveEntries.clear();

84 85
    if (d->isRunning()) {
        d->wait();
86 87
    }

88
    delete d;
89
}
90

91 92 93 94 95
ReadOnlyArchiveInterface *Job::archiveInterface()
{
    return m_archiveInterface;
}

96 97
void Job::start()
{
98
    jobTimer.start();
99

100
    if (archiveInterface()->waitForFinishedSignal()) {
101 102 103 104 105 106
        // 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();
    }
107 108
}

109 110
void Job::connectToArchiveInterfaceSignals()
{
Laurent Montel's avatar
Laurent Montel committed
111 112 113 114 115 116
    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);
117
    connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished);
Laurent Montel's avatar
Laurent Montel committed
118
    connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery);
119 120
}

121 122
void Job::onCancelled()
{
123
    qCDebug(ARK) << "Cancelled emitted";
124 125 126
    setError(KJob::KilledJobError);
}

127 128
void Job::onError(const QString & message, const QString & details)
{
129
    Q_UNUSED(details)
130

131
    qCDebug(ARK) << "Error emitted:" << message;
132
    setError(KJob::UserDefinedError);
133 134 135
    setErrorText(message);
}

136
void Job::onEntry(Archive::Entry *entry)
137
{
138
    emit newEntry(entry);
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
}

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)
{
158
    qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms";
159 160 161 162 163 164 165 166 167 168 169

    emitResult();
}

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

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

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

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

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

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

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

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

216 217 218
    return m_isSingleFolderArchive;
}

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

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

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

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

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

253 254 255
    return m_subfolderName;
}

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

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

276 277 278 279 280 281 282
    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;
    }

283
    connectToArchiveInterfaceSignals();
284

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

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

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

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

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

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

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

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

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

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

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

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

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

    connectToArchiveInterfaceSignals();

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

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

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

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

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

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

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

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

void AddJob::doWork()
{
407
    // Set current dir.
408 409 410 411 412 413 414 415
    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);
    }

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

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

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

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

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

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

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

    Job::onFinished(result);
}

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 547
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
548
    : Job(interface)
549
    , m_entries(entries)
550 551 552 553 554
{
}

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

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

    Q_ASSERT(m_writeInterface);

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

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

Elvis Angelaccio's avatar
Elvis Angelaccio committed
570 571
CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface)
    : Job(interface)
572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592
    , 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
593 594
TestJob::TestJob(ReadOnlyArchiveInterface *interface)
    : Job(interface)
Ragnar Thomsen's avatar
Ragnar Thomsen committed
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 623
{
    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;
}

624
} // namespace Kerfuffle
625

626