jobs.cpp 14.6 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 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
 *
 * 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.
 */
27

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

32
#include <QDir>
33
#include <QFileInfo>
34
#include <QRegularExpression>
35
#include <QThread>
36
#include <QTimer>
37

38
#include <KLocalizedString>
39

40 41
//#define DEBUG_RACECONDITION

42 43
namespace Kerfuffle
{
44

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

55
    virtual void run() Q_DECL_OVERRIDE;
56

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

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

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

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

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

    setCapabilities(KJob::Killable);
}

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

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

98
    delete d;
99
}
100

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

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

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

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

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

131 132
void Job::connectToArchiveInterfaceSignals()
{
Laurent Montel's avatar
Laurent Montel committed
133 134 135 136 137 138 139 140
    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);
141 142
}

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

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

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

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

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

    emitResult();
}

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

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

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

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

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

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

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

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

238 239 240
    return m_isSingleFolderArchive;
}

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

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

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

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

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

275 276 277
    return m_subfolderName;
}

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

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

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

305
    connectToArchiveInterfaceSignals();
306

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

312
    bool ret = archiveInterface()->copyFiles(m_entries, m_destinationDir, m_options);
313

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

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

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

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

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

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

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


QString TempExtractJob::validatedFilePath() const
{
353
    QString path = extractionDir() + QLatin1Char('/') + m_entry->property("fullPath").toString();
354 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

    // 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();

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

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

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

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

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

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

    m_tmpExtractDir = new QTemporaryDir();
}

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

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

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

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

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

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

    ReadWriteArchiveInterface *m_writeInterface =
440
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());
441 442 443

    Q_ASSERT(m_writeInterface);

444 445 446 447 448 449 450 451 452
    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.
453
    foreach (Archive::Entry *entry, m_entries) {
454 455
        // #191821: workDir must be used instead of QDir::current()
        //          so that symlinks aren't resolved automatically
456 457
        const QString &fullPath = entry->property("fullPath").toString();
        QString relativePath = workDir.relativeFilePath(fullPath);
458

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

463 464
        qCDebug(ARK) << entry->property("fullPath") << entry->isDir() << relativePath;
        entry->setFullPath(relativePath);
465 466
    }

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

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

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

    Job::onFinished(result);
}

484
DeleteJob::DeleteJob(QList<Archive::Entry*> &entries, ReadWriteArchiveInterface *interface)
Elvis Angelaccio's avatar
Elvis Angelaccio committed
485
    : Job(interface)
486
    , m_entries(entries)
487 488 489 490 491
{
}

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

    ReadWriteArchiveInterface *m_writeInterface =
495
        qobject_cast<ReadWriteArchiveInterface*>(archiveInterface());
496 497 498

    Q_ASSERT(m_writeInterface);

499
    connectToArchiveInterfaceSignals();
500
    bool ret = m_writeInterface->deleteFiles(m_entries);
501

502
    if (!archiveInterface()->waitForFinishedSignal()) {
503
        onFinished(ret);
504
    }
505
}
506

Elvis Angelaccio's avatar
Elvis Angelaccio committed
507 508
CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface)
    : Job(interface)
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
    , 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
530 531
TestJob::TestJob(ReadOnlyArchiveInterface *interface)
    : Job(interface)
Ragnar Thomsen's avatar
Ragnar Thomsen committed
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 560
{
    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;
}

561
} // namespace Kerfuffle
562

563