diff --git a/autotests/jobtest.cpp b/autotests/jobtest.cpp index 4eaa882d..7bd512b1 100644 --- a/autotests/jobtest.cpp +++ b/autotests/jobtest.cpp @@ -1,2123 +1,2129 @@ /* This file is part of the KDE project Copyright (C) 2004-2006 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "jobtest.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kiotesthelper.h" // createTestFile etc. #ifndef Q_OS_WIN #include // for readlink #endif QTEST_MAIN(JobTest) // The code comes partly from kdebase/kioslave/trash/testtrash.cpp static QString otherTmpDir() { #ifdef Q_OS_WIN return QDir::tempPath() + "/jobtest/"; #else // This one needs to be on another partition return QStringLiteral("/tmp/jobtest/"); #endif } static bool otherTmpDirIsOnSamePartition() // true on CI because it's a LXC container { KMountPoint::Ptr srcMountPoint = KMountPoint::currentMountPoints().findByPath(homeTmpDir()); KMountPoint::Ptr destMountPoint = KMountPoint::currentMountPoints().findByPath(otherTmpDir()); Q_ASSERT(srcMountPoint); Q_ASSERT(destMountPoint); return srcMountPoint->mountedFrom() == destMountPoint->mountedFrom(); } void JobTest::initTestCase() { QStandardPaths::setTestModeEnabled(true); QCoreApplication::instance()->setApplicationName("kio/jobtest"); // testing for #357499 // To avoid a runtime dependency on klauncher qputenv("KDE_FORK_SLAVES", "yes"); + // to make sure io is not too fast + qputenv("KIOSLAVE_FILE_ENABLE_TESTMODE", "1"); + s_referenceTimeStamp = QDateTime::currentDateTime().addSecs(-30); // 30 seconds ago // Start with a clean base dir cleanupTestCase(); homeTmpDir(); // create it if (!QFile::exists(otherTmpDir())) { bool ok = QDir().mkdir(otherTmpDir()); if (!ok) { qFatal("couldn't create %s", qPrintable(otherTmpDir())); } } qRegisterMetaType("KJob*"); qRegisterMetaType("KIO::Job*"); qRegisterMetaType("QDateTime"); } void JobTest::cleanupTestCase() { QDir(homeTmpDir()).removeRecursively(); QDir(otherTmpDir()).removeRecursively(); } void JobTest::enterLoop() { QEventLoop eventLoop; connect(this, &JobTest::exitLoop, &eventLoop, &QEventLoop::quit); eventLoop.exec(QEventLoop::ExcludeUserInputEvents); } void JobTest::storedGet() { qDebug(); const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); QUrl u = QUrl::fromLocalFile(filePath); m_result = -1; KIO::StoredTransferJob *job = KIO::storedGet(u, KIO::NoReload, KIO::HideProgressInfo); QSignalSpy spyPercent(job, SIGNAL(percent(KJob*,ulong))); QVERIFY(spyPercent.isValid()); job->setUiDelegate(nullptr); connect(job, &KJob::result, this, &JobTest::slotGetResult); enterLoop(); QCOMPARE(m_result, 0); // no error QCOMPARE(m_data, QByteArray("Hello\0world", 11)); QCOMPARE(m_data.size(), 11); QVERIFY(!spyPercent.isEmpty()); } void JobTest::slotGetResult(KJob *job) { m_result = job->error(); m_data = static_cast(job)->data(); emit exitLoop(); } void JobTest::put() { const QString filePath = homeTmpDir() + "fileFromHome"; QUrl u = QUrl::fromLocalFile(filePath); KIO::TransferJob *job = KIO::put(u, 0600, KIO::Overwrite | KIO::HideProgressInfo); quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago job->setModificationTime(mtime); job->setUiDelegate(nullptr); connect(job, &KJob::result, this, &JobTest::slotResult); connect(job, &KIO::TransferJob::dataReq, this, &JobTest::slotDataReq); m_result = -1; m_dataReqCount = 0; enterLoop(); QVERIFY(m_result == 0); // no error QFileInfo fileInfo(filePath); QVERIFY(fileInfo.exists()); QCOMPARE(fileInfo.size(), 30LL); // "This is a test for KIO::put()\n" QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)); QCOMPARE(fileInfo.lastModified(), mtime); } void JobTest::slotDataReq(KIO::Job *, QByteArray &data) { // Really not the way you'd write a slotDataReq usually :) switch (m_dataReqCount++) { case 0: data = "This is a test for "; break; case 1: data = "KIO::put()\n"; break; case 2: data = QByteArray(); break; } } void JobTest::slotResult(KJob *job) { m_result = job->error(); emit exitLoop(); } void JobTest::storedPut() { const QString filePath = homeTmpDir() + "fileFromHome"; QUrl u = QUrl::fromLocalFile(filePath); QByteArray putData = "This is the put data"; KIO::TransferJob *job = KIO::storedPut(putData, u, 0600, KIO::Overwrite | KIO::HideProgressInfo); QSignalSpy spyPercent(job, SIGNAL(percent(KJob*,ulong))); QVERIFY(spyPercent.isValid()); quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago job->setModificationTime(mtime); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QFileInfo fileInfo(filePath); QVERIFY(fileInfo.exists()); QCOMPARE(fileInfo.size(), (long long)putData.size()); QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)); QCOMPARE(fileInfo.lastModified(), mtime); QVERIFY(!spyPercent.isEmpty()); } void JobTest::storedPutIODevice() { const QString filePath = homeTmpDir() + "fileFromHome"; QBuffer putData; putData.setData("This is the put data"); QVERIFY(putData.open(QIODevice::ReadOnly)); KIO::TransferJob *job = KIO::storedPut(&putData, QUrl::fromLocalFile(filePath), 0600, KIO::Overwrite | KIO::HideProgressInfo); QSignalSpy spyPercent(job, SIGNAL(percent(KJob*,ulong))); QVERIFY(spyPercent.isValid()); quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago job->setModificationTime(mtime); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QFileInfo fileInfo(filePath); QVERIFY(fileInfo.exists()); QCOMPARE(fileInfo.size(), (long long)putData.size()); QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)); QCOMPARE(fileInfo.lastModified(), mtime); QVERIFY(!spyPercent.isEmpty()); } void JobTest::storedPutIODeviceFile() { // Given a source file and a destination file const QString src = homeTmpDir() + "fileFromHome"; createTestFile(src); QVERIFY(QFile::exists(src)); QFile srcFile(src); QVERIFY(srcFile.open(QIODevice::ReadOnly)); const QString dest = homeTmpDir() + "fileFromHome_copied"; QFile::remove(dest); const QUrl destUrl = QUrl::fromLocalFile(dest); // When using storedPut with the QFile as argument KIO::StoredTransferJob *job = KIO::storedPut(&srcFile, destUrl, 0600, KIO::Overwrite | KIO::HideProgressInfo); // Then the copy should succeed and the dest file exist QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QCOMPARE(QFileInfo(src).size(), QFileInfo(dest).size()); QFile::remove(dest); } void JobTest::storedPutIODeviceTempFile() { // Create a temp file in the current dir. QTemporaryFile tempFile(QStringLiteral("jobtest-tmp")); QVERIFY(tempFile.open()); // Write something into the file. QTextStream stream(&tempFile); stream << QStringLiteral("This is the put data"); stream.flush(); QVERIFY(QFileInfo(tempFile).size() > 0); const QString dest = homeTmpDir() + QLatin1String("tmpfile-dest"); const QUrl destUrl = QUrl::fromLocalFile(dest); // QTemporaryFiles are open in ReadWrite mode, // so we don't need to close and reopen, // but we need to rewind to the beginning. tempFile.seek(0); auto job = KIO::storedPut(&tempFile, destUrl, -1); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo::exists(dest)); QCOMPARE(QFileInfo(dest).size(), QFileInfo(tempFile).size()); QVERIFY(QFile::remove(dest)); } void JobTest::storedPutIODeviceFastDevice() { const QString filePath = homeTmpDir() + "fileFromHome"; const QUrl u = QUrl::fromLocalFile(filePath); const QByteArray putDataContents = "This is the put data"; QBuffer putDataBuffer; QVERIFY(putDataBuffer.open(QIODevice::ReadWrite)); KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo); QSignalSpy spyPercent(job, SIGNAL(percent(KJob*,ulong))); QVERIFY(spyPercent.isValid()); quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago job->setModificationTime(mtime); job->setTotalSize(putDataContents.size()); job->setUiDelegate(nullptr); job->setAsyncDataEnabled(true); // Emit the readChannelFinished even before the job has had time to start const auto pos = putDataBuffer.pos(); int size = putDataBuffer.write(putDataContents); putDataBuffer.seek(pos); putDataBuffer.readChannelFinished(); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(size, putDataContents.size()); QCOMPARE(putDataBuffer.bytesAvailable(), 0); QFileInfo fileInfo(filePath); QVERIFY(fileInfo.exists()); QCOMPARE(fileInfo.size(), (long long)putDataContents.size()); QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)); QCOMPARE(fileInfo.lastModified(), mtime); QVERIFY(!spyPercent.isEmpty()); } void JobTest::storedPutIODeviceSlowDevice() { const QString filePath = homeTmpDir() + "fileFromHome"; const QUrl u = QUrl::fromLocalFile(filePath); const QByteArray putDataContents = "This is the put data"; QBuffer putDataBuffer; QVERIFY(putDataBuffer.open(QIODevice::ReadWrite)); KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo); QSignalSpy spyPercent(job, SIGNAL(percent(KJob*,ulong))); QVERIFY(spyPercent.isValid()); quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago job->setModificationTime(mtime); job->setTotalSize(putDataContents.size()); job->setUiDelegate(nullptr); job->setAsyncDataEnabled(true); int size = 0; const auto writeOnce = [&putDataBuffer, &size, putDataContents]() { const auto pos = putDataBuffer.pos(); size += putDataBuffer.write(putDataContents); putDataBuffer.seek(pos); // qDebug() << "written" << size; }; QTimer::singleShot(200, this, writeOnce); QTimer::singleShot(400, this, writeOnce); // Simulate the transfer is done QTimer::singleShot(450, this, [&putDataBuffer](){ putDataBuffer.readChannelFinished(); }); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(size, putDataContents.size() * 2); QCOMPARE(putDataBuffer.bytesAvailable(), 0); QFileInfo fileInfo(filePath); QVERIFY(fileInfo.exists()); QCOMPARE(fileInfo.size(), (long long)putDataContents.size() * 2); QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)); QCOMPARE(fileInfo.lastModified(), mtime); QVERIFY(!spyPercent.isEmpty()); } void JobTest::storedPutIODeviceSlowDeviceBigChunk() { const QString filePath = homeTmpDir() + "fileFromHome"; const QUrl u = QUrl::fromLocalFile(filePath); const QByteArray putDataContents(300000, 'K'); // Make sure the 300000 is bigger than MAX_READ_BUF_SIZE QBuffer putDataBuffer; QVERIFY(putDataBuffer.open(QIODevice::ReadWrite)); KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo); QSignalSpy spyPercent(job, SIGNAL(percent(KJob*,ulong))); QVERIFY(spyPercent.isValid()); quint64 secsSinceEpoch = QDateTime::currentSecsSinceEpoch(); // Use second granularity, supported on all filesystems QDateTime mtime = QDateTime::fromSecsSinceEpoch(secsSinceEpoch - 30); // 30 seconds ago job->setModificationTime(mtime); job->setTotalSize(putDataContents.size()); job->setUiDelegate(nullptr); job->setAsyncDataEnabled(true); int size = 0; const auto writeOnce = [&putDataBuffer, &size, putDataContents]() { const auto pos = putDataBuffer.pos(); size += putDataBuffer.write(putDataContents); putDataBuffer.seek(pos); // qDebug() << "written" << size; }; QTimer::singleShot(200, this, writeOnce); // Simulate the transfer is done QTimer::singleShot(450, this, [&putDataBuffer](){ putDataBuffer.readChannelFinished(); }); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(size, putDataContents.size()); QCOMPARE(putDataBuffer.bytesAvailable(), 0); QFileInfo fileInfo(filePath); QVERIFY(fileInfo.exists()); QCOMPARE(fileInfo.size(), (long long)putDataContents.size()); QCOMPARE((int)fileInfo.permissions(), (int)(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser)); QCOMPARE(fileInfo.lastModified(), mtime); QVERIFY(!spyPercent.isEmpty()); } void JobTest::asyncStoredPutReadyReadAfterFinish() { const QString filePath = homeTmpDir() + "fileFromHome"; const QUrl u = QUrl::fromLocalFile(filePath); QBuffer putDataBuffer; QVERIFY(putDataBuffer.open(QIODevice::ReadWrite)); KIO::StoredTransferJob *job = KIO::storedPut(&putDataBuffer, u, 0600, KIO::Overwrite | KIO::HideProgressInfo); job->setAsyncDataEnabled(true); bool jobFinished = false; connect(job, &KJob::finished, [&jobFinished, &putDataBuffer] { putDataBuffer.readyRead(); jobFinished = true; }); QTimer::singleShot(200, this, [job]() { job->kill(); }); QTRY_VERIFY(jobFinished); } //// void JobTest::copyLocalFile(const QString &src, const QString &dest) { const QUrl u = QUrl::fromLocalFile(src); const QUrl d = QUrl::fromLocalFile(dest); const int perms = 0666; // copy the file with file_copy KIO::Job *job = KIO::file_copy(u, d, perms, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there QCOMPARE(int(QFileInfo(dest).permissions()), int(0x6666)); { // check that the timestamp is the same (#24443) // Note: this only works because of copy() in kio_file. // The datapump solution ignores mtime, the app has to call FileCopyJob::setModificationTime() QFileInfo srcInfo(src); QFileInfo destInfo(dest); #ifdef Q_OS_WIN // win32 time may differs in msec part QCOMPARE(srcInfo.lastModified().toString("dd.MM.yyyy hh:mm"), destInfo.lastModified().toString("dd.MM.yyyy hh:mm")); #else QCOMPARE(srcInfo.lastModified(), destInfo.lastModified()); #endif } // cleanup and retry with KIO::copy() QFile::remove(dest); job = KIO::copy(u, d, KIO::HideProgressInfo); QSignalSpy spyCopyingDone(job, SIGNAL(copyingDone(KIO::Job*,QUrl,QUrl,QDateTime,bool,bool))); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there { // check that the timestamp is the same (#24443) QFileInfo srcInfo(src); QFileInfo destInfo(dest); #ifdef Q_OS_WIN // win32 time may differs in msec part QCOMPARE(srcInfo.lastModified().toString("dd.MM.yyyy hh:mm"), destInfo.lastModified().toString("dd.MM.yyyy hh:mm")); #else QCOMPARE(srcInfo.lastModified(), destInfo.lastModified()); #endif } QCOMPARE(spyCopyingDone.count(), 1); // cleanup and retry with KIO::copyAs() QFile::remove(dest); job = KIO::copyAs(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there // Do it again, with Overwrite. job = KIO::copyAs(u, d, KIO::Overwrite | KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(QFile::exists(src)); // still there // Do it again, without Overwrite (should fail). job = KIO::copyAs(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(!job->exec()); // Clean up QFile::remove(dest); } void JobTest::copyLocalDirectory(const QString &src, const QString &_dest, int flags) { QVERIFY(QFileInfo(src).isDir()); QVERIFY(QFileInfo(src + "/testfile").isFile()); QUrl u = QUrl::fromLocalFile(src); QString dest(_dest); QUrl d = QUrl::fromLocalFile(dest); if (flags & AlreadyExists) { QVERIFY(QFile::exists(dest)); } else { QVERIFY(!QFile::exists(dest)); } KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(QFileInfo(dest).isDir()); QVERIFY(QFileInfo(dest + "/testfile").isFile()); QVERIFY(QFile::exists(src)); // still there if (flags & AlreadyExists) { dest += '/' + u.fileName(); //qDebug() << "Expecting dest=" << dest; } // CopyJob::setNextDirAttribute isn't implemented for Windows currently. #ifndef Q_OS_WIN { // Check that the timestamp is the same (#24443) QFileInfo srcInfo(src); QFileInfo destInfo(dest); QCOMPARE(srcInfo.lastModified(), destInfo.lastModified()); } #endif // Do it again, with Overwrite. // Use copyAs, we don't want a subdir inside d. job = KIO::copyAs(u, d, KIO::HideProgressInfo | KIO::Overwrite); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); // Do it again, without Overwrite (should fail). job = KIO::copyAs(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(!job->exec()); } #ifndef Q_OS_WIN static QString linkTarget(const QString &path) { // Use readlink on Unix because symLinkTarget turns relative targets into absolute (#352927) char linkTargetBuffer[4096]; const int n = readlink(QFile::encodeName(path).constData(), linkTargetBuffer, sizeof(linkTargetBuffer) - 1); if (n != -1) { linkTargetBuffer[n] = 0; } return QFile::decodeName(linkTargetBuffer); } static void copyLocalSymlink(const QString &src, const QString &dest, const QString &expectedLinkTarget) { QT_STATBUF buf; QVERIFY(QT_LSTAT(QFile::encodeName(src).constData(), &buf) == 0); QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(dest); // copy the symlink KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(QString::number(job->error()))); QVERIFY(QT_LSTAT(QFile::encodeName(dest).constData(), &buf) == 0); // dest exists QCOMPARE(linkTarget(dest), expectedLinkTarget); // cleanup QFile::remove(dest); } #endif void JobTest::copyFileToSamePartition() { const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = homeTmpDir() + "fileFromHome_copied"; createTestFile(filePath); copyLocalFile(filePath, dest); } void JobTest::copyDirectoryToSamePartition() { qDebug(); const QString src = homeTmpDir() + "dirFromHome"; const QString dest = homeTmpDir() + "dirFromHome_copied"; createTestDirectory(src); copyLocalDirectory(src, dest); } void JobTest::copyDirectoryToExistingDirectory() { qDebug(); // just the same as copyDirectoryToSamePartition, but this time dest exists. // So we get a subdir, "dirFromHome_copy/dirFromHome" const QString src = homeTmpDir() + "dirFromHome"; const QString dest = homeTmpDir() + "dirFromHome_copied"; createTestDirectory(src); createTestDirectory(dest); copyLocalDirectory(src, dest, AlreadyExists); } void JobTest::copyFileToOtherPartition() { qDebug(); const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = otherTmpDir() + "fileFromHome_copied"; createTestFile(filePath); copyLocalFile(filePath, dest); } void JobTest::copyDirectoryToOtherPartition() { qDebug(); const QString src = homeTmpDir() + "dirFromHome"; const QString dest = otherTmpDir() + "dirFromHome_copied"; createTestDirectory(src); copyLocalDirectory(src, dest); } void JobTest::copyRelativeSymlinkToSamePartition() // #352927 { #ifdef Q_OS_WIN QSKIP("Skipping symlink test on Windows"); #else const QString filePath = homeTmpDir() + "testlink"; const QString dest = homeTmpDir() + "testlink_copied"; createTestSymlink(filePath, "relative"); copyLocalSymlink(filePath, dest, QStringLiteral("relative")); QFile::remove(filePath); #endif } void JobTest::copyAbsoluteSymlinkToOtherPartition() { #ifdef Q_OS_WIN QSKIP("Skipping symlink test on Windows"); #else const QString filePath = homeTmpDir() + "testlink"; const QString dest = otherTmpDir() + "testlink_copied"; createTestSymlink(filePath, QFile::encodeName(homeTmpDir())); copyLocalSymlink(filePath, dest, homeTmpDir()); QFile::remove(filePath); #endif } void JobTest::copyFolderWithUnaccessibleSubfolder() { #ifdef Q_OS_WIN QSKIP("Skipping unaccessible folder test on Windows, cannot remove all permissions from a folder"); #endif const QString src_dir = homeTmpDir() + "srcHome"; const QString dst_dir = homeTmpDir() + "dstHome"; QDir().remove(src_dir); QDir().remove(dst_dir); createTestDirectory(src_dir); createTestDirectory(src_dir + "/folder1"); QString inaccessible = src_dir + "/folder1/inaccessible"; createTestDirectory(inaccessible); QFile(inaccessible).setPermissions(QFile::Permissions()); // Make it inaccessible //Copying should throw some warnings, as it cannot access some folders KIO::CopyJob *job = KIO::copy(QUrl::fromLocalFile(src_dir), QUrl::fromLocalFile(dst_dir), KIO::HideProgressInfo); QSignalSpy spy(job, SIGNAL(warning(KJob*,QString,QString))); job->setUiDelegate(nullptr); // no skip dialog, thanks QVERIFY2(job->exec(), qPrintable(job->errorString())); QFile(inaccessible).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner)); KIO::DeleteJob *deljob1 = KIO::del(QUrl::fromLocalFile(src_dir), KIO::HideProgressInfo); deljob1->setUiDelegate(nullptr); // no skip dialog, thanks QVERIFY(deljob1->exec()); KIO::DeleteJob *deljob2 = KIO::del(QUrl::fromLocalFile(dst_dir), KIO::HideProgressInfo); deljob2->setUiDelegate(nullptr); // no skip dialog, thanks QVERIFY(deljob2->exec()); QCOMPARE(spy.count(), 1); // one warning should be emitted by the copy job } void JobTest::copyDataUrl() { // GIVEN const QString dst_dir = homeTmpDir(); QVERIFY(!QFileInfo::exists(dst_dir + "/data")); // WHEN KIO::CopyJob *job = KIO::copy(QUrl("data:,Hello%2C%20World!"), QUrl::fromLocalFile(dst_dir), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); // THEN QVERIFY(QFileInfo(dst_dir + "/data").isFile()); QFile::remove(dst_dir + "/data"); } void JobTest::suspendFileCopy() { const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = homeTmpDir() + "fileFromHome_copied"; createTestFile(filePath); const QUrl u = QUrl::fromLocalFile(filePath); const QUrl d = QUrl::fromLocalFile(dest); KIO::Job *job = KIO::file_copy(u, d, KIO::HideProgressInfo); QSignalSpy spyResult(job, SIGNAL(result(KJob*))); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(job->suspend()); QVERIFY(!spyResult.wait(300)); QVERIFY(job->resume()); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QFile::remove(dest); } void JobTest::suspendCopy() { const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = homeTmpDir() + "fileFromHome_copied"; createTestFile(filePath); const QUrl u = QUrl::fromLocalFile(filePath); const QUrl d = QUrl::fromLocalFile(dest); KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo); QSignalSpy spyResult(job, SIGNAL(result(KJob*))); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(job->suspend()); QVERIFY(!spyResult.wait(300)); QVERIFY(job->resume()); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QFile::remove(dest); } void JobTest::moveLocalFile(const QString &src, const QString &dest) { QVERIFY(QFile::exists(src)); QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(dest); // move the file with file_move KIO::Job *job = KIO::file_move(u, d, 0666, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(!QFile::exists(src)); // not there anymore QCOMPARE(int(QFileInfo(dest).permissions()), int(0x6666)); // move it back with KIO::move() job = KIO::move(d, u, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(dest)); QVERIFY(QFile::exists(src)); // it's back } static void moveLocalSymlink(const QString &src, const QString &dest) { QT_STATBUF buf; QVERIFY(QT_LSTAT(QFile::encodeName(src).constData(), &buf) == 0); QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(dest); // move the symlink with move, NOT with file_move KIO::Job *job = KIO::move(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QT_LSTAT(QFile::encodeName(dest).constData(), &buf) == 0); QVERIFY(!QFile::exists(src)); // not there anymore // move it back with KIO::move() job = KIO::move(d, u, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QT_LSTAT(QFile::encodeName(dest).constData(), &buf) != 0); // doesn't exist anymore QVERIFY(QT_LSTAT(QFile::encodeName(src).constData(), &buf) == 0); // it's back } void JobTest::moveLocalDirectory(const QString &src, const QString &dest) { qDebug() << src << " " << dest; QVERIFY(QFile::exists(src)); QVERIFY(QFileInfo(src).isDir()); QVERIFY(QFileInfo(src + "/testfile").isFile()); #ifndef Q_OS_WIN QVERIFY(QFileInfo(src + "/testlink").isSymLink()); #endif QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(dest); KIO::Job *job = KIO::move(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest)); QVERIFY(QFileInfo(dest).isDir()); QVERIFY(QFileInfo(dest + "/testfile").isFile()); QVERIFY(!QFile::exists(src)); // not there anymore #ifndef Q_OS_WIN QVERIFY(QFileInfo(dest + "/testlink").isSymLink()); #endif } void JobTest::moveFileToSamePartition() { qDebug(); const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = homeTmpDir() + "fileFromHome_moved"; createTestFile(filePath); moveLocalFile(filePath, dest); } void JobTest::moveDirectoryToSamePartition() { qDebug(); const QString src = homeTmpDir() + "dirFromHome"; const QString dest = homeTmpDir() + "dirFromHome_moved"; createTestDirectory(src); moveLocalDirectory(src, dest); } void JobTest::moveDirectoryIntoItself() { qDebug(); const QString src = homeTmpDir() + "dirFromHome"; const QString dest = src + "/foo"; createTestDirectory(src); QVERIFY(QFile::exists(src)); QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(dest); KIO::CopyJob *job = KIO::move(u, d); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_CANNOT_MOVE_INTO_ITSELF); QCOMPARE(job->errorString(), i18n("A folder cannot be moved into itself")); QDir(dest).removeRecursively(); } void JobTest::moveFileToOtherPartition() { qDebug(); const QString filePath = homeTmpDir() + "fileFromHome"; const QString dest = otherTmpDir() + "fileFromHome_moved"; createTestFile(filePath); moveLocalFile(filePath, dest); } void JobTest::moveSymlinkToOtherPartition() { #ifndef Q_OS_WIN qDebug(); const QString filePath = homeTmpDir() + "testlink"; const QString dest = otherTmpDir() + "testlink_moved"; createTestSymlink(filePath); moveLocalSymlink(filePath, dest); #endif } void JobTest::moveDirectoryToOtherPartition() { qDebug(); #ifndef Q_OS_WIN const QString src = homeTmpDir() + "dirFromHome"; const QString dest = otherTmpDir() + "dirFromHome_moved"; createTestDirectory(src); moveLocalDirectory(src, dest); #endif } void JobTest::moveFileNoPermissions() { #ifdef Q_OS_WIN QSKIP("Skipping unaccessible folder test on Windows, cannot remove all permissions from a folder"); #endif // Given a file that cannot be moved (subdir has no permissions) const QString subdir = homeTmpDir() + "subdir"; QVERIFY(QDir().mkpath(subdir)); const QString src = subdir + "/thefile"; createTestFile(src); QVERIFY(QFile(subdir).setPermissions(QFile::Permissions())); // Make it inaccessible // When trying to move it const QString dest = homeTmpDir() + "dest"; KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); // no skip dialog, thanks // The job should fail with "access denied" QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_ACCESS_DENIED); // Note that, just like mv(1), KIO's behavior depends on whether // a direct rename(2) was used, or a full copy+del. In the first case // there is no destination file created, but in the second case the // destination file remains. // In this test it's the same partition, so no dest created. QVERIFY(!QFile::exists(dest)); // Cleanup QVERIFY(QFile(subdir).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner))); QVERIFY(QFile::exists(src)); QVERIFY(QDir(subdir).removeRecursively()); } void JobTest::moveDirectoryNoPermissions() { #ifdef Q_OS_WIN QSKIP("Skipping unaccessible folder test on Windows, cannot remove all permissions from a folder"); #endif // Given a dir that cannot be moved (parent dir has no permissions) const QString subdir = homeTmpDir() + "subdir"; const QString src = subdir + "/thedir"; QVERIFY(QDir().mkpath(src)); QVERIFY(QFileInfo(src).isDir()); QVERIFY(QFile(subdir).setPermissions(QFile::Permissions())); // Make it inaccessible // When trying to move it const QString dest = homeTmpDir() + "mdnp"; KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); // no skip dialog, thanks // The job should fail with "access denied" QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_ACCESS_DENIED); QVERIFY(!QFile::exists(dest)); // Cleanup QVERIFY(QFile(subdir).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner))); QVERIFY(QFile::exists(src)); QVERIFY(QDir(subdir).removeRecursively()); } void JobTest::moveDirectoryToReadonlyFilesystem_data() { QTest::addColumn>("sources"); QTest::addColumn("expectedErrorCode"); const QString srcFileHomePath = homeTmpDir() + "srcFileHome"; const QUrl srcFileHome = QUrl::fromLocalFile(srcFileHomePath); createTestFile(srcFileHomePath); const QString srcFileOtherPath = otherTmpDir() + "srcFileOther"; const QUrl srcFileOther = QUrl::fromLocalFile(srcFileOtherPath); createTestFile(srcFileOtherPath); const QString srcDirHomePath = homeTmpDir() + "srcDirHome"; const QUrl srcDirHome = QUrl::fromLocalFile(srcDirHomePath); createTestDirectory(srcDirHomePath); const QString srcDirHome2Path = homeTmpDir() + "srcDirHome2"; const QUrl srcDirHome2 = QUrl::fromLocalFile(srcDirHome2Path); createTestDirectory(srcDirHome2Path); const QString srcDirOtherPath = otherTmpDir() + "srcDirOther"; const QUrl srcDirOther = QUrl::fromLocalFile(srcDirOtherPath); createTestDirectory(srcDirOtherPath); QTest::newRow("file_same_partition") << QList{srcFileHome} << int(KIO::ERR_WRITE_ACCESS_DENIED); QTest::newRow("file_other_partition") << QList{srcFileOther} << int(KIO::ERR_WRITE_ACCESS_DENIED); QTest::newRow("one_dir_same_partition") << QList{srcDirHome} << int(KIO::ERR_WRITE_ACCESS_DENIED); QTest::newRow("one_dir_other_partition") << QList{srcDirOther} << int(KIO::ERR_WRITE_ACCESS_DENIED); QTest::newRow("dirs_same_partition") << QList{srcDirHome, srcDirHome2} << int(KIO::ERR_WRITE_ACCESS_DENIED); QTest::newRow("dirs_both_partitions") << QList{srcDirOther, srcDirHome} << int(KIO::ERR_WRITE_ACCESS_DENIED); } void JobTest::moveDirectoryToReadonlyFilesystem() { QFETCH(QList, sources); QFETCH(int, expectedErrorCode); const QString dst_dir = homeTmpDir() + "readonlyDest"; const QUrl dst = QUrl::fromLocalFile(dst_dir); QVERIFY2(QDir().mkdir(dst_dir), qPrintable(dst_dir)); QFile(dst_dir).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::ExeOwner)); // Make it readonly, moving should throw some errors KIO::CopyJob *job = KIO::move(sources, dst, KIO::HideProgressInfo | KIO::NoPrivilegeExecution); job->setUiDelegate(nullptr); QVERIFY(!job->exec()); QCOMPARE(job->error(), expectedErrorCode); for (const QUrl &srcUrl : qAsConst(sources)) { QVERIFY(QFileInfo::exists(srcUrl.toLocalFile())); // no moving happened } KIO::CopyJob *job2 = KIO::move(sources, dst, KIO::HideProgressInfo); job2->setUiDelegate(nullptr); QVERIFY(!job2->exec()); if (job2->error() != KIO::ERR_CANNOT_MKDIR) { // This can happen when moving between partitions, but on CI it's the same partition so allow both QCOMPARE(job2->error(), expectedErrorCode); } for (const QUrl &srcUrl : qAsConst(sources)) { QVERIFY(QFileInfo::exists(srcUrl.toLocalFile())); // no moving happened } // Cleanup QVERIFY(QFile(dst_dir).setPermissions(QFile::Permissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner))); QVERIFY(QDir(dst_dir).removeRecursively()); } void JobTest::listRecursive() { // Note: many other tests must have been run before since we rely on the files they created const QString src = homeTmpDir(); #ifndef Q_OS_WIN // Add a symlink to a dir, to make sure we don't recurse into those bool symlinkOk = symlink("dirFromHome", QFile::encodeName(src + "/dirFromHome_link").constData()) == 0; QVERIFY(symlinkOk); #endif KIO::ListJob *job = KIO::listRecursive(QUrl::fromLocalFile(src), KIO::HideProgressInfo); job->setUiDelegate(nullptr); connect(job, &KIO::ListJob::entries, this, &JobTest::slotEntries); QVERIFY2(job->exec(), qPrintable(job->errorString())); m_names.sort(); QByteArray ref_names = QByteArray(".,..," "dirFromHome,dirFromHome/testfile," "dirFromHome/testlink," // exists on Windows too, see createTestDirectory "dirFromHome_copied," "dirFromHome_copied/dirFromHome,dirFromHome_copied/dirFromHome/testfile," "dirFromHome_copied/dirFromHome/testlink," "dirFromHome_copied/testfile," "dirFromHome_copied/testlink," #ifndef Q_OS_WIN "dirFromHome_link," #endif "fileFromHome"); const QString joinedNames = m_names.join(QLatin1Char(',')); if (joinedNames.toLatin1() != ref_names) { qDebug("%s", qPrintable(joinedNames)); qDebug("%s", ref_names.data()); } QCOMPARE(joinedNames.toLatin1(), ref_names); } void JobTest::listFile() { const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); KIO::ListJob *job = KIO::listDir(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY(!job->exec()); QCOMPARE(job->error(), static_cast(KIO::ERR_IS_FILE)); // And list something that doesn't exist const QString path = homeTmpDir() + "fileFromHomeDoesNotExist"; job = KIO::listDir(QUrl::fromLocalFile(path), KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY(!job->exec()); QCOMPARE(job->error(), static_cast(KIO::ERR_DOES_NOT_EXIST)); } void JobTest::killJob() { const QString src = homeTmpDir(); KIO::ListJob *job = KIO::listDir(QUrl::fromLocalFile(src), KIO::HideProgressInfo); QVERIFY(job->isAutoDelete()); QPointer ptr(job); job->setUiDelegate(nullptr); qApp->processEvents(); // let the job start, it's no fun otherwise job->kill(); qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); // process the deferred delete of the job QVERIFY(ptr.isNull()); } void JobTest::killJobBeforeStart() { const QString src = homeTmpDir(); KIO::Job *job = KIO::stat(QUrl::fromLocalFile(src), KIO::HideProgressInfo); QVERIFY(job->isAutoDelete()); QPointer ptr(job); job->setUiDelegate(nullptr); job->kill(); qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); // process the deferred delete of the job QVERIFY(ptr.isNull()); qApp->processEvents(); // does KIO scheduler crash here? nope. } void JobTest::deleteJobBeforeStart() // #163171 { const QString src = homeTmpDir(); KIO::Job *job = KIO::stat(QUrl::fromLocalFile(src), KIO::HideProgressInfo); QVERIFY(job->isAutoDelete()); job->setUiDelegate(nullptr); delete job; qApp->processEvents(); // does KIO scheduler crash here? } void JobTest::directorySize() { // Note: many other tests must have been run before since we rely on the files they created const QString src = homeTmpDir(); KIO::DirectorySizeJob *job = KIO::directorySize(QUrl::fromLocalFile(src)); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); qDebug() << "totalSize: " << job->totalSize(); qDebug() << "totalFiles: " << job->totalFiles(); qDebug() << "totalSubdirs: " << job->totalSubdirs(); #ifdef Q_OS_WIN QCOMPARE(job->totalFiles(), 5ULL); // see expected result in listRecursive() above QCOMPARE(job->totalSubdirs(), 3ULL); // see expected result in listRecursive() above QVERIFY(job->totalSize() > 54); #else QCOMPARE(job->totalFiles(), 7ULL); // see expected result in listRecursive() above QCOMPARE(job->totalSubdirs(), 4ULL); // see expected result in listRecursive() above QVERIFY2(job->totalSize() >= 60, qPrintable(QString("totalSize was %1").arg(job->totalSize()))); // size of subdir entries is filesystem dependent. E.g. this is 16428 with ext4 but only 272 with xfs, and 63 on FreeBSD #endif qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); } void JobTest::directorySizeError() { KIO::DirectorySizeJob *job = KIO::directorySize(QUrl::fromLocalFile(QStringLiteral("/I/Dont/Exist"))); job->setUiDelegate(nullptr); QVERIFY(!job->exec()); qApp->sendPostedEvents(nullptr, QEvent::DeferredDelete); } void JobTest::slotEntries(KIO::Job *, const KIO::UDSEntryList &lst) { for (KIO::UDSEntryList::ConstIterator it = lst.begin(); it != lst.end(); ++it) { QString displayName = (*it).stringValue(KIO::UDSEntry::UDS_NAME); //QUrl url = (*it).stringValue( KIO::UDSEntry::UDS_URL ); m_names.append(displayName); } } void JobTest::calculateRemainingSeconds() { unsigned int seconds = KIO::calculateRemainingSeconds(2 * 86400 - 60, 0, 1); QCOMPARE(seconds, static_cast(2 * 86400 - 60)); QString text = KIO::convertSeconds(seconds); QCOMPARE(text, i18n("1 day 23:59:00")); seconds = KIO::calculateRemainingSeconds(520, 20, 10); QCOMPARE(seconds, static_cast(50)); text = KIO::convertSeconds(seconds); QCOMPARE(text, i18n("00:00:50")); } void JobTest::getInvalidUrl() { QUrl url(QStringLiteral("http://strange/")); QVERIFY(!url.isValid()); KIO::SimpleJob *job = KIO::get(url, KIO::NoReload, KIO::HideProgressInfo); QVERIFY(job != nullptr); job->setUiDelegate(nullptr); KIO::Scheduler::setJobPriority(job, 1); // shouldn't crash (#135456) QVERIFY(!job->exec()); // it should fail :) } void JobTest::slotMimetype(KIO::Job *job, const QString &type) { QVERIFY(job != nullptr); m_mimetype = type; } void JobTest::deleteFile() { const QString dest = otherTmpDir() + "fileFromHome_copied"; createTestFile(dest); KIO::Job *job = KIO::del(QUrl::fromLocalFile(dest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(dest)); } void JobTest::deleteDirectory() { const QString dest = otherTmpDir() + "dirFromHome_copied"; if (!QFile::exists(dest)) { createTestDirectory(dest); } // Let's put a few things in there to see if the recursive deletion works correctly // A hidden file: createTestFile(dest + "/.hidden"); #ifndef Q_OS_WIN // A broken symlink: createTestSymlink(dest + "/broken_symlink"); // A symlink to a dir: bool symlink_ok = symlink(QFile::encodeName(QFileInfo(QFINDTESTDATA("jobtest.cpp")).absolutePath()).constData(), QFile::encodeName(dest + "/symlink_to_dir").constData()) == 0; if (!symlink_ok) { qFatal("couldn't create symlink: %s", strerror(errno)); } #endif KIO::Job *job = KIO::del(QUrl::fromLocalFile(dest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(dest)); } void JobTest::deleteSymlink(bool using_fast_path) { extern KIOCORE_EXPORT bool kio_resolve_local_urls; kio_resolve_local_urls = !using_fast_path; #ifndef Q_OS_WIN const QString src = homeTmpDir() + "dirFromHome"; createTestDirectory(src); QVERIFY(QFile::exists(src)); const QString dest = homeTmpDir() + "/dirFromHome_link"; if (!QFile::exists(dest)) { // Add a symlink to a dir, to make sure we don't recurse into those bool symlinkOk = symlink(QFile::encodeName(src).constData(), QFile::encodeName(dest).constData()) == 0; QVERIFY(symlinkOk); QVERIFY(QFile::exists(dest)); } KIO::Job *job = KIO::del(QUrl::fromLocalFile(dest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(dest)); QVERIFY(QFile::exists(src)); #endif kio_resolve_local_urls = true; } void JobTest::deleteSymlink() { #ifndef Q_OS_WIN deleteSymlink(true); deleteSymlink(false); #endif } void JobTest::deleteManyDirs(bool using_fast_path) { extern KIOCORE_EXPORT bool kio_resolve_local_urls; kio_resolve_local_urls = !using_fast_path; const int numDirs = 50; QList dirs; for (int i = 0; i < numDirs; ++i) { const QString dir = homeTmpDir() + "dir" + QString::number(i); createTestDirectory(dir); dirs << QUrl::fromLocalFile(dir); } QElapsedTimer dt; dt.start(); KIO::Job *job = KIO::del(dirs, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); for (const QUrl &dir : qAsConst(dirs)) { QVERIFY(!QFile::exists(dir.toLocalFile())); } qDebug() << "Deleted" << numDirs << "dirs in" << dt.elapsed() << "milliseconds"; kio_resolve_local_urls = true; } void JobTest::deleteManyDirs() { deleteManyDirs(true); deleteManyDirs(false); } static QList createManyFiles(const QString &baseDir, int numFiles) { QList ret; ret.reserve(numFiles); for (int i = 0; i < numFiles; ++i) { // create empty file const QString file = baseDir + QString::number(i); QFile f(file); bool ok = f.open(QIODevice::WriteOnly); if (ok) { f.write("Hello"); ret.append(QUrl::fromLocalFile(file)); } } return ret; } void JobTest::deleteManyFilesIndependently() { QElapsedTimer dt; dt.start(); const int numFiles = 100; // Use 1000 for performance testing const QString baseDir = homeTmpDir(); const QList urls = createManyFiles(baseDir, numFiles); QCOMPARE(urls.count(), numFiles); for (int i = 0; i < numFiles; ++i) { // delete each file independently. lots of jobs. this stress-tests kio scheduling. const QUrl url = urls.at(i); const QString file = url.toLocalFile(); QVERIFY(QFile::exists(file)); //qDebug() << file; KIO::Job *job = KIO::del(url, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(file)); } qDebug() << "Deleted" << numFiles << "files in" << dt.elapsed() << "milliseconds"; } void JobTest::deleteManyFilesTogether(bool using_fast_path) { extern KIOCORE_EXPORT bool kio_resolve_local_urls; kio_resolve_local_urls = !using_fast_path; QElapsedTimer dt; dt.start(); const int numFiles = 100; // Use 1000 for performance testing const QString baseDir = homeTmpDir(); const QList urls = createManyFiles(baseDir, numFiles); QCOMPARE(urls.count(), numFiles); //qDebug() << file; KIO::Job *job = KIO::del(urls, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); qDebug() << "Deleted" << numFiles << "files in" << dt.elapsed() << "milliseconds"; kio_resolve_local_urls = true; } void JobTest::deleteManyFilesTogether() { deleteManyFilesTogether(true); deleteManyFilesTogether(false); } void JobTest::rmdirEmpty() { const QString dir = homeTmpDir() + "dir"; QDir().mkdir(dir); QVERIFY(QFile::exists(dir)); KIO::Job *job = KIO::rmdir(QUrl::fromLocalFile(dir)); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(dir)); } void JobTest::rmdirNotEmpty() { const QString dir = homeTmpDir() + "dir"; createTestDirectory(dir); createTestDirectory(dir + "/subdir"); KIO::Job *job = KIO::rmdir(QUrl::fromLocalFile(dir)); QVERIFY(!job->exec()); QVERIFY(QFile::exists(dir)); } void JobTest::stat() { #if 1 const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); const QUrl url(QUrl::fromLocalFile(filePath)); KIO::StatJob *job = KIO::stat(url, KIO::HideProgressInfo); QVERIFY(job); QVERIFY2(job->exec(), qPrintable(job->errorString())); // TODO set setSide, setDetails const KIO::UDSEntry &entry = job->statResult(); QVERIFY(!entry.isDir()); QVERIFY(!entry.isLink()); QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QStringLiteral("fileFromHome")); // Compare what we get via kio_file and what we get when KFileItem stat()s directly const KFileItem kioItem(entry, url); const KFileItem fileItem(url); QCOMPARE(kioItem.name(), fileItem.name()); QCOMPARE(kioItem.url(), fileItem.url()); QCOMPARE(kioItem.size(), fileItem.size()); QCOMPARE(kioItem.user(), fileItem.user()); QCOMPARE(kioItem.group(), fileItem.group()); QCOMPARE(kioItem.mimetype(), fileItem.mimetype()); QCOMPARE(kioItem.permissions(), fileItem.permissions()); QCOMPARE(kioItem.time(KFileItem::ModificationTime), fileItem.time(KFileItem::ModificationTime)); QCOMPARE(kioItem.time(KFileItem::AccessTime), fileItem.time(KFileItem::AccessTime)); #else // Testing stat over HTTP KIO::StatJob *job = KIO::stat(QUrl("http://www.kde.org"), KIO::HideProgressInfo); QVERIFY(job); QVERIFY2(job->exec(), qPrintable(job->errorString())); // TODO set setSide, setDetails const KIO::UDSEntry &entry = job->statResult(); QVERIFY(!entry.isDir()); QVERIFY(!entry.isLink()); QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QString()); #endif } #ifndef Q_OS_WIN void JobTest::statSymlink() { const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); const QString symlink = otherTmpDir() + "link"; QVERIFY(QFile(filePath).link(symlink)); QVERIFY(QFile::exists(symlink)); setTimeStamp(symlink, QDateTime::currentDateTime().addSecs(-20)); // differentiate link time and source file time const QUrl url(QUrl::fromLocalFile(symlink)); KIO::StatJob *job = KIO::stat(url, KIO::HideProgressInfo); QVERIFY(job); QVERIFY2(job->exec(), qPrintable(job->errorString())); // TODO set setSide, setDetails const KIO::UDSEntry &entry = job->statResult(); QVERIFY(!entry.isDir()); QVERIFY(entry.isLink()); QCOMPARE(entry.stringValue(KIO::UDSEntry::UDS_NAME), QStringLiteral("link")); // Compare what we get via kio_file and what we get when KFileItem stat()s directly const KFileItem kioItem(entry, url); const KFileItem fileItem(url); QCOMPARE(kioItem.name(), fileItem.name()); QCOMPARE(kioItem.url(), fileItem.url()); QVERIFY(kioItem.isLink()); QVERIFY(fileItem.isLink()); QCOMPARE(kioItem.linkDest(), fileItem.linkDest()); QCOMPARE(kioItem.size(), fileItem.size()); QCOMPARE(kioItem.user(), fileItem.user()); QCOMPARE(kioItem.group(), fileItem.group()); QCOMPARE(kioItem.mimetype(), fileItem.mimetype()); QCOMPARE(kioItem.permissions(), fileItem.permissions()); QCOMPARE(kioItem.time(KFileItem::ModificationTime), fileItem.time(KFileItem::ModificationTime)); QCOMPARE(kioItem.time(KFileItem::AccessTime), fileItem.time(KFileItem::AccessTime)); } #endif void JobTest::mostLocalUrl() { const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); KIO::StatJob *job = KIO::mostLocalUrl(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo); QVERIFY(job); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(job->mostLocalUrl().toLocalFile(), filePath); } void JobTest::chmodFile() { const QString filePath = homeTmpDir() + "fileForChmod"; createTestFile(filePath); KFileItem item(QUrl::fromLocalFile(filePath)); const mode_t origPerm = item.permissions(); mode_t newPerm = origPerm ^ S_IWGRP; QVERIFY(newPerm != origPerm); KFileItemList items; items << item; KIO::Job *job = KIO::chmod(items, newPerm, S_IWGRP /*TODO: QFile::WriteGroup*/, QString(), QString(), false, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); KFileItem newItem(QUrl::fromLocalFile(filePath)); QCOMPARE(QString::number(newItem.permissions(), 8), QString::number(newPerm, 8)); QFile::remove(filePath); } #ifdef Q_OS_UNIX void JobTest::chmodSticky() { const QString dirPath = homeTmpDir() + "dirForChmodSticky"; QDir().mkpath(dirPath); KFileItem item(QUrl::fromLocalFile(dirPath)); const mode_t origPerm = item.permissions(); mode_t newPerm = origPerm ^ S_ISVTX; QVERIFY(newPerm != origPerm); KFileItemList items({item}); KIO::Job *job = KIO::chmod(items, newPerm, S_ISVTX, QString(), QString(), false, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); KFileItem newItem(QUrl::fromLocalFile(dirPath)); QCOMPARE(QString::number(newItem.permissions(), 8), QString::number(newPerm, 8)); QVERIFY(QDir().rmdir(dirPath)); } #endif void JobTest::chmodFileError() { // chown(root) should fail const QString filePath = homeTmpDir() + "fileForChmod"; createTestFile(filePath); KFileItem item(QUrl::fromLocalFile(filePath)); const mode_t origPerm = item.permissions(); mode_t newPerm = origPerm ^ S_IWGRP; QVERIFY(newPerm != origPerm); KFileItemList items; items << item; KIO::Job *job = KIO::chmod(items, newPerm, S_IWGRP /*TODO: QFile::WriteGroup*/, QStringLiteral("root"), QString(), false, KIO::HideProgressInfo); // Simulate the user pressing "Skip" in the dialog. PredefinedAnswerJobUiDelegate extension; extension.m_skipResult = KIO::Result_Skip; job->setUiDelegateExtension(&extension); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(extension.m_askSkipCalled, 1); KFileItem newItem(QUrl::fromLocalFile(filePath)); // We skipped, so the chmod didn't happen. QCOMPARE(QString::number(newItem.permissions(), 8), QString::number(origPerm, 8)); QFile::remove(filePath); } void JobTest::mimeType() { #if 1 const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); KIO::MimetypeJob *job = KIO::mimetype(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo); QVERIFY(job); QSignalSpy spyMimeType(job, SIGNAL(mimetype(KIO::Job*,QString))); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(spyMimeType.count(), 1); QCOMPARE(spyMimeType[0][0], QVariant::fromValue(static_cast(job))); QCOMPARE(spyMimeType[0][1].toString(), QStringLiteral("application/octet-stream")); #else // Testing mimetype over HTTP KIO::MimetypeJob *job = KIO::mimetype(QUrl("http://www.kde.org"), KIO::HideProgressInfo); QVERIFY(job); QSignalSpy spyMimeType(job, SIGNAL(mimetype(KIO::Job*,QString))); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(spyMimeType.count(), 1); QCOMPARE(spyMimeType[0][0], QVariant::fromValue(static_cast(job))); QCOMPARE(spyMimeType[0][1].toString(), QString("text/html")); #endif } void JobTest::mimeTypeError() { // KIO::mimetype() on a file that doesn't exist const QString filePath = homeTmpDir() + "doesNotExist"; KIO::MimetypeJob *job = KIO::mimetype(QUrl::fromLocalFile(filePath), KIO::HideProgressInfo); QVERIFY(job); QSignalSpy spyMimeType(job, SIGNAL(mimetype(KIO::Job*,QString))); QSignalSpy spyResult(job, SIGNAL(result(KJob*))); QVERIFY(!job->exec()); QCOMPARE(spyMimeType.count(), 0); QCOMPARE(spyResult.count(), 1); } void JobTest::moveFileDestAlreadyExists() // #157601 { const QString file1 = homeTmpDir() + "fileFromHome"; createTestFile(file1); const QString file2 = homeTmpDir() + "anotherFile"; createTestFile(file2); const QString existingDest = otherTmpDir() + "fileFromHome"; createTestFile(existingDest); QList urls; urls << QUrl::fromLocalFile(file1) << QUrl::fromLocalFile(file2); KIO::CopyJob *job = KIO::move(urls, QUrl::fromLocalFile(otherTmpDir()), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); job->setAutoSkip(true); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(file1)); // it was skipped QVERIFY(!QFile::exists(file2)); // it was moved } void JobTest::moveDestAlreadyExistsAutoRename_data() { QTest::addColumn("samePartition"); QTest::addColumn("moveDirs"); QTest::newRow("files same partition") << true << false; QTest::newRow("files other partition") << false << false; QTest::newRow("dirs same partition") << true << true; QTest::newRow("dirs other partition") << false << true; } void JobTest::moveDestAlreadyExistsAutoRename() { QFETCH(bool, samePartition); QFETCH(bool, moveDirs); QString dir; if (samePartition) { dir = homeTmpDir() + "dir/"; QVERIFY(QDir(dir).exists() || QDir().mkdir(dir)); } else { dir = otherTmpDir(); } moveDestAlreadyExistsAutoRename(dir, moveDirs); if (samePartition) { // cleanup KIO::Job *job = KIO::del(QUrl::fromLocalFile(dir), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(dir)); } } void JobTest::moveDestAlreadyExistsAutoRename(const QString &destDir, bool moveDirs) // #256650 { const QString prefix = moveDirs ? QStringLiteral("dir ") : QStringLiteral("file "); const QString file1 = homeTmpDir() + prefix + "(1)"; const QString file2 = homeTmpDir() + prefix + "(2)"; const QString existingDest1 = destDir + prefix + "(1)"; const QString existingDest2 = destDir + prefix + "(2)"; const QStringList sources = QStringList() << file1 << file2 << existingDest1 << existingDest2; for (const QString &source : sources) { if (moveDirs) { QVERIFY(QDir().mkdir(source)); } else { createTestFile(source); } } QList urls = {QUrl::fromLocalFile(file1), QUrl::fromLocalFile(file2)}; KIO::CopyJob *job = KIO::move(urls, QUrl::fromLocalFile(destDir), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); job->setAutoRename(true); QSignalSpy spyRenamed(job, &KIO::CopyJob::renamed); //qDebug() << QDir(destDir).entryList(); QVERIFY2(job->exec(), qPrintable(job->errorString())); qDebug() << QDir(destDir).entryList(); QVERIFY(!QFile::exists(file1)); // it was moved QVERIFY(!QFile::exists(file2)); // it was moved QVERIFY(QFile::exists(existingDest1)); QVERIFY(QFile::exists(existingDest2)); const QString file3 = destDir + prefix + "(3)"; const QString file4 = destDir + prefix + "(4)"; QVERIFY(QFile::exists(file3)); QVERIFY(QFile::exists(file4)); QCOMPARE(spyRenamed.count(), 2); auto list = spyRenamed.takeFirst(); QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(destDir + prefix + "(1)")); QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(file3)); list = spyRenamed.takeFirst(); QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(destDir + prefix + "(2)")); QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(file4)); if (moveDirs) { QDir().rmdir(file1); QDir().rmdir(file2); QDir().rmdir(file3); QDir().rmdir(file4); } else { QFile::remove(file1); QFile::remove(file2); QFile::remove(file3); QFile::remove(file4); } } void JobTest::copyDirectoryAlreadyExistsSkip() { // when copying a directory (which contains at least one file) to some location, and then // copying the same dir to the same location again, and clicking "Skip" there should be no // segmentation fault, bug 408350 const QString src = homeTmpDir() + "a"; createTestDirectory(src); const QString dest = homeTmpDir() + "dest"; createTestDirectory(dest); QUrl u = QUrl::fromLocalFile(src); QUrl d = QUrl::fromLocalFile(dest); KIO::Job *job = KIO::copy(u, d, KIO::HideProgressInfo); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest + QStringLiteral("/a/testfile"))); job = KIO::copy(u, d, KIO::HideProgressInfo); // Simulate the user pressing "Skip" in the dialog. PredefinedAnswerJobUiDelegate extension; extension.m_skipResult = KIO::Result_Skip; job->setUiDelegateExtension(&extension); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(dest + QStringLiteral("/a/testfile"))); QDir(src).removeRecursively(); QDir(dest).removeRecursively(); } void JobTest::copyFileAlreadyExistsRename() { const QString sourceFile = homeTmpDir() + "file"; const QString dest = homeTmpDir() + "dest/"; const QString alreadyExisting = dest + "file"; const QString renamedFile = dest + "file-renamed"; createTestFile(sourceFile); createTestFile(alreadyExisting); QVERIFY(QFile::exists(sourceFile)); QVERIFY(QFile::exists(alreadyExisting)); createTestDirectory(dest); QUrl s = QUrl::fromLocalFile(sourceFile); QUrl d = QUrl::fromLocalFile(dest); KIO::CopyJob* job = KIO::copy(s, d, KIO::HideProgressInfo); // Simulate the user pressing "Rename" in the dialog and choosing another destination. PredefinedAnswerJobUiDelegate extension; extension.m_renameResult = KIO::Result_Rename; extension.m_renamedest = renamedFile; job->setUiDelegateExtension(&extension); QSignalSpy spyRenamed(job, &KIO::CopyJob::renamed); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(renamedFile)); QCOMPARE(spyRenamed.count(), 1); auto list = spyRenamed.takeFirst(); QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(alreadyExisting)); QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(renamedFile)); QVERIFY(QFile(sourceFile).remove()); QVERIFY(QDir(dest).removeRecursively()); } void JobTest::safeOverwrite_data() { QTest::addColumn("destFileExists"); QTest::newRow("dest file exists") << true; QTest::newRow("dest file doesn't exist") << false; } void JobTest::safeOverwrite() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif QFETCH(bool, destFileExists); const QString srcDir = homeTmpDir() + "overwrite"; const QString srcFile = srcDir + "/testfile"; const QString destDir = otherTmpDir() + "overwrite_other"; const QString destFile = destDir + "/testfile"; const QString destPartFile = destFile + ".part"; createTestDirectory(srcDir); createTestDirectory(destDir); QVERIFY(QFile::resize(srcFile, 1000000)); //~1MB if (!destFileExists) { QVERIFY(QFile::remove(destFile)); } if (otherTmpDirIsOnSamePartition()) { QSKIP(qPrintable(QStringLiteral("This test requires %1 and %2 to be on different partitions").arg(srcDir, destDir))); } KIO::FileCopyJob *job = KIO::file_move(QUrl::fromLocalFile(srcFile), QUrl::fromLocalFile(destFile), -1, KIO::HideProgressInfo | KIO::Overwrite); job->setUiDelegate(nullptr); QSignalSpy spyTotalSize(job, &KIO::FileCopyJob::totalSize); connect(job, &KIO::FileCopyJob::totalSize, this, [destFileExists, destPartFile](KJob *job, qulonglong totalSize) { Q_UNUSED(job); if (totalSize > 0) { QCOMPARE(destFileExists, QFile::exists(destPartFile)); } }); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFile::exists(destFile)); QVERIFY(!QFile::exists(srcFile)); QVERIFY(!QFile::exists(destPartFile)); QCOMPARE(spyTotalSize.count(), 1); QDir(srcDir).removeRecursively(); QDir(destDir).removeRecursively(); } void JobTest::moveAndOverwrite() { const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); QString existingDest = otherTmpDir() + "fileFromHome"; createTestFile(existingDest); KIO::FileCopyJob *job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(sourceFile)); // it was moved #ifndef Q_OS_WIN // Now same thing when the target is a symlink to the source createTestFile(sourceFile); createTestSymlink(existingDest, QFile::encodeName(sourceFile)); QVERIFY(QFile::exists(existingDest)); job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(sourceFile)); // it was moved // Now same thing when the target is a symlink to another file createTestFile(sourceFile); createTestFile(sourceFile + QLatin1Char('2')); createTestSymlink(existingDest, QFile::encodeName(sourceFile + QLatin1Char('2'))); QVERIFY(QFile::exists(existingDest)); job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(sourceFile)); // it was moved // Now same thing when the target is a _broken_ symlink createTestFile(sourceFile); createTestSymlink(existingDest); QVERIFY(!QFile::exists(existingDest)); // it exists, but it's broken... job = KIO::file_move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), -1, KIO::HideProgressInfo | KIO::Overwrite); job->setUiDelegate(nullptr); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(!QFile::exists(sourceFile)); // it was moved #endif } void JobTest::moveOverSymlinkToSelf() // #169547 { #ifndef Q_OS_WIN const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString existingDest = homeTmpDir() + "testlink"; createTestSymlink(existingDest, QFile::encodeName(sourceFile)); QVERIFY(QFile::exists(existingDest)); KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(existingDest), KIO::HideProgressInfo); job->setUiDelegate(nullptr); job->setUiDelegateExtension(nullptr); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_FILE_ALREADY_EXIST); // and not ERR_IDENTICAL_FILES! QVERIFY(QFile::exists(sourceFile)); // it not moved #endif } void JobTest::createSymlink() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString destDir = homeTmpDir() + "dest"; QVERIFY(QDir().mkpath(destDir)); // With KIO::link (high-level) KIO::CopyJob *job = KIO::link(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(destDir), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo::exists(sourceFile)); const QString dest = destDir + "/fileFromHome"; QVERIFY(QFileInfo(dest).isSymLink()); QCOMPARE(QFileInfo(dest).symLinkTarget(), sourceFile); QFile::remove(dest); // With KIO::symlink (low-level) const QString linkPath = destDir + "/link"; KIO::Job *symlinkJob = KIO::symlink(sourceFile, QUrl::fromLocalFile(linkPath), KIO::HideProgressInfo); QVERIFY2(symlinkJob->exec(), qPrintable(symlinkJob->errorString())); QVERIFY(QFileInfo::exists(sourceFile)); QVERIFY(QFileInfo(linkPath).isSymLink()); QCOMPARE(QFileInfo(linkPath).symLinkTarget(), sourceFile); // Cleanup QVERIFY(QDir(destDir).removeRecursively()); } void JobTest::createSymlinkTargetDirDoesntExist() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString destDir = homeTmpDir() + "dest/does/not/exist"; KIO::CopyJob *job = KIO::link(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(destDir), KIO::HideProgressInfo); QVERIFY(!job->exec()); QCOMPARE(job->error(), static_cast(KIO::ERR_CANNOT_SYMLINK)); } void JobTest::createSymlinkAsShouldSucceed() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString dest = homeTmpDir() + "testlink"; QFile::remove(dest); // just in case KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo::exists(sourceFile)); QVERIFY(QFileInfo(dest).isSymLink()); QVERIFY(QFile::remove(dest)); } void JobTest::createSymlinkAsShouldFailDirectoryExists() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString dest = homeTmpDir() + "dest"; QVERIFY(QDir().mkpath(dest)); // dest exists as a directory // With KIO::link (high-level) KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_DIR_ALREADY_EXIST); QVERIFY(QFileInfo::exists(sourceFile)); QVERIFY(!QFileInfo::exists(dest + "/fileFromHome")); // With KIO::symlink (low-level) KIO::Job *symlinkJob = KIO::symlink(sourceFile, QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY(!symlinkJob->exec()); QCOMPARE(symlinkJob->error(), (int)KIO::ERR_DIR_ALREADY_EXIST); QVERIFY(QFileInfo::exists(sourceFile)); // Cleanup QVERIFY(QDir().rmdir(dest)); } void JobTest::createSymlinkAsShouldFailFileExists() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString dest = homeTmpDir() + "testlink"; QFile::remove(dest); // just in case // First time works KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo(dest).isSymLink()); // Second time fails (already exists) job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_FILE_ALREADY_EXIST); // KIO::symlink fails too KIO::Job *symlinkJob = KIO::symlink(sourceFile, QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY(!symlinkJob->exec()); QCOMPARE(symlinkJob->error(), (int)KIO::ERR_FILE_ALREADY_EXIST); // Cleanup QVERIFY(QFile::remove(sourceFile)); QVERIFY(QFile::remove(dest)); } void JobTest::createSymlinkWithOverwriteShouldWork() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = homeTmpDir() + "fileFromHome"; createTestFile(sourceFile); const QString dest = homeTmpDir() + "testlink"; QFile::remove(dest); // just in case // First time works KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo(dest).isSymLink()); // Changing the link target, with overwrite, works job = KIO::linkAs(QUrl::fromLocalFile(sourceFile + QLatin1Char('2')), QUrl::fromLocalFile(dest), KIO::Overwrite | KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo(dest).isSymLink()); QCOMPARE(QFileInfo(dest).symLinkTarget(), QString(sourceFile + QLatin1Char('2'))); // Changing the link target using KIO::symlink, with overwrite, works KIO::Job *symlinkJob = KIO::symlink(sourceFile + QLatin1Char('3'), QUrl::fromLocalFile(dest), KIO::Overwrite | KIO::HideProgressInfo); QVERIFY2(symlinkJob->exec(), qPrintable(symlinkJob->errorString())); QVERIFY(QFileInfo(dest).isSymLink()); QCOMPARE(QFileInfo(dest).symLinkTarget(), QString(sourceFile + QLatin1Char('3'))); // Cleanup QVERIFY(QFile::remove(dest)); QVERIFY(QFile::remove(sourceFile)); } void JobTest::createBrokenSymlink() { #ifdef Q_OS_WIN QSKIP("Test skipped on Windows"); #endif const QString sourceFile = "/does/not/exist"; const QString dest = homeTmpDir() + "testlink"; QFile::remove(dest); // just in case KIO::CopyJob *job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY2(job->exec(), qPrintable(job->errorString())); QVERIFY(QFileInfo(dest).isSymLink()); // Second time fails (already exists) job = KIO::linkAs(QUrl::fromLocalFile(sourceFile), QUrl::fromLocalFile(dest), KIO::HideProgressInfo); QVERIFY(!job->exec()); QCOMPARE(job->error(), (int)KIO::ERR_FILE_ALREADY_EXIST); QVERIFY(QFile::remove(dest)); } void JobTest::multiGet() { const int numFiles = 10; const QString baseDir = homeTmpDir(); const QList urls = createManyFiles(baseDir, numFiles); QCOMPARE(urls.count(), numFiles); //qDebug() << file; KIO::MultiGetJob *job = KIO::multi_get(0, urls.at(0), KIO::MetaData()); // TODO: missing KIO::HideProgressInfo QSignalSpy spyData(job, SIGNAL(data(long,QByteArray))); QSignalSpy spyMimeType(job, SIGNAL(mimetype(long,QString))); QSignalSpy spyResultId(job, SIGNAL(result(long))); QSignalSpy spyResult(job, SIGNAL(result(KJob*))); job->setUiDelegate(nullptr); for (int i = 1; i < numFiles; ++i) { const QUrl url = urls.at(i); job->get(i, url, KIO::MetaData()); } //connect(job, &KIO::MultiGetJob::result, [=] (long id) { qDebug() << "ID I got" << id;}); //connect(job, &KJob::result, [this](KJob* ) {qDebug() << "END";}); QVERIFY2(job->exec(), qPrintable(job->errorString())); QCOMPARE(spyResult.count(), 1); QCOMPARE(spyResultId.count(), numFiles); QCOMPARE(spyMimeType.count(), numFiles); QCOMPARE(spyData.count(), numFiles * 2); for (int i = 0; i < numFiles; ++i) { QCOMPARE(spyResultId.at(i).at(0).toInt(), i); QCOMPARE(spyMimeType.at(i).at(0).toInt(), i); QCOMPARE(spyMimeType.at(i).at(1).toString(), QStringLiteral("text/plain")); QCOMPARE(spyData.at(i * 2).at(0).toInt(), i); QCOMPARE(QString(spyData.at(i * 2).at(1).toByteArray()), QStringLiteral("Hello")); QCOMPARE(spyData.at(i * 2 + 1).at(0).toInt(), i); QCOMPARE(QString(spyData.at(i * 2 + 1).at(1).toByteArray()), QLatin1String("")); } } void JobTest::cancelCopyAndCleanDest_data() { QTest::addColumn("suspend"); QTest::addColumn("overwrite"); QTest::newRow("suspend_no_overwrite") << true << false; QTest::newRow("no_suspend_no_overwrite") << false << false; #ifndef Q_OS_WIN QTest::newRow("suspend_with_overwrite") << true << true; QTest::newRow("no_suspend_with_overwrite") << false << true; #endif } void JobTest::cancelCopyAndCleanDest() { QFETCH(bool, suspend); QFETCH(bool, overwrite); const QString baseDir = homeTmpDir(); const QString srcTemplate = baseDir + QStringLiteral("testfile_XXXXXX"); - const QString destFile = baseDir + QStringLiteral("testfile_copy_") + QString::fromLatin1(QTest::currentDataTag()); + const QString destFile = baseDir + QStringLiteral("testfile_copy_slow_") + QString::fromLatin1(QTest::currentDataTag()); QTemporaryFile f(srcTemplate); if (!f.open()) { qFatal("Couldn't open %s", qPrintable(f.fileName())); } - f.seek(9999999); + f.seek(999999); f.write("0"); f.close(); - QCOMPARE(f.size(), 10000000); //~10MB + QCOMPARE(f.size(), 1000000); //~1MB if (overwrite) { createTestFile(destFile); } + const QString destToCheck = (overwrite) ? destFile + QStringLiteral(".part") : destFile; KIO::JobFlag m_overwriteFlag = overwrite ? KIO::Overwrite : KIO::DefaultFlags; KIO::FileCopyJob *copyJob = KIO::file_copy(QUrl::fromLocalFile(f.fileName()), QUrl::fromLocalFile(destFile), -1, KIO::HideProgressInfo | m_overwriteFlag); copyJob->setUiDelegate(nullptr); QSignalSpy spyProcessedSize(copyJob, &KIO::Job::processedSize); - connect(copyJob, &KIO::Job::processedSize, this, [destFile, suspend, overwrite](KJob *job, qulonglong processedSize) { + QSignalSpy spyFinished(copyJob, &KIO::Job::finished); + connect(copyJob, &KIO::Job::processedSize, this, [destFile, suspend, destToCheck](KJob *job, qulonglong processedSize) { if (processedSize > 0) { - const QString destToCheck = (!overwrite) ? destFile : destFile + QStringLiteral(".part"); QVERIFY2(QFile::exists(destToCheck), qPrintable(destToCheck)); if (suspend) { job->suspend(); } - job->kill(); - QVERIFY(!QFile::exists(destToCheck)); + QVERIFY(job->kill()); } }); + QVERIFY(!copyJob->exec()); QCOMPARE(spyProcessedSize.count(), 1); - // Give time to the kioslave to finish copy() and warn about chown/chmod failing (because FileCopyJob::doKill removed the file) - // Less confusing if the warnings show here. - QTest::qWait(500); - QVERIFY(!QFile::exists(destFile)); + QCOMPARE(spyFinished.count(), 1); + QCOMPARE(copyJob->error(), KIO::ERR_USER_CANCELED); + + // the destination file actual deletion happens after finished() is emitted + // we need to give some time to the ioslave to finish the file cleaning + QTRY_VERIFY2(!QFile::exists(destToCheck), qPrintable(destToCheck)); } diff --git a/src/core/filecopyjob.cpp b/src/core/filecopyjob.cpp index f13cde4c..c15acb8b 100644 --- a/src/core/filecopyjob.cpp +++ b/src/core/filecopyjob.cpp @@ -1,615 +1,621 @@ /* This file is part of the KDE libraries Copyright (C) 2000 Stephan Kulow 2000-2009 David Faure This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "filecopyjob.h" #include "job_p.h" #include #include #include "kprotocolmanager.h" #include "scheduler.h" #include "slave.h" #include using namespace KIO; static inline Slave *jobSlave(SimpleJob *job) { return SimpleJobPrivate::get(job)->m_slave; } /** @internal */ class KIO::FileCopyJobPrivate: public KIO::JobPrivate { public: FileCopyJobPrivate(const QUrl &src, const QUrl &dest, int permissions, bool move, JobFlags flags) : m_sourceSize(filesize_t(-1)), m_src(src), m_dest(dest), m_moveJob(nullptr), m_copyJob(nullptr), m_delJob(nullptr), m_chmodJob(nullptr), m_getJob(nullptr), m_putJob(nullptr), m_permissions(permissions), m_move(move), m_mustChmod(0), m_bFileCopyInProgress(false), m_flags(flags) { } KIO::filesize_t m_sourceSize; QDateTime m_modificationTime; QUrl m_src; QUrl m_dest; QByteArray m_buffer; SimpleJob *m_moveJob; SimpleJob *m_copyJob; SimpleJob *m_delJob; SimpleJob *m_chmodJob; TransferJob *m_getJob; TransferJob *m_putJob; int m_permissions; bool m_move: 1; bool m_canResume: 1; bool m_resumeAnswerSent: 1; bool m_mustChmod: 1; bool m_bFileCopyInProgress: 1; JobFlags m_flags; void startBestCopyMethod(); void startCopyJob(); void startCopyJob(const QUrl &slave_url); void startRenameJob(const QUrl &slave_url); void startDataPump(); void connectSubjob(SimpleJob *job); void slotStart(); void slotData(KIO::Job *, const QByteArray &data); void slotDataReq(KIO::Job *, QByteArray &data); void slotMimetype(KIO::Job *, const QString &type); /** * Forward signal from subjob * @param job the job that emitted this signal * @param offset the offset to resume from */ void slotCanResume(KIO::Job *job, KIO::filesize_t offset); Q_DECLARE_PUBLIC(FileCopyJob) static inline FileCopyJob *newJob(const QUrl &src, const QUrl &dest, int permissions, bool move, JobFlags flags) { //qDebug() << src << "->" << dest; FileCopyJob *job = new FileCopyJob( *new FileCopyJobPrivate(src, dest, permissions, move, flags)); job->setProperty("destUrl", dest.toString()); job->setUiDelegate(KIO::createDefaultJobUiDelegate()); if (!(flags & HideProgressInfo)) { KIO::getJobTracker()->registerJob(job); } if (!(flags & NoPrivilegeExecution)) { job->d_func()->m_privilegeExecutionEnabled = true; job->d_func()->m_operationType = move ? Move : Copy; } return job; } }; /* * The FileCopyJob works according to the famous Bavarian * 'Alternating Bitburger Protocol': we either drink a beer or we * we order a beer, but never both at the same time. * Translated to io-slaves: We alternate between receiving a block of data * and sending it away. */ FileCopyJob::FileCopyJob(FileCopyJobPrivate &dd) : Job(dd) { Q_D(FileCopyJob); QTimer::singleShot(0, this, [d]() { d->slotStart(); }); } void FileCopyJobPrivate::slotStart() { Q_Q(FileCopyJob); if (!m_move) { JobPrivate::emitCopying(q, m_src, m_dest); } else { JobPrivate::emitMoving(q, m_src, m_dest); } if (m_move) { // The if() below must be the same as the one in startBestCopyMethod if ((m_src.scheme() == m_dest.scheme()) && (m_src.host() == m_dest.host()) && (m_src.port() == m_dest.port()) && (m_src.userName() == m_dest.userName()) && (m_src.password() == m_dest.password())) { startRenameJob(m_src); return; } else if (m_src.isLocalFile() && KProtocolManager::canRenameFromFile(m_dest)) { startRenameJob(m_dest); return; } else if (m_dest.isLocalFile() && KProtocolManager::canRenameToFile(m_src)) { startRenameJob(m_src); return; } // No fast-move available, use copy + del. } startBestCopyMethod(); } void FileCopyJobPrivate::startBestCopyMethod() { if ((m_src.scheme() == m_dest.scheme()) && (m_src.host() == m_dest.host()) && (m_src.port() == m_dest.port()) && (m_src.userName() == m_dest.userName()) && (m_src.password() == m_dest.password())) { startCopyJob(); } else if (m_src.isLocalFile() && KProtocolManager::canCopyFromFile(m_dest)) { startCopyJob(m_dest); } else if (m_dest.isLocalFile() && KProtocolManager::canCopyToFile(m_src) && !KIO::Scheduler::isSlaveOnHoldFor(m_src)) { startCopyJob(m_src); } else { startDataPump(); } } FileCopyJob::~FileCopyJob() { } void FileCopyJob::setSourceSize(KIO::filesize_t size) { Q_D(FileCopyJob); d->m_sourceSize = size; if (size != (KIO::filesize_t) - 1) { setTotalAmount(KJob::Bytes, size); } } void FileCopyJob::setModificationTime(const QDateTime &mtime) { Q_D(FileCopyJob); d->m_modificationTime = mtime; } QUrl FileCopyJob::srcUrl() const { return d_func()->m_src; } QUrl FileCopyJob::destUrl() const { return d_func()->m_dest; } void FileCopyJobPrivate::startCopyJob() { startCopyJob(m_src); } void FileCopyJobPrivate::startCopyJob(const QUrl &slave_url) { Q_Q(FileCopyJob); //qDebug(); KIO_ARGS << m_src << m_dest << m_permissions << (qint8)(m_flags & Overwrite); auto job = new DirectCopyJob(slave_url, packedArgs); m_copyJob = job; m_copyJob->setParentJob(q); if (m_modificationTime.isValid()) { m_copyJob->addMetaData(QStringLiteral("modified"), m_modificationTime.toString(Qt::ISODate)); // #55804 } q->addSubjob(m_copyJob); connectSubjob(m_copyJob); q->connect(job, &DirectCopyJob::canResume, q, [this](KIO::Job *job, KIO::filesize_t offset) { slotCanResume(job, offset); }); } void FileCopyJobPrivate::startRenameJob(const QUrl &slave_url) { Q_Q(FileCopyJob); m_mustChmod = true; // CMD_RENAME by itself doesn't change permissions KIO_ARGS << m_src << m_dest << (qint8)(m_flags & Overwrite); m_moveJob = SimpleJobPrivate::newJobNoUi(slave_url, CMD_RENAME, packedArgs); m_moveJob->setParentJob(q); if (m_modificationTime.isValid()) { m_moveJob->addMetaData(QStringLiteral("modified"), m_modificationTime.toString(Qt::ISODate)); // #55804 } q->addSubjob(m_moveJob); connectSubjob(m_moveJob); } void FileCopyJobPrivate::connectSubjob(SimpleJob *job) { Q_Q(FileCopyJob); q->connect(job, &KJob::totalSize, q, [q](KJob *job, qulonglong totalSize) { Q_UNUSED(job); if (totalSize != q->totalAmount(KJob::Bytes)) { q->setTotalAmount(KJob::Bytes, totalSize); } }); q->connect(job, &KJob::processedSize, q, [q, this](KJob *job, qulonglong processedSize) { if (job == m_copyJob) { m_bFileCopyInProgress = processedSize > 0; } q->setProcessedAmount(KJob::Bytes, processedSize); }); q->connect(job, QOverload::of(&KJob::percent), q, [q](KJob *job, ulong percent) { Q_UNUSED(job); if (percent > q->percent()) { q->setPercent(percent); } }); if (q->isSuspended()) { job->suspend(); } } bool FileCopyJob::doSuspend() { Q_D(FileCopyJob); if (d->m_moveJob) { d->m_moveJob->suspend(); } if (d->m_copyJob) { d->m_copyJob->suspend(); } if (d->m_getJob) { d->m_getJob->suspend(); } if (d->m_putJob) { d->m_putJob->suspend(); } Job::doSuspend(); return true; } bool FileCopyJob::doResume() { Q_D(FileCopyJob); if (d->m_moveJob) { d->m_moveJob->resume(); } if (d->m_copyJob) { d->m_copyJob->resume(); } if (d->m_getJob) { d->m_getJob->resume(); } if (d->m_putJob) { d->m_putJob->resume(); } Job::doResume(); return true; } void FileCopyJobPrivate::startDataPump() { Q_Q(FileCopyJob); //qDebug(); m_canResume = false; m_resumeAnswerSent = false; m_getJob = nullptr; // for now m_putJob = put(m_dest, m_permissions, (m_flags | HideProgressInfo) /* no GUI */); m_putJob->setParentJob(q); //qDebug() << "m_putJob=" << m_putJob << "m_dest=" << m_dest; if (m_modificationTime.isValid()) { m_putJob->setModificationTime(m_modificationTime); } // The first thing the put job will tell us is whether we can // resume or not (this is always emitted) q->connect(m_putJob, &KIO::TransferJob::canResume, q, [this](KIO::Job *job, KIO::filesize_t offset) { slotCanResume(job, offset); }); q->connect(m_putJob, &KIO::TransferJob::dataReq, q, [this](KIO::Job *job, QByteArray &data) { slotDataReq(job, data); }); q->addSubjob(m_putJob); } void FileCopyJobPrivate::slotCanResume(KIO::Job *job, KIO::filesize_t offset) { Q_Q(FileCopyJob); if (job == m_putJob || job == m_copyJob) { //qDebug() << "'can resume' from PUT job. offset=" << KIO::number(offset); if (offset) { RenameDialog_Result res = R_RESUME; if (!KProtocolManager::autoResume() && !(m_flags & Overwrite) && m_uiDelegateExtension) { QString newPath; KIO::Job *job = (q->parentJob()) ? q->parentJob() : q; // Ask confirmation about resuming previous transfer res = m_uiDelegateExtension->askFileRename( job, i18n("File Already Exists"), m_src, m_dest, RenameDialog_Options(RenameDialog_Overwrite | RenameDialog_Resume | RenameDialog_NoRename), newPath, m_sourceSize, offset); } if (res == R_OVERWRITE || (m_flags & Overwrite)) { offset = 0; } else if (res == R_CANCEL) { if (job == m_putJob) { m_putJob->kill(FileCopyJob::Quietly); q->removeSubjob(m_putJob); m_putJob = nullptr; } else { m_copyJob->kill(FileCopyJob::Quietly); q->removeSubjob(m_copyJob); m_copyJob = nullptr; } q->setError(ERR_USER_CANCELED); q->emitResult(); return; } } else { m_resumeAnswerSent = true; // No need for an answer } if (job == m_putJob) { m_getJob = KIO::get(m_src, NoReload, HideProgressInfo /* no GUI */); m_getJob->setParentJob(q); //qDebug() << "m_getJob=" << m_getJob << m_src; m_getJob->addMetaData(QStringLiteral("errorPage"), QStringLiteral("false")); m_getJob->addMetaData(QStringLiteral("AllowCompressedPage"), QStringLiteral("false")); // Set size in subjob. This helps if the slave doesn't emit totalSize. if (m_sourceSize != (KIO::filesize_t) - 1) { m_getJob->setTotalAmount(KJob::Bytes, m_sourceSize); } if (offset) { //qDebug() << "Setting metadata for resume to" << (unsigned long) offset; m_getJob->addMetaData(QStringLiteral("range-start"), KIO::number(offset)); // Might or might not get emitted q->connect(m_getJob, &KIO::TransferJob::canResume, q, [this](KIO::Job *job, KIO::filesize_t offset) { slotCanResume(job, offset); }); } jobSlave(m_putJob)->setOffset(offset); m_putJob->d_func()->internalSuspend(); q->addSubjob(m_getJob); connectSubjob(m_getJob); // Progress info depends on get m_getJob->d_func()->internalResume(); // Order a beer q->connect(m_getJob, &KIO::TransferJob::data, q, [this](KIO::Job *job, const QByteArray &data) { slotData(job, data); }); q->connect(m_getJob, QOverload::of(&KIO::TransferJob::mimetype), q, [this](KIO::Job *job, const QString &type) { slotMimetype(job, type); }); } else { // copyjob jobSlave(m_copyJob)->sendResumeAnswer(offset != 0); } } else if (job == m_getJob) { // Cool, the get job said ok, we can resume m_canResume = true; //qDebug() << "'can resume' from the GET job -> we can resume"; jobSlave(m_getJob)->setOffset(jobSlave(m_putJob)->offset()); } else { qCWarning(KIO_CORE) << "unknown job=" << job << "m_getJob=" << m_getJob << "m_putJob=" << m_putJob; } } void FileCopyJobPrivate::slotData(KIO::Job *, const QByteArray &data) { //qDebug() << "data size:" << data.size(); Q_ASSERT(m_putJob); if (!m_putJob) { return; // Don't crash } m_getJob->d_func()->internalSuspend(); m_putJob->d_func()->internalResume(); // Drink the beer m_buffer += data; // On the first set of data incoming, we tell the "put" slave about our // decision about resuming if (!m_resumeAnswerSent) { m_resumeAnswerSent = true; //qDebug() << "(first time) -> send resume answer " << m_canResume; jobSlave(m_putJob)->sendResumeAnswer(m_canResume); } } void FileCopyJobPrivate::slotDataReq(KIO::Job *, QByteArray &data) { Q_Q(FileCopyJob); //qDebug(); if (!m_resumeAnswerSent && !m_getJob) { // This can't happen q->setError(ERR_INTERNAL); q->setErrorText(QStringLiteral("'Put' job did not send canResume or 'Get' job did not send data!")); m_putJob->kill(FileCopyJob::Quietly); q->removeSubjob(m_putJob); m_putJob = nullptr; q->emitResult(); return; } if (m_getJob) { m_getJob->d_func()->internalResume(); // Order more beer m_putJob->d_func()->internalSuspend(); } data = m_buffer; m_buffer = QByteArray(); } void FileCopyJobPrivate::slotMimetype(KIO::Job *, const QString &type) { Q_Q(FileCopyJob); emit q->mimetype(q, type); } void FileCopyJob::slotResult(KJob *job) { Q_D(FileCopyJob); //qDebug() << "this=" << this << "job=" << job; removeSubjob(job); // If result comes from copyjob then we are not writing anymore. if (job == d->m_copyJob) { d->m_bFileCopyInProgress = false; } // Did job have an error ? if (job->error()) { if ((job == d->m_moveJob) && (job->error() == ERR_UNSUPPORTED_ACTION)) { d->m_moveJob = nullptr; d->startBestCopyMethod(); return; } else if ((job == d->m_copyJob) && (job->error() == ERR_UNSUPPORTED_ACTION)) { d->m_copyJob = nullptr; d->startDataPump(); return; } else if (job == d->m_getJob) { d->m_getJob = nullptr; if (d->m_putJob) { d->m_putJob->kill(Quietly); removeSubjob(d->m_putJob); } } else if (job == d->m_putJob) { d->m_putJob = nullptr; if (d->m_getJob) { d->m_getJob->kill(Quietly); removeSubjob(d->m_getJob); } } else if (job == d->m_chmodJob) { d->m_chmodJob = nullptr; if (d->m_delJob) { d->m_delJob->kill(Quietly); removeSubjob(d->m_delJob); } } else if (job == d->m_delJob) { d->m_delJob = nullptr; if (d->m_chmodJob) { d->m_chmodJob->kill(Quietly); removeSubjob(d->m_chmodJob); } } setError(job->error()); setErrorText(job->errorText()); emitResult(); return; } if (d->m_mustChmod) { // If d->m_permissions == -1, keep the default permissions if (d->m_permissions != -1) { d->m_chmodJob = chmod(d->m_dest, d->m_permissions); addSubjob(d->m_chmodJob); } d->m_mustChmod = false; } if (job == d->m_moveJob) { d->m_moveJob = nullptr; // Finished } if (job == d->m_copyJob) { d->m_copyJob = nullptr; if (d->m_move) { d->m_delJob = file_delete(d->m_src, HideProgressInfo/*no GUI*/); // Delete source addSubjob(d->m_delJob); } } if (job == d->m_getJob) { //qDebug() << "m_getJob finished"; d->m_getJob = nullptr; // No action required if (d->m_putJob) { d->m_putJob->d_func()->internalResume(); } } if (job == d->m_putJob) { //qDebug() << "m_putJob finished"; d->m_putJob = nullptr; if (d->m_getJob) { // The get job is still running, probably after emitting data(QByteArray()) // and before we receive its finished(). d->m_getJob->d_func()->internalResume(); } if (d->m_move) { d->m_delJob = file_delete(d->m_src, HideProgressInfo/*no GUI*/); // Delete source addSubjob(d->m_delJob); } } if (job == d->m_delJob) { d->m_delJob = nullptr; // Finished } if (job == d->m_chmodJob) { d->m_chmodJob = nullptr; // Finished } if (!hasSubjobs()) { emitResult(); } } bool FileCopyJob::doKill() { +#ifdef Q_OS_WIN +//TODO Use SetConsoleCtrlHandler on Windows or similar behaviour. +// https://stackoverflow.com/questions/2007516/is-there-a-posix-sigterm-alternative-on-windows-a-gentle-kill-for-console-ap +// https://danielkaes.wordpress.com/2009/06/04/how-to-catch-kill-events-with-python/ +// https://phabricator.kde.org/D25117#566107 + Q_D(FileCopyJob); // If we are interrupted in the middle of file copying, // we may end up with corrupted file at the destination. // It is better to clean up this file. If a copy is being // made as part of move operation then delete the dest only if // source file is intact (m_delJob == NULL). if (d->m_bFileCopyInProgress && d->m_copyJob && d->m_dest.isLocalFile()) { if (d->m_flags & Overwrite) { QFile::remove(d->m_dest.toLocalFile() + QStringLiteral(".part")); } else { QFile::remove(d->m_dest.toLocalFile()); } } - +#endif return Job::doKill(); } FileCopyJob *KIO::file_copy(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags) { return FileCopyJobPrivate::newJob(src, dest, permissions, false, flags); } FileCopyJob *KIO::file_move(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags) { FileCopyJob *job = FileCopyJobPrivate::newJob(src, dest, permissions, true, flags); if (job->uiDelegateExtension()) { job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent); } return job; } #include "moc_filecopyjob.cpp" diff --git a/src/ioslaves/file/file.cpp b/src/ioslaves/file/file.cpp index 55949ab2..2dc898b7 100644 --- a/src/ioslaves/file/file.cpp +++ b/src/ioslaves/file/file.cpp @@ -1,1573 +1,1574 @@ /* Copyright (C) 2000-2002 Stephan Kulow Copyright (C) 2000-2002 David Faure Copyright (C) 2000-2002 Waldo Bastian Copyright (C) 2006 Allan Sandfeld Jensen Copyright (C) 2007 Thiago Macieira This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License (LGPL) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "file.h" #include #include #include #include #include "kioglobal_p.h" #ifdef Q_OS_UNIX #include "legacycodec.h" #endif #include #include #ifdef Q_OS_WIN #include #include #include //struct timeval #else #include #endif #include #include #include #include #include #ifdef Q_OS_WIN #include #include #endif #include #include #include #include #include #include #include #include #if HAVE_STATX #include #endif #if HAVE_VOLMGT #include #include #endif #include #include Q_LOGGING_CATEGORY(KIO_FILE, "kf5.kio.kio_file") // Pseudo plugin class to embed meta data class KIOPluginForMetaData : public QObject { Q_OBJECT Q_PLUGIN_METADATA(IID "org.kde.kio.slave.file" FILE "file.json") }; using namespace KIO; #define MAX_IPC_SIZE (1024*32) static QString readLogFile(const QByteArray &_filename); #if HAVE_POSIX_ACL static void appendACLAtoms(const QByteArray &path, UDSEntry &entry, mode_t type); #endif extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv) { QCoreApplication app(argc, argv); // needed for QSocketNotifier app.setApplicationName(QStringLiteral("kio_file")); if (argc != 4) { fprintf(stderr, "Usage: kio_file protocol domain-socket1 domain-socket2\n"); exit(-1); } #ifdef Q_OS_UNIX // From Qt doc : "Note that you should not delete codecs yourself: once created they become Qt's responsibility" (void)new LegacyCodec; #endif FileProtocol slave(argv[2], argv[3]); // Make sure the first kDebug is after the slave ctor (which sets a SIGPIPE handler) // This is useful in case kdeinit was autostarted by another app, which then exited and closed fd2 // (e.g. ctest does that, or closing the terminal window would do that) //qDebug() << "Starting" << getpid(); slave.dispatchLoop(); //qDebug() << "Done"; return 0; } static QFile::Permissions modeToQFilePermissions(int mode) { QFile::Permissions perms; if (mode & S_IRUSR) { perms |= QFile::ReadOwner; } if (mode & S_IWUSR) { perms |= QFile::WriteOwner; } if (mode & S_IXUSR) { perms |= QFile::ExeOwner; } if (mode & S_IRGRP) { perms |= QFile::ReadGroup; } if (mode & S_IWGRP) { perms |= QFile::WriteGroup; } if (mode & S_IXGRP) { perms |= QFile::ExeGroup; } if (mode & S_IROTH) { perms |= QFile::ReadOther; } if (mode & S_IWOTH) { perms |= QFile::WriteOther; } if (mode & S_IXOTH) { perms |= QFile::ExeOther; } return perms; } FileProtocol::FileProtocol(const QByteArray &pool, const QByteArray &app) : SlaveBase(QByteArrayLiteral("file"), pool, app), mFile(nullptr) { + testMode = !qEnvironmentVariableIsEmpty("KIOSLAVE_FILE_ENABLE_TESTMODE"); } FileProtocol::~FileProtocol() { } #if HAVE_POSIX_ACL static QString aclToText(acl_t acl) { ssize_t size = 0; char *txt = acl_to_text(acl, &size); const QString ret = QString::fromLatin1(txt, size); acl_free(txt); return ret; } #endif int FileProtocol::setACL(const char *path, mode_t perm, bool directoryDefault) { int ret = 0; #if HAVE_POSIX_ACL const QString ACLString = metaData(QStringLiteral("ACL_STRING")); const QString defaultACLString = metaData(QStringLiteral("DEFAULT_ACL_STRING")); // Empty strings mean leave as is if (!ACLString.isEmpty()) { acl_t acl = nullptr; if (ACLString == QLatin1String("ACL_DELETE")) { // user told us to delete the extended ACL, so let's write only // the minimal (UNIX permission bits) part acl = acl_from_mode(perm); } acl = acl_from_text(ACLString.toLatin1().constData()); if (acl_valid(acl) == 0) { // let's be safe ret = acl_set_file(path, ACL_TYPE_ACCESS, acl); // qDebug() << "Set ACL on:" << path << "to:" << aclToText(acl); } acl_free(acl); if (ret != 0) { return ret; // better stop trying right away } } if (directoryDefault && !defaultACLString.isEmpty()) { if (defaultACLString == QLatin1String("ACL_DELETE")) { // user told us to delete the default ACL, do so ret += acl_delete_def_file(path); } else { acl_t acl = acl_from_text(defaultACLString.toLatin1().constData()); if (acl_valid(acl) == 0) { // let's be safe ret += acl_set_file(path, ACL_TYPE_DEFAULT, acl); // qDebug() << "Set Default ACL on:" << path << "to:" << aclToText(acl); } acl_free(acl); } } #else Q_UNUSED(path); Q_UNUSED(perm); Q_UNUSED(directoryDefault); #endif return ret; } void FileProtocol::chmod(const QUrl &url, int permissions) { const QString path(url.toLocalFile()); const QByteArray _path(QFile::encodeName(path)); /* FIXME: Should be atomic */ #ifdef Q_OS_UNIX // QFile::Permissions does not support special attributes like sticky if (::chmod(_path.constData(), permissions) == -1 || #else if (!QFile::setPermissions(path, modeToQFilePermissions(permissions)) || #endif (setACL(_path.data(), permissions, false) == -1) || /* if not a directory, cannot set default ACLs */ (setACL(_path.data(), permissions, true) == -1 && errno != ENOTDIR)) { if (auto err = execWithElevatedPrivilege(CHMOD, {_path, permissions}, errno)) { if (!err.wasCanceled()) { switch (err) { case EPERM: case EACCES: error(KIO::ERR_ACCESS_DENIED, path); break; #if defined(ENOTSUP) case ENOTSUP: // from setACL since chmod can't return ENOTSUP error(KIO::ERR_UNSUPPORTED_ACTION, i18n("Setting ACL for %1", path)); break; #endif case ENOSPC: error(KIO::ERR_DISK_FULL, path); break; default: error(KIO::ERR_CANNOT_CHMOD, path); } return; } } } finished(); } void FileProtocol::setModificationTime(const QUrl &url, const QDateTime &mtime) { const QString path(url.toLocalFile()); QT_STATBUF statbuf; if (QT_LSTAT(QFile::encodeName(path).constData(), &statbuf) == 0) { struct utimbuf utbuf; utbuf.actime = statbuf.st_atime; // access time, unchanged utbuf.modtime = mtime.toSecsSinceEpoch(); // modification time if (::utime(QFile::encodeName(path).constData(), &utbuf) != 0) { if (auto err = execWithElevatedPrivilege(UTIME, {path, qint64(utbuf.actime), qint64(utbuf.modtime)}, errno)) { if (!err.wasCanceled()) { // TODO: errno could be EACCES, EPERM, EROFS error(KIO::ERR_CANNOT_SETTIME, path); } } } else { finished(); } } else { error(KIO::ERR_DOES_NOT_EXIST, path); } } void FileProtocol::mkdir(const QUrl &url, int permissions) { const QString path(url.toLocalFile()); // qDebug() << path << "permission=" << permissions; // Remove existing file or symlink, if requested (#151851) if (metaData(QStringLiteral("overwrite")) == QLatin1String("true")) { if (!QFile::remove(path)) { execWithElevatedPrivilege(DEL, {path}, errno); } } QT_STATBUF buff; if (QT_LSTAT(QFile::encodeName(path).constData(), &buff) == -1) { bool dirCreated = QDir().mkdir(path); if (!dirCreated) { if (auto err = execWithElevatedPrivilege(MKDIR, {path}, errno)) { if (!err.wasCanceled()) { //TODO: add access denied & disk full (or another reasons) handling (into Qt, possibly) error(KIO::ERR_CANNOT_MKDIR, path); } return; } dirCreated = true; } if (dirCreated) { if (permissions != -1) { chmod(url, permissions); } else { finished(); } return; } } if ((buff.st_mode & QT_STAT_MASK) == QT_STAT_DIR) { // qDebug() << "ERR_DIR_ALREADY_EXIST"; error(KIO::ERR_DIR_ALREADY_EXIST, path); return; } error(KIO::ERR_FILE_ALREADY_EXIST, path); return; } void FileProtocol::redirect(const QUrl &url) { QUrl redir(url); redir.setScheme(configValue(QStringLiteral("DefaultRemoteProtocol"), QStringLiteral("smb"))); // if we would redirect into the Windows world, let's also check for the // DavWWWRoot "token" which in the Windows world tells win explorer to access // a webdav url // https://www.webdavsystem.com/server/access/windows if ((redir.scheme() == QLatin1String("smb")) && redir.path().startsWith(QLatin1String("/DavWWWRoot/"))) { redir.setPath(redir.path().mid(11)); // remove /DavWWWRoot redir.setScheme(QStringLiteral("webdav")); } redirection(redir); finished(); } void FileProtocol::get(const QUrl &url) { if (!url.isLocalFile()) { redirect(url); return; } const QString path(url.toLocalFile()); QT_STATBUF buff; if (QT_STAT(QFile::encodeName(path).constData(), &buff) == -1) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, path); } else { error(KIO::ERR_DOES_NOT_EXIST, path); } return; } if ((buff.st_mode & QT_STAT_MASK) == QT_STAT_DIR) { error(KIO::ERR_IS_DIRECTORY, path); return; } if ((buff.st_mode & QT_STAT_MASK) != QT_STAT_REG) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, path); return; } QFile f(path); if (!f.open(QIODevice::ReadOnly)) { if (auto err = tryOpen(f, QFile::encodeName(path), O_RDONLY, S_IRUSR, errno)) { if (!err.wasCanceled()) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, path); } return; } } #if HAVE_FADVISE //TODO check return code posix_fadvise(f.handle(), 0, 0, POSIX_FADV_SEQUENTIAL); #endif // Determine the mimetype of the file to be retrieved, and emit it. // This is mandatory in all slaves (for KRun/BrowserRun to work) // In real "remote" slaves, this is usually done using mimeTypeForFileNameAndData // after receiving some data. But we don't know how much data the mimemagic rules // need, so for local files, better use mimeTypeForFile. QMimeDatabase db; QMimeType mt = db.mimeTypeForFile(url.toLocalFile()); mimeType(mt.name()); // Emit total size AFTER mimetype totalSize(buff.st_size); KIO::filesize_t processed_size = 0; QString resumeOffset = metaData(QStringLiteral("range-start")); if (resumeOffset.isEmpty()) { resumeOffset = metaData(QStringLiteral("resume")); // old name } if (!resumeOffset.isEmpty()) { bool ok; KIO::fileoffset_t offset = resumeOffset.toLongLong(&ok); if (ok && (offset > 0) && (offset < buff.st_size)) { if (f.seek(offset)) { canResume(); processed_size = offset; // qDebug() << "Resume offset:" << KIO::number(offset); } } } char buffer[ MAX_IPC_SIZE ]; QByteArray array; while (1) { int n = f.read(buffer, MAX_IPC_SIZE); if (n == -1) { if (errno == EINTR) { continue; } error(KIO::ERR_CANNOT_READ, path); f.close(); return; } if (n == 0) { break; // Finished } array = QByteArray::fromRawData(buffer, n); data(array); array.clear(); processed_size += n; processedSize(processed_size); //qDebug() << "Processed: " << KIO::number (processed_size); } data(QByteArray()); f.close(); processedSize(buff.st_size); finished(); } void FileProtocol::open(const QUrl &url, QIODevice::OpenMode mode) { // qDebug() << url; QString openPath = url.toLocalFile(); QT_STATBUF buff; if (QT_STAT(QFile::encodeName(openPath).constData(), &buff) == -1) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, openPath); } else { error(KIO::ERR_DOES_NOT_EXIST, openPath); } return; } if ((buff.st_mode & QT_STAT_MASK) == QT_STAT_DIR) { error(KIO::ERR_IS_DIRECTORY, openPath); return; } if ((buff.st_mode & QT_STAT_MASK) != QT_STAT_REG) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, openPath); return; } mFile = new QFile(openPath); if (!mFile->open(mode)) { if (mode & QIODevice::ReadOnly) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, openPath); } else { error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, openPath); } return; } // Determine the mimetype of the file to be retrieved, and emit it. // This is mandatory in all slaves (for KRun/BrowserRun to work). // If we're not opening the file ReadOnly or ReadWrite, don't attempt to // read the file and send the mimetype. if (mode & QIODevice::ReadOnly) { QMimeDatabase db; QMimeType mt = db.mimeTypeForFile(url.toLocalFile()); mimeType(mt.name()); } totalSize(buff.st_size); position(0); opened(); } void FileProtocol::read(KIO::filesize_t bytes) { // qDebug() << "File::open -- read"; Q_ASSERT(mFile && mFile->isOpen()); QVarLengthArray buffer(bytes); qint64 bytesRead = mFile->read(buffer.data(), bytes); if (bytesRead == -1) { qCWarning(KIO_FILE) << "Couldn't read. Error:" << mFile->errorString(); error(KIO::ERR_CANNOT_READ, mFile->fileName()); closeWithoutFinish(); } else { const QByteArray fileData = QByteArray::fromRawData(buffer.data(), bytesRead); data(fileData); } } void FileProtocol::write(const QByteArray &data) { // qDebug() << "File::open -- write"; Q_ASSERT(mFile && mFile->isWritable()); qint64 bytesWritten = mFile->write(data); if (bytesWritten == -1) { if (mFile->error() == QFileDevice::ResourceError) { // disk full error(KIO::ERR_DISK_FULL, mFile->fileName()); closeWithoutFinish(); } else { qCWarning(KIO_FILE) << "Couldn't write. Error:" << mFile->errorString(); error(KIO::ERR_CANNOT_WRITE, mFile->fileName()); closeWithoutFinish(); } } else { written(bytesWritten); } } void FileProtocol::seek(KIO::filesize_t offset) { // qDebug() << "File::open -- seek"; Q_ASSERT(mFile && mFile->isOpen()); if (mFile->seek(offset)) { position(offset); } else { error(KIO::ERR_CANNOT_SEEK, mFile->fileName()); closeWithoutFinish(); } } void FileProtocol::truncate(KIO::filesize_t length) { Q_ASSERT(mFile && mFile->isOpen()); if (mFile->resize(length)) { truncated(length); } else { error(KIO::ERR_CANNOT_TRUNCATE, mFile->fileName()); closeWithoutFinish(); } } void FileProtocol::closeWithoutFinish() { Q_ASSERT(mFile); delete mFile; mFile = nullptr; } void FileProtocol::close() { // qDebug() << "File::open -- close "; closeWithoutFinish(); finished(); } void FileProtocol::put(const QUrl &url, int _mode, KIO::JobFlags _flags) { if (privilegeOperationUnitTestMode()) { finished(); return; } const QString dest_orig = url.toLocalFile(); // qDebug() << dest_orig << "mode=" << _mode; QString dest_part(dest_orig + QLatin1String(".part")); QT_STATBUF buff_orig; const bool bOrigExists = (QT_LSTAT(QFile::encodeName(dest_orig).constData(), &buff_orig) != -1); bool bPartExists = false; const bool bMarkPartial = configValue(QStringLiteral("MarkPartial"), true); if (bMarkPartial) { QT_STATBUF buff_part; bPartExists = (QT_LSTAT(QFile::encodeName(dest_part).constData(), &buff_part) != -1); if (bPartExists && !(_flags & KIO::Resume) && !(_flags & KIO::Overwrite) && buff_part.st_size > 0 && ((buff_part.st_mode & QT_STAT_MASK) == QT_STAT_REG)) { // qDebug() << "calling canResume with" << KIO::number(buff_part.st_size); // Maybe we can use this partial file for resuming // Tell about the size we have, and the app will tell us // if it's ok to resume or not. _flags |= canResume(buff_part.st_size) ? KIO::Resume : KIO::DefaultFlags; // qDebug() << "got answer" << (_flags & KIO::Resume); } } if (bOrigExists && !(_flags & KIO::Overwrite) && !(_flags & KIO::Resume)) { if ((buff_orig.st_mode & QT_STAT_MASK) == QT_STAT_DIR) { error(KIO::ERR_DIR_ALREADY_EXIST, dest_orig); } else { error(KIO::ERR_FILE_ALREADY_EXIST, dest_orig); } return; } int result; QString dest; QFile f; // Loop until we got 0 (end of data) do { QByteArray buffer; dataReq(); // Request for data result = readData(buffer); if (result >= 0) { if (dest.isEmpty()) { if (bMarkPartial) { // qDebug() << "Appending .part extension to" << dest_orig; dest = dest_part; if (bPartExists && !(_flags & KIO::Resume)) { // qDebug() << "Deleting partial file" << dest_part; QFile::remove(dest_part); // Catch errors when we try to open the file. } } else { dest = dest_orig; if (bOrigExists && !(_flags & KIO::Resume)) { // qDebug() << "Deleting destination file" << dest_orig; QFile::remove(dest_orig); // Catch errors when we try to open the file. } } f.setFileName(dest); if ((_flags & KIO::Resume)) { f.open(QIODevice::ReadWrite | QIODevice::Append); } else { f.open(QIODevice::Truncate | QIODevice::WriteOnly); if (_mode != -1) { // WABA: Make sure that we keep writing permissions ourselves, // otherwise we can be in for a surprise on NFS. mode_t initialMode = _mode | S_IWUSR | S_IRUSR; f.setPermissions(modeToQFilePermissions(initialMode)); } } if (!f.isOpen()) { int oflags = 0; int filemode = _mode; if ((_flags & KIO::Resume)) { oflags = O_RDWR | O_APPEND; } else { oflags = O_WRONLY | O_TRUNC | O_CREAT; if (_mode != -1) { filemode = _mode | S_IWUSR | S_IRUSR; } } if (auto err = tryOpen(f, QFile::encodeName(dest), oflags, filemode, errno)) { if (!err.wasCanceled()) { // qDebug() << "####################### COULD NOT WRITE" << dest << "_mode=" << _mode; // qDebug() << "QFile error==" << f.error() << "(" << f.errorString() << ")"; if (f.error() == QFileDevice::PermissionsError) { error(KIO::ERR_WRITE_ACCESS_DENIED, dest); } else { error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest); } } return; } else { #ifndef Q_OS_WIN if ((_flags & KIO::Resume)) { execWithElevatedPrivilege(CHOWN, {dest, getuid(), getgid()}, errno); QFile::setPermissions(dest, modeToQFilePermissions(filemode)); } #endif } } } if (f.write(buffer) == -1) { if (f.error() == QFile::ResourceError) { // disk full error(KIO::ERR_DISK_FULL, dest_orig); result = -2; // means: remove dest file } else { qCWarning(KIO_FILE) << "Couldn't write. Error:" << f.errorString(); error(KIO::ERR_CANNOT_WRITE, dest_orig); result = -1; } } } else { qCWarning(KIO_FILE) << "readData() returned" << result; error(KIO::ERR_CANNOT_WRITE, dest_orig); } } while (result > 0); // An error occurred deal with it. if (result < 0) { // qDebug() << "Error during 'put'. Aborting."; if (f.isOpen()) { f.close(); QT_STATBUF buff; if (QT_STAT(QFile::encodeName(dest).constData(), &buff) == 0) { int size = configValue(QStringLiteral("MinimumKeepSize"), DEFAULT_MINIMUM_KEEP_SIZE); if (buff.st_size < size) { QFile::remove(dest); } } } return; } if (!f.isOpen()) { // we got nothing to write out, so we never opened the file finished(); return; } f.close(); if (f.error() != QFile::NoError) { qCWarning(KIO_FILE) << "Error when closing file descriptor:" << f.errorString(); error(KIO::ERR_CANNOT_WRITE, dest_orig); return; } // after full download rename the file back to original name if (bMarkPartial) { //QFile::rename() never overwrites the destination file unlike ::remove, //so we must remove it manually first if (_flags & KIO::Overwrite) { if (!QFile::remove(dest_orig)) { execWithElevatedPrivilege(DEL, {dest_orig}, errno); } } if (!QFile::rename(dest, dest_orig)) { if (auto err = execWithElevatedPrivilege(RENAME, {dest, dest_orig}, errno)) { if (!err.wasCanceled()) { qCWarning(KIO_FILE) << " Couldn't rename " << dest << " to " << dest_orig; error(KIO::ERR_CANNOT_RENAME_PARTIAL, dest_orig); } return; } } org::kde::KDirNotify::emitFileRenamed(QUrl::fromLocalFile(dest), QUrl::fromLocalFile(dest_orig)); } // set final permissions if (_mode != -1 && !(_flags & KIO::Resume)) { if (!QFile::setPermissions(dest_orig, modeToQFilePermissions(_mode))) { // couldn't chmod. Eat the error if the filesystem apparently doesn't support it. KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(dest_orig); if (mp && mp->testFileSystemFlag(KMountPoint::SupportsChmod)) { if (tryChangeFileAttr(CHMOD, {dest_orig, _mode}, errno)) { warning(i18n("Could not change permissions for\n%1", dest_orig)); } } } } // set modification time const QString mtimeStr = metaData(QStringLiteral("modified")); if (!mtimeStr.isEmpty()) { QDateTime dt = QDateTime::fromString(mtimeStr, Qt::ISODate); if (dt.isValid()) { QT_STATBUF dest_statbuf; if (QT_STAT(QFile::encodeName(dest_orig).constData(), &dest_statbuf) == 0) { #ifndef Q_OS_WIN struct timeval utbuf[2]; // access time utbuf[0].tv_sec = dest_statbuf.st_atime; // access time, unchanged ## TODO preserve msec utbuf[0].tv_usec = 0; // modification time utbuf[1].tv_sec = dt.toSecsSinceEpoch(); utbuf[1].tv_usec = dt.time().msec() * 1000; utimes(QFile::encodeName(dest_orig).constData(), utbuf); #else struct utimbuf utbuf; utbuf.actime = dest_statbuf.st_atime; utbuf.modtime = dt.toSecsSinceEpoch(); if (utime(QFile::encodeName(dest_orig).constData(), &utbuf) != 0) { tryChangeFileAttr(UTIME, {dest_orig, qint64(utbuf.actime), qint64(utbuf.modtime)}, errno); } #endif } } } // We have done our job => finish finished(); } QString FileProtocol::getUserName(KUserId uid) const { if (Q_UNLIKELY(!uid.isValid())) { return QString(); } auto it = mUsercache.find(uid); if (it == mUsercache.end()) { KUser user(uid); QString name = user.loginName(); if (name.isEmpty()) { name = uid.toString(); } it = mUsercache.insert(uid, name); } return *it; } QString FileProtocol::getGroupName(KGroupId gid) const { if (Q_UNLIKELY(!gid.isValid())) { return QString(); } auto it = mGroupcache.find(gid); if (it == mGroupcache.end()) { KUserGroup group(gid); QString name = group.name(); if (name.isEmpty()) { name = gid.toString(); } it = mGroupcache.insert(gid, name); } return *it; } #if HAVE_STATX // statx syscall is available inline int LSTAT(const char* path, struct statx * buff) { return statx(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW, STATX_BASIC_STATS | STATX_BTIME, buff); } inline int STAT(const char* path, struct statx * buff) { return statx(AT_FDCWD, path, AT_STATX_SYNC_AS_STAT, STATX_BASIC_STATS | STATX_BTIME, buff); } inline static uint16_t stat_mode(struct statx &buf) { return buf.stx_mode; } inline static uint32_t stat_dev(struct statx &buf) { return buf.stx_dev_major; } inline static uint64_t stat_ino(struct statx &buf) { return buf.stx_ino; } inline static uint64_t stat_size(struct statx &buf) { return buf.stx_size; } inline static uint32_t stat_uid(struct statx &buf) { return buf.stx_uid; } inline static uint32_t stat_gid(struct statx &buf) { return buf.stx_gid; } inline static int64_t stat_atime(struct statx &buf) { return buf.stx_atime.tv_sec; } inline static int64_t stat_mtime(struct statx &buf) { return buf.stx_mtime.tv_sec; } #else // regular stat struct inline int LSTAT(const char* path, QT_STATBUF * buff) { return QT_LSTAT(path, buff); } inline int STAT(const char* path, QT_STATBUF * buff) { return QT_STAT(path, buff); } inline static mode_t stat_mode(QT_STATBUF &buf) { return buf.st_mode; } inline static dev_t stat_dev(QT_STATBUF &buf) { return buf.st_dev; } inline static ino_t stat_ino(QT_STATBUF &buf) { return buf.st_ino; } inline static off_t stat_size(QT_STATBUF &buf) { return buf.st_size; } #ifndef Q_OS_WIN inline static uid_t stat_uid(QT_STATBUF &buf) { return buf.st_uid; } inline static gid_t stat_gid(QT_STATBUF &buf) { return buf.st_gid; } #endif inline static time_t stat_atime(QT_STATBUF &buf) { return buf.st_atime; } inline static time_t stat_mtime(QT_STATBUF &buf) { return buf.st_mtime; } #endif bool FileProtocol::createUDSEntry(const QString &filename, const QByteArray &path, UDSEntry &entry, short int details) { assert(entry.count() == 0); // by contract :-) switch (details) { case 0: // filename, access, type, size, linkdest entry.reserve(5); break; case 1: // uid, gid, atime, mtime, btime entry.reserve(10); break; case 2: // acl data entry.reserve(13); break; default: // case details > 2 // dev, inode entry.reserve(15); break; } entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename); mode_t type; mode_t access; bool isBrokenSymLink = false; signed long long size = 0LL; #if HAVE_POSIX_ACL QByteArray targetPath = path; #endif #if HAVE_STATX // statx syscall is available struct statx buff; #else QT_STATBUF buff; #endif if (LSTAT(path.data(), &buff) == 0) { if (details > 2) { entry.fastInsert(KIO::UDSEntry::UDS_DEVICE_ID, stat_dev(buff)); entry.fastInsert(KIO::UDSEntry::UDS_INODE, stat_ino(buff)); } if ((stat_mode(buff) & QT_STAT_MASK) == QT_STAT_LNK) { #ifdef Q_OS_WIN const QString linkTarget = QFile::symLinkTarget(QFile::decodeName(path)); #else // Use readlink on Unix because symLinkTarget turns relative targets into absolute (#352927) #if HAVE_STATX size_t lowerBound = 256; size_t higherBound = 1024; uint64_t s = stat_size(buff); if (s > SIZE_MAX) { qCWarning(KIO_FILE) << "file size bigger than SIZE_MAX, too big for readlink use!" << path; return false; } size_t size = static_cast(s); using SizeType = size_t; #else off_t lowerBound = 256; off_t higherBound = 1024; off_t size = stat_size(buff); using SizeType = off_t; #endif SizeType bufferSize = qBound(lowerBound, size +1, higherBound); QByteArray linkTargetBuffer; linkTargetBuffer.resize(bufferSize); while (true) { ssize_t n = readlink(path.constData(), linkTargetBuffer.data(), bufferSize); if (n < 0 && errno != ERANGE) { qCWarning(KIO_FILE) << "readlink failed!" << path; return false; } else if (n > 0 && static_cast(n) != bufferSize) { // the buffer was not filled in the last iteration // we are finished reading, break the loop linkTargetBuffer.truncate(n); break; } bufferSize *= 2; linkTargetBuffer.resize(bufferSize); } const QString linkTarget = QFile::decodeName(linkTargetBuffer); #endif entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, linkTarget); // A symlink -> follow it only if details>1 if (details > 1) { if (STAT(path.constData(), &buff) == -1) { isBrokenSymLink = true; } else { #if HAVE_POSIX_ACL // valid symlink, will get the ACLs of the destination targetPath = linkTargetBuffer; #endif } } } } else { // qCWarning(KIO_FILE) << "lstat didn't work on " << path.data(); return false; } if (isBrokenSymLink) { // It is a link pointing to nowhere type = S_IFMT - 1; access = S_IRWXU | S_IRWXG | S_IRWXO; size = 0LL; } else { type = stat_mode(buff) & S_IFMT; // extract file type access = stat_mode(buff) & 07777; // extract permissions size = stat_size(buff); } entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, type); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, access); entry.fastInsert(KIO::UDSEntry::UDS_SIZE, size); #if HAVE_POSIX_ACL if (details > 1) { /* Append an atom indicating whether the file has extended acl information * and if withACL is specified also one with the acl itself. If it's a directory * and it has a default ACL, also append that. */ appendACLAtoms(targetPath, entry, type); } #endif if (details > 0) { entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, stat_mtime(buff)); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, stat_atime(buff)); #ifndef Q_OS_WIN entry.fastInsert(KIO::UDSEntry::UDS_USER, getUserName(KUserId(stat_uid(buff)))); entry.fastInsert(KIO::UDSEntry::UDS_GROUP, getGroupName(KGroupId(stat_gid(buff)))); #else #pragma message("TODO: st_uid and st_gid are always zero, use GetSecurityInfo to find the owner") #endif #ifdef st_birthtime /* For example FreeBSD's and NetBSD's stat contains a field for * the inode birth time: st_birthtime * This however only works on UFS and ZFS, and not, on say, NFS. * Instead of setting a bogus fallback like st_mtime, only use * it if it is greater than 0. */ if (buff.st_birthtime > 0) { entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, buff.st_birthtime); } #elif defined __st_birthtime /* As above, but OpenBSD calls it slightly differently. */ if (buff.__st_birthtime > 0) { entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, buff.__st_birthtime); } #elif HAVE_STATX /* And linux version using statx syscall */ if (buff.stx_mask & STATX_BTIME) { entry.fastInsert(KIO::UDSEntry::UDS_CREATION_TIME, buff.stx_btime.tv_sec); } #endif } return true; } void FileProtocol::special(const QByteArray &data) { int tmp; QDataStream stream(data); stream >> tmp; switch (tmp) { case 1: { QString fstype, dev, point; qint8 iRo; stream >> iRo >> fstype >> dev >> point; bool ro = (iRo != 0); // qDebug() << "MOUNTING fstype=" << fstype << " dev=" << dev << " point=" << point << " ro=" << ro; bool ok = pmount(dev); if (ok) { finished(); } else { mount(ro, fstype.toLatin1().constData(), dev, point); } } break; case 2: { QString point; stream >> point; bool ok = pumount(point); if (ok) { finished(); } else { unmount(point); } } break; default: break; } } static QStringList fallbackSystemPath() { return QStringList{ QStringLiteral("/sbin"), QStringLiteral("/bin"), }; } void FileProtocol::mount(bool _ro, const char *_fstype, const QString &_dev, const QString &_point) { // qDebug() << "fstype=" << _fstype; #ifndef _WIN32_WCE #if HAVE_VOLMGT /* * support for Solaris volume management */ QString err; QByteArray devname = QFile::encodeName(_dev); if (volmgt_running()) { // qDebug() << "VOLMGT: vold ok."; if (volmgt_check(devname.data()) == 0) { // qDebug() << "VOLMGT: no media in " << devname.data(); err = i18n("No Media inserted or Media not recognized."); error(KIO::ERR_CANNOT_MOUNT, err); return; } else { // qDebug() << "VOLMGT: " << devname.data() << ": media ok"; finished(); return; } } else { err = i18n("\"vold\" is not running."); // qDebug() << "VOLMGT: " << err; error(KIO::ERR_CANNOT_MOUNT, err); return; } #else QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.open(); QByteArray tmpFileName = QFile::encodeName(tmpFile.fileName()); QByteArray dev; if (_dev.startsWith(QLatin1String("LABEL="))) { // turn LABEL=foo into -L foo (#71430) QString labelName = _dev.mid(6); dev = "-L " + QFile::encodeName(KShell::quoteArg(labelName)); // is it correct to assume same encoding as filesystem? } else if (_dev.startsWith(QLatin1String("UUID="))) { // and UUID=bar into -U bar QString uuidName = _dev.mid(5); dev = "-U " + QFile::encodeName(KShell::quoteArg(uuidName)); } else { dev = QFile::encodeName(KShell::quoteArg(_dev)); // get those ready to be given to a shell } QByteArray point = QFile::encodeName(KShell::quoteArg(_point)); bool fstype_empty = !_fstype || !*_fstype; QByteArray fstype = KShell::quoteArg(QString::fromLatin1(_fstype)).toLatin1(); // good guess QByteArray readonly = _ro ? "-r" : ""; QByteArray mountProg = QStandardPaths::findExecutable(QStringLiteral("mount")).toLocal8Bit(); if (mountProg.isEmpty()) { mountProg = QStandardPaths::findExecutable(QStringLiteral("mount"), fallbackSystemPath()).toLocal8Bit(); } if (mountProg.isEmpty()) { error(KIO::ERR_CANNOT_MOUNT, i18n("Could not find program \"mount\"")); return; } // Two steps, in case mount doesn't like it when we pass all options for (int step = 0; step <= 1; step++) { QByteArray buffer = mountProg + ' '; // Mount using device only if no fstype nor mountpoint (KDE-1.x like) if (!dev.isEmpty() && _point.isEmpty() && fstype_empty) { buffer += dev; } else // Mount using the mountpoint, if no fstype nor device (impossible in first step) if (!_point.isEmpty() && dev.isEmpty() && fstype_empty) { buffer += point; } else // mount giving device + mountpoint but no fstype if (!_point.isEmpty() && !dev.isEmpty() && fstype_empty) { buffer += readonly + ' ' + dev + ' ' + point; } else // mount giving device + mountpoint + fstype #if defined(__svr4__) && defined(Q_OS_SOLARIS) // MARCO for Solaris 8 and I // believe this is true for SVR4 in general buffer += "-F " + fstype + ' ' + (_ro ? "-oro" : "") + ' ' + dev + ' ' + point; #else buffer += readonly + " -t " + fstype + ' ' + dev + ' ' + point; #endif buffer += " 2>" + tmpFileName; // qDebug() << buffer; int mount_ret = system(buffer.constData()); QString err = readLogFile(tmpFileName); if (err.isEmpty() && mount_ret == 0) { finished(); return; } else { // Didn't work - or maybe we just got a warning KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByDevice(_dev); // Is the device mounted ? if (mp && mount_ret == 0) { // qDebug() << "mount got a warning:" << err; warning(err); finished(); return; } else { if ((step == 0) && !_point.isEmpty()) { // qDebug() << err; // qDebug() << "Mounting with those options didn't work, trying with only mountpoint"; fstype = ""; fstype_empty = true; dev = ""; // The reason for trying with only mountpoint (instead of // only device) is that some people (hi Malte!) have the // same device associated with two mountpoints // for different fstypes, like /dev/fd0 /mnt/e2floppy and // /dev/fd0 /mnt/dosfloppy. // If the user has the same mountpoint associated with two // different devices, well they shouldn't specify the // mountpoint but just the device. } else { error(KIO::ERR_CANNOT_MOUNT, err); return; } } } } #endif /* ! HAVE_VOLMGT */ #else QString err; err = i18n("mounting is not supported by wince."); error(KIO::ERR_CANNOT_MOUNT, err); #endif } void FileProtocol::unmount(const QString &_point) { #ifndef _WIN32_WCE QByteArray buffer; QTemporaryFile tmpFile; tmpFile.setAutoRemove(false); tmpFile.open(); QByteArray tmpFileName = QFile::encodeName(tmpFile.fileName()); QString err; #if HAVE_VOLMGT /* * support for Solaris volume management */ char *devname; char *ptr; FILE *mnttab; struct mnttab mnt; if (volmgt_running()) { // qDebug() << "VOLMGT: looking for " << _point.toLocal8Bit(); if ((mnttab = QT_FOPEN(MNTTAB, "r")) == nullptr) { err = QLatin1String("could not open mnttab"); // qDebug() << "VOLMGT: " << err; error(KIO::ERR_CANNOT_UNMOUNT, err); return; } /* * since there's no way to derive the device name from * the mount point through the volmgt library (and * media_findname() won't work in this case), we have to * look ourselves... */ devname = nullptr; rewind(mnttab); while (getmntent(mnttab, &mnt) == nullptr) { if (strcmp(_point.toLocal8Bit(), mnt.mnt_mountp) == 0) { devname = mnt.mnt_special; break; } } fclose(mnttab); if (devname == nullptr) { err = QLatin1String("not in mnttab"); // qDebug() << "VOLMGT: " << QFile::encodeName(_point).data() << ": " << err; error(KIO::ERR_CANNOT_UNMOUNT, err); return; } /* * strip off the directory name (volume name) * the eject(1) command will handle unmounting and * physically eject the media (if possible) */ ptr = strrchr(devname, '/'); *ptr = '\0'; QByteArray qdevname(QFile::encodeName(KShell::quoteArg(QFile::decodeName(QByteArray(devname)))).data()); buffer = "/usr/bin/eject " + qdevname + " 2>" + tmpFileName; // qDebug() << "VOLMGT: eject " << qdevname; /* * from eject(1): exit status == 0 => need to manually eject * exit status == 4 => media was ejected */ if (WEXITSTATUS(system(buffer.constData())) == 4) { /* * this is not an error, so skip "readLogFile()" * to avoid wrong/confusing error popup. The * temporary file is removed by QTemporaryFile's * destructor, so don't do that manually. */ finished(); return; } } else { /* * eject(1) should do its job without vold(1M) running, * so we probably could call eject anyway, but since the * media is mounted now, vold must've died for some reason * during the user's session, so it should be restarted... */ err = i18n("\"vold\" is not running."); // qDebug() << "VOLMGT: " << err; error(KIO::ERR_CANNOT_UNMOUNT, err); return; } #else QByteArray umountProg = QStandardPaths::findExecutable(QStringLiteral("umount")).toLocal8Bit(); if (umountProg.isEmpty()) { umountProg = QStandardPaths::findExecutable(QStringLiteral("umount"), fallbackSystemPath()).toLocal8Bit(); } if (umountProg.isEmpty()) { error(KIO::ERR_CANNOT_UNMOUNT, i18n("Could not find program \"umount\"")); return; } buffer = umountProg + ' ' + QFile::encodeName(KShell::quoteArg(_point)) + " 2>" + tmpFileName; system(buffer.constData()); #endif /* HAVE_VOLMGT */ err = readLogFile(tmpFileName); if (err.isEmpty()) { finished(); } else { error(KIO::ERR_CANNOT_UNMOUNT, err); } #else QString err; err = i18n("unmounting is not supported by wince."); error(KIO::ERR_CANNOT_MOUNT, err); #endif } /************************************* * * pmount handling * *************************************/ bool FileProtocol::pmount(const QString &dev) { #ifndef _WIN32_WCE QString pmountProg = QStandardPaths::findExecutable(QStringLiteral("pmount")); if (pmountProg.isEmpty()) { pmountProg = QStandardPaths::findExecutable(QStringLiteral("pmount"), fallbackSystemPath()); } if (pmountProg.isEmpty()) { return false; } QByteArray buffer = QFile::encodeName(pmountProg) + ' ' + QFile::encodeName(KShell::quoteArg(dev)); int res = system(buffer.constData()); return res == 0; #else return false; #endif } bool FileProtocol::pumount(const QString &point) { #ifndef _WIN32_WCE KMountPoint::Ptr mp = KMountPoint::currentMountPoints(KMountPoint::NeedRealDeviceName).findByPath(point); if (!mp) { return false; } QString dev = mp->realDeviceName(); if (dev.isEmpty()) { return false; } QString pumountProg = QStandardPaths::findExecutable(QStringLiteral("pumount")); if (pumountProg.isEmpty()) { pumountProg = QStandardPaths::findExecutable(QStringLiteral("pumount"), fallbackSystemPath()); } if (pumountProg.isEmpty()) { return false; } const QByteArray buffer = QFile::encodeName(pumountProg) + ' ' + QFile::encodeName(KShell::quoteArg(dev)); int res = system(buffer.data()); return res == 0; #else return false; #endif } /************************************* * * Utilities * *************************************/ static QString readLogFile(const QByteArray &_filename) { QString result; QFile file(QFile::decodeName(_filename)); if (file.open(QIODevice::ReadOnly)) { result = QString::fromLocal8Bit(file.readAll()); } (void)file.remove(); return result; } /************************************* * * ACL handling helpers * *************************************/ #if HAVE_POSIX_ACL bool FileProtocol::isExtendedACL(acl_t acl) { return (acl_equiv_mode(acl, nullptr) != 0); } static void appendACLAtoms(const QByteArray &path, UDSEntry &entry, mode_t type) { // first check for a noop if (acl_extended_file(path.data()) == 0) { return; } acl_t acl = nullptr; acl_t defaultAcl = nullptr; bool isDir = (type & QT_STAT_MASK) == QT_STAT_DIR; // do we have an acl for the file, and/or a default acl for the dir, if it is one? acl = acl_get_file(path.data(), ACL_TYPE_ACCESS); /* Sadly libacl does not provided a means of checking for extended ACL and default * ACL separately. Since a directory can have both, we need to check again. */ if (isDir) { if (acl) { if (!FileProtocol::isExtendedACL(acl)) { acl_free(acl); acl = nullptr; } } defaultAcl = acl_get_file(path.data(), ACL_TYPE_DEFAULT); } if (acl || defaultAcl) { // qDebug() << path.constData() << "has extended ACL entries"; entry.fastInsert(KIO::UDSEntry::UDS_EXTENDED_ACL, 1); if (acl) { const QString str = aclToText(acl); entry.fastInsert(KIO::UDSEntry::UDS_ACL_STRING, str); // qDebug() << path.constData() << "ACL:" << str; acl_free(acl); } if (defaultAcl) { const QString str = aclToText(defaultAcl); entry.fastInsert(KIO::UDSEntry::UDS_DEFAULT_ACL_STRING, str); // qDebug() << path.constData() << "DEFAULT ACL:" << str; acl_free(defaultAcl); } } } #endif // We could port this to KTempDir::removeDir but then we wouldn't be able to tell the user // where exactly the deletion failed, in case of errors. bool FileProtocol::deleteRecursive(const QString &path) { //qDebug() << path; QDirIterator it(path, QDir::AllEntries | QDir::NoDotAndDotDot | QDir::System | QDir::Hidden, QDirIterator::Subdirectories); QStringList dirsToDelete; while (it.hasNext()) { const QString itemPath = it.next(); //qDebug() << "itemPath=" << itemPath; const QFileInfo info = it.fileInfo(); if (info.isDir() && !info.isSymLink()) { dirsToDelete.prepend(itemPath); } else { //qDebug() << "QFile::remove" << itemPath; if (!QFile::remove(itemPath)) { if (auto err = execWithElevatedPrivilege(DEL, {itemPath}, errno)) { if (!err.wasCanceled()) { error(KIO::ERR_CANNOT_DELETE, itemPath); } return false; } } } } QDir dir; for (const QString &itemPath : qAsConst(dirsToDelete)) { //qDebug() << "QDir::rmdir" << itemPath; if (!dir.rmdir(itemPath)) { if (auto err = execWithElevatedPrivilege(RMDIR, {itemPath}, errno)) { if (!err.wasCanceled()) { error(KIO::ERR_CANNOT_DELETE, itemPath); } return false; } } } return true; } void FileProtocol::fileSystemFreeSpace(const QUrl &url) { if (url.isLocalFile()) { const KDiskFreeSpaceInfo spaceInfo = KDiskFreeSpaceInfo::freeSpaceInfo(url.toLocalFile()); if (spaceInfo.isValid()) { setMetaData(QStringLiteral("total"), QString::number(spaceInfo.size())); setMetaData(QStringLiteral("available"), QString::number(spaceInfo.available())); finished(); } else { error(KIO::ERR_CANNOT_STAT, url.url()); } } else { error(KIO::ERR_UNSUPPORTED_PROTOCOL, url.url()); } } void FileProtocol::virtual_hook(int id, void *data) { switch(id) { case SlaveBase::GetFileSystemFreeSpace: { QUrl *url = static_cast(data); fileSystemFreeSpace(*url); } break; case SlaveBase::Truncate: { auto length = static_cast(data); truncate(*length); } break; default: { SlaveBase::virtual_hook(id, data); } break; } } // needed for JSON file embedding #include "file.moc" diff --git a/src/ioslaves/file/file.h b/src/ioslaves/file/file.h index 1f819632..f7958d2a 100644 --- a/src/ioslaves/file/file.h +++ b/src/ioslaves/file/file.h @@ -1,126 +1,128 @@ /* Copyright (C) 2000-2002 Stephan Kulow Copyright (C) 2000-2002 David Faure Copyright (C) 2000-2002 Waldo Bastian This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License (LGPL) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __file_h__ #define __file_h__ #include #include #include #include #include #include #include // mode_t #include #if HAVE_POSIX_ACL #include #include #endif #include "file_p.h" #include Q_DECLARE_LOGGING_CATEGORY(KIO_FILE) class FileProtocol : public QObject, public KIO::SlaveBase { Q_OBJECT public: FileProtocol(const QByteArray &pool, const QByteArray &app); virtual ~FileProtocol(); void get(const QUrl &url) override; virtual void put(const QUrl &url, int _mode, KIO::JobFlags _flags) override; virtual void copy(const QUrl &src, const QUrl &dest, int mode, KIO::JobFlags flags) override; virtual void rename(const QUrl &src, const QUrl &dest, KIO::JobFlags flags) override; virtual void symlink(const QString &target, const QUrl &dest, KIO::JobFlags flags) override; void stat(const QUrl &url) override; void listDir(const QUrl &url) override; void mkdir(const QUrl &url, int permissions) override; void chmod(const QUrl &url, int permissions) override; void chown(const QUrl &url, const QString &owner, const QString &group) override; void setModificationTime(const QUrl &url, const QDateTime &mtime) override; void del(const QUrl &url, bool isfile) override; void open(const QUrl &url, QIODevice::OpenMode mode) override; void read(KIO::filesize_t size) override; void write(const QByteArray &data) override; void seek(KIO::filesize_t offset) override; void truncate(KIO::filesize_t length); void close() override; /** * Special commands supported by this slave: * 1 - mount * 2 - unmount */ void special(const QByteArray &data) override; void unmount(const QString &point); void mount(bool _ro, const char *_fstype, const QString &dev, const QString &point); bool pumount(const QString &point); bool pmount(const QString &dev); #if HAVE_POSIX_ACL static bool isExtendedACL(acl_t acl); #endif protected: void virtual_hook(int id, void *data) override; private: bool createUDSEntry(const QString &filename, const QByteArray &path, KIO::UDSEntry &entry, short int details); int setACL(const char *path, mode_t perm, bool _directoryDefault); QString getUserName(KUserId uid) const; QString getGroupName(KGroupId gid) const; bool deleteRecursive(const QString &path); void fileSystemFreeSpace(const QUrl &url); // KF6 TODO: Turn into virtual method in SlaveBase bool privilegeOperationUnitTestMode(); PrivilegeOperationReturnValue execWithElevatedPrivilege(ActionType action, const QVariantList &args, int errcode); PrivilegeOperationReturnValue tryOpen(QFile &f, const QByteArray &path, int flags, int mode, int errcode); // We want to execute chmod/chown/utime with elevated privileges (in copy & put) // only during the brief period privileges are elevated. If it's not the case show // a warning and continue. PrivilegeOperationReturnValue tryChangeFileAttr(ActionType action, const QVariantList &args, int errcode); void redirect(const QUrl &url); // Close without calling finish(). Use this to close after error. void closeWithoutFinish(); private: mutable QHash mUsercache; mutable QHash mGroupcache; QFile *mFile; + + bool testMode = false; }; #endif diff --git a/src/ioslaves/file/file_unix.cpp b/src/ioslaves/file/file_unix.cpp index 4d544290..338351cf 100644 --- a/src/ioslaves/file/file_unix.cpp +++ b/src/ioslaves/file/file_unix.cpp @@ -1,977 +1,992 @@ /* Copyright (C) 2000-2002 Stephan Kulow Copyright (C) 2000-2002 David Faure Copyright (C) 2000-2002 Waldo Bastian Copyright (C) 2006 Allan Sandfeld Jensen Copyright (C) 2007 Thiago Macieira Copyright (C) 2007 Christian Ehrlicher This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License (LGPL) as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "file.h" #include #include #include #include #include +#include #include #include #include #include #include #if HAVE_SYS_XATTR_H #include #endif #include #include #include #include "fdreceiver.h" //sendfile has different semantics in different platforms #if HAVE_SENDFILE && defined Q_OS_LINUX #define USE_SENDFILE 1 #endif #ifdef USE_SENDFILE #include #endif using namespace KIO; #define MAX_IPC_SIZE (1024*32) static bool same_inode(const QT_STATBUF &src, const QT_STATBUF &dest) { if (src.st_ino == dest.st_ino && src.st_dev == dest.st_dev) { return true; } return false; } static const QString socketPath() { const QString runtimeDir = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation); return QStringLiteral("%1/filehelper%2%3").arg(runtimeDir, KRandom::randomString(6)).arg(getpid()); } static QString actionDetails(ActionType actionType, const QVariantList &args) { QString action, detail; switch (actionType) { case CHMOD: action = i18n("Change File Permissions"); detail = i18n("New Permissions: %1", args[1].toInt()); break; case CHOWN: action = i18n("Change File Owner"); detail = i18n("New Owner: UID=%1, GID=%2", args[1].toInt(), args[2].toInt()); break; case DEL: action = i18n("Remove File"); break; case RMDIR: action = i18n("Remove Directory"); break; case MKDIR: action = i18n("Create Directory"); detail = i18n("Directory Permissions: %1", args[1].toInt()); break; case OPEN: action = i18n("Open File"); break; case OPENDIR: action = i18n("Open Directory"); break; case RENAME: action = i18n("Rename"); detail = i18n("New Filename: %1", args[1].toString()); break; case SYMLINK: action = i18n("Create Symlink"); detail = i18n("Target: %1", args[1].toString()); break; case UTIME: action = i18n("Change Timestamp"); break; default: action = i18n("Unknown Action"); break; } const QString metadata = i18n("Action: %1\n" "Source: %2\n" "%3", action, args[0].toString(), detail); return metadata; } bool FileProtocol::privilegeOperationUnitTestMode() { return (metaData(QStringLiteral("UnitTesting")) == QLatin1String("true")) && (requestPrivilegeOperation(QStringLiteral("Test Call")) == KIO::OperationAllowed); } PrivilegeOperationReturnValue FileProtocol::tryOpen(QFile &f, const QByteArray &path, int flags, int mode, int errcode) { const QString sockPath = socketPath(); FdReceiver fdRecv(QFile::encodeName(sockPath).toStdString()); if (!fdRecv.isListening()) { return PrivilegeOperationReturnValue::failure(errcode); } QIODevice::OpenMode openMode; if (flags & O_RDONLY) { openMode |= QIODevice::ReadOnly; } if (flags & O_WRONLY || flags & O_CREAT) { openMode |= QIODevice::WriteOnly; } if (flags & O_RDWR) { openMode |= QIODevice::ReadWrite; } if (flags & O_TRUNC) { openMode |= QIODevice::Truncate; } if (flags & O_APPEND) { openMode |= QIODevice::Append; } if (auto err = execWithElevatedPrivilege(OPEN, {path, flags, mode, sockPath}, errcode)) { return err; } else { int fd = fdRecv.fileDescriptor(); if (fd < 3 || !f.open(fd, openMode, QFileDevice::AutoCloseHandle)) { return PrivilegeOperationReturnValue::failure(errcode); } } return PrivilegeOperationReturnValue::success(); } PrivilegeOperationReturnValue FileProtocol::tryChangeFileAttr(ActionType action, const QVariantList &args, int errcode) { KAuth::Action execAction(QStringLiteral("org.kde.kio.file.exec")); execAction.setHelperId(QStringLiteral("org.kde.kio.file")); if (execAction.status() == KAuth::Action::AuthorizedStatus) { return execWithElevatedPrivilege(action, args, errcode); } return PrivilegeOperationReturnValue::failure(errcode); } void FileProtocol::copy(const QUrl &srcUrl, const QUrl &destUrl, int _mode, JobFlags _flags) { if (privilegeOperationUnitTestMode()) { finished(); return; } // qDebug() << "copy(): " << srcUrl << " -> " << destUrl << ", mode=" << _mode; const QString src = srcUrl.toLocalFile(); QString dest = destUrl.toLocalFile(); QByteArray _src(QFile::encodeName(src)); QByteArray _dest(QFile::encodeName(dest)); QByteArray _dest_backup; QT_STATBUF buff_src; #if HAVE_POSIX_ACL acl_t acl; #endif if (QT_STAT(_src.data(), &buff_src) == -1) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, src); } else { error(KIO::ERR_DOES_NOT_EXIST, src); } return; } if ((buff_src.st_mode & QT_STAT_MASK) == QT_STAT_DIR) { error(KIO::ERR_IS_DIRECTORY, src); return; } if (S_ISFIFO(buff_src.st_mode) || S_ISSOCK(buff_src.st_mode)) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, src); return; } QT_STATBUF buff_dest; bool dest_exists = (QT_LSTAT(_dest.data(), &buff_dest) != -1); if (dest_exists) { if (same_inode(buff_dest, buff_src)) { error(KIO::ERR_IDENTICAL_FILES, dest); return; } if ((buff_dest.st_mode & QT_STAT_MASK) == QT_STAT_DIR) { error(KIO::ERR_DIR_ALREADY_EXIST, dest); return; } if (_flags & KIO::Overwrite) { // If the destination is a symlink and overwrite is TRUE, // remove the symlink first to prevent the scenario where // the symlink actually points to current source! if ((buff_dest.st_mode & QT_STAT_MASK) == QT_STAT_LNK) { //qDebug() << "copy(): LINK DESTINATION"; if (!QFile::remove(dest)) { if (auto err = execWithElevatedPrivilege(DEL, {_dest}, errno)) { if (!err.wasCanceled()) { error(KIO::ERR_CANNOT_DELETE_ORIGINAL, dest); } return; } } } else if ((buff_dest.st_mode & QT_STAT_MASK) == QT_STAT_REG) { _dest_backup = _dest; dest.append(QStringLiteral(".part")); _dest = QFile::encodeName(dest); } } else { error(KIO::ERR_FILE_ALREADY_EXIST, dest); return; } } QFile src_file(src); if (!src_file.open(QIODevice::ReadOnly)) { if (auto err = tryOpen(src_file, _src, O_RDONLY, S_IRUSR, errno)) { if (!err.wasCanceled()) { error(KIO::ERR_CANNOT_OPEN_FOR_READING, src); } return; } } #if HAVE_FADVISE posix_fadvise(src_file.handle(), 0, 0, POSIX_FADV_SEQUENTIAL); #endif QFile dest_file(dest); if (!dest_file.open(QIODevice::Truncate | QIODevice::WriteOnly)) { if (auto err = tryOpen(dest_file, _dest, O_WRONLY | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR, errno)) { if (!err.wasCanceled()) { // qDebug() << "###### COULD NOT WRITE " << dest; if (err == EACCES) { error(KIO::ERR_WRITE_ACCESS_DENIED, dest); } else { error(KIO::ERR_CANNOT_OPEN_FOR_WRITING, dest); } } src_file.close(); return; } } // nobody shall be allowed to peek into the file during creation // Note that error handling is omitted for this call, we don't want to error on e.g. VFAT dest_file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner); #if HAVE_FADVISE posix_fadvise(dest_file.handle(), 0, 0, POSIX_FADV_SEQUENTIAL); #endif #if HAVE_POSIX_ACL acl = acl_get_fd(src_file.handle()); if (acl && !isExtendedACL(acl)) { // qDebug() << _dest.data() << " doesn't have extended ACL"; acl_free(acl); acl = nullptr; } #endif totalSize(buff_src.st_size); KIO::filesize_t processed_size = 0; char buffer[ MAX_IPC_SIZE ]; ssize_t n = 0; #ifdef USE_SENDFILE bool use_sendfile = buff_src.st_size < 0x7FFFFFFF; #endif bool existing_dest_delete_attempted = false; - while (1) { + while (!wasKilled()) { + + if (testMode && dest_file.fileName().contains(QLatin1String("slow"))) { + QThread::usleep(500); + } + #ifdef USE_SENDFILE if (use_sendfile) { off_t sf = processed_size; n = ::sendfile(dest_file.handle(), src_file.handle(), &sf, MAX_IPC_SIZE); processed_size = sf; if (n == -1 && (errno == EINVAL || errno == ENOSYS)) { //not all filesystems support sendfile() // qDebug() << "sendfile() not supported, falling back "; use_sendfile = false; } } if (!use_sendfile) #endif n = ::read(src_file.handle(), buffer, MAX_IPC_SIZE); if (n == -1) { if (errno == EINTR) { continue; } #ifdef USE_SENDFILE if (use_sendfile) { // qDebug() << "sendfile() error:" << strerror(errno); if (errno == ENOSPC) { // disk full if (!_dest_backup.isEmpty() && !existing_dest_delete_attempted) { ::unlink(_dest_backup.constData()); existing_dest_delete_attempted = true; continue; } error(KIO::ERR_DISK_FULL, dest); } else { error(KIO::ERR_SLAVE_DEFINED, i18n("Cannot copy file from %1 to %2. (Errno: %3)", src, dest, errno)); } } else #endif error(KIO::ERR_CANNOT_READ, src); src_file.close(); dest_file.close(); #if HAVE_POSIX_ACL if (acl) { acl_free(acl); } #endif if (!QFile::remove(dest)) { // don't keep partly copied file execWithElevatedPrivilege(DEL, {_dest}, errno); } return; } if (n == 0) { break; // Finished } #ifdef USE_SENDFILE if (!use_sendfile) { #endif if (dest_file.write(buffer, n) != n) { if (dest_file.error() == QFileDevice::ResourceError) { // disk full if (!_dest_backup.isEmpty() && !existing_dest_delete_attempted) { ::unlink(_dest_backup.constData()); existing_dest_delete_attempted = true; continue; } error(KIO::ERR_DISK_FULL, dest); } else { qCWarning(KIO_FILE) << "Couldn't write[2]. Error:" << dest_file.errorString(); error(KIO::ERR_CANNOT_WRITE, dest); } #if HAVE_POSIX_ACL if (acl) { acl_free(acl); } #endif if (!QFile::remove(dest)) { // don't keep partly copied file execWithElevatedPrivilege(DEL, {_dest}, errno); } return; } processed_size += n; #ifdef USE_SENDFILE } #endif processedSize(processed_size); } src_file.close(); dest_file.close(); + if (wasKilled()) { + qCDebug(KIO_FILE) << "Clean dest file after ioslave was killed:" << dest; + if (!QFile::remove(dest)) { // don't keep partly copied file + execWithElevatedPrivilege(DEL, {_dest}, errno); + } + error(KIO::ERR_USER_CANCELED, dest); + return; + } + if (dest_file.error() != QFile::NoError) { qCWarning(KIO_FILE) << "Error when closing file descriptor[2]:" << dest_file.errorString(); error(KIO::ERR_CANNOT_WRITE, dest); #if HAVE_POSIX_ACL if (acl) { acl_free(acl); } #endif if (!QFile::remove(dest)) { // don't keep partly copied file execWithElevatedPrivilege(DEL, {_dest}, errno); } return; } // set final permissions // if no special mode given, preserve the mode from the sourcefile if (_mode == -1) { _mode = buff_src.st_mode; } if ((::chmod(_dest.data(), _mode) != 0) #if HAVE_POSIX_ACL || (acl && acl_set_file(_dest.data(), ACL_TYPE_ACCESS, acl) != 0) #endif ) { const int errCode = errno; KMountPoint::Ptr mp = KMountPoint::currentMountPoints().findByPath(dest); // Eat the error if the filesystem apparently doesn't support chmod. // This test isn't fullproof though, vboxsf (VirtualBox shared folder) supports // chmod if the host is Linux, and doesn't if the host is Windows. Hard to detect. if (mp && mp->testFileSystemFlag(KMountPoint::SupportsChmod)) { if (tryChangeFileAttr(CHMOD, {_dest, _mode}, errCode)) { qCWarning(KIO_FILE) << "Could not change permissions for" << dest; } } } #if HAVE_POSIX_ACL if (acl) { acl_free(acl); } #endif // preserve ownership if (::chown(_dest.data(), -1 /*keep user*/, buff_src.st_gid) == 0) { // as we are the owner of the new file, we can always change the group, but // we might not be allowed to change the owner (void)::chown(_dest.data(), buff_src.st_uid, -1 /*keep group*/); } else { if (tryChangeFileAttr(CHOWN, {_dest, buff_src.st_uid, buff_src.st_gid}, errno)) { qCWarning(KIO_FILE) << "Couldn't preserve group for" << dest; } } // copy access and modification time struct utimbuf ut; ut.actime = buff_src.st_atime; ut.modtime = buff_src.st_mtime; if (::utime(_dest.data(), &ut) != 0) { if (tryChangeFileAttr(UTIME, {_dest, qint64(ut.actime), qint64(ut.modtime)}, errno)) { qCWarning(KIO_FILE) << "Couldn't preserve access and modification time for" << dest; } } if (!_dest_backup.isEmpty()) { if (::unlink(_dest_backup.constData()) == -1) { qCWarning(KIO_FILE) << "Couldn't remove original dest" << _dest_backup << "(" << strerror(errno) << ")"; } if (::rename(_dest.constData(), _dest_backup.constData()) == -1) { qCWarning(KIO_FILE) << "Couldn't rename" << _dest << "to" << _dest_backup << "(" << strerror(errno) << ")"; } } processedSize(buff_src.st_size); finished(); } static bool isLocalFileSameHost(const QUrl &url) { if (!url.isLocalFile()) { return false; } if (url.host().isEmpty() || (url.host() == QLatin1String("localhost"))) { return true; } char hostname[ 256 ]; hostname[ 0 ] = '\0'; if (!gethostname(hostname, 255)) { hostname[sizeof(hostname) - 1] = '\0'; } return (QString::compare(url.host(), QLatin1String(hostname), Qt::CaseInsensitive) == 0); } #if HAVE_SYS_XATTR_H static bool isNtfsHidden(const QString &filename) { constexpr auto attrName = "system.ntfs_attrib_be"; const auto filenameEncoded = QFile::encodeName(filename); #ifdef Q_OS_MACOS auto length = getxattr(filenameEncoded.data(), attrName, nullptr, 0, 0, XATTR_NOFOLLOW); #else auto length = getxattr(filenameEncoded.data(), attrName, nullptr, 0); #endif if (length <= 0) { return false; } constexpr size_t xattr_size = 1024; char strAttr[xattr_size]; #ifdef Q_OS_MACOS length = getxattr(filenameEncoded.data(), attrName, strAttr, xattr_size, 0, XATTR_NOFOLLOW); #else length = getxattr(filenameEncoded.data(), attrName, strAttr, xattr_size); #endif if (length <= 0) { return false; } // Decode result to hex string static constexpr auto digits = "0123456789abcdef"; QVarLengthArray hexAttr(static_cast(length) * 2 + 4); char *c = strAttr; char *e = hexAttr.data(); *e++ ='0'; *e++ = 'x'; for (auto n = 0; n < length; n++, c++) { *e++ = digits[(static_cast(*c) >> 4)]; *e++ = digits[(static_cast(*c) & 0x0F)]; } *e = '\0'; // Decode hex string to int auto intAttr = static_cast(strtol(hexAttr.data(), nullptr, 16)); constexpr auto FILE_ATTRIBUTE_HIDDEN = 0x2u; return static_cast(intAttr & FILE_ATTRIBUTE_HIDDEN); } #endif void FileProtocol::listDir(const QUrl &url) { if (!isLocalFileSameHost(url)) { QUrl redir(url); redir.setScheme(configValue(QStringLiteral("DefaultRemoteProtocol"), QStringLiteral("smb"))); redirection(redir); // qDebug() << "redirecting to " << redir; finished(); return; } const QString path(url.toLocalFile()); const QByteArray _path(QFile::encodeName(path)); DIR *dp = opendir(_path.data()); if (dp == nullptr) { switch (errno) { case ENOENT: error(KIO::ERR_DOES_NOT_EXIST, path); return; case ENOTDIR: error(KIO::ERR_IS_FILE, path); break; #ifdef ENOMEDIUM case ENOMEDIUM: error(ERR_SLAVE_DEFINED, i18n("No media in device for %1", path)); break; #endif default: error(KIO::ERR_CANNOT_ENTER_DIRECTORY, path); break; } return; } /* set the current dir to the path to speed up in not having to pass an absolute path. We restore the path later to get out of the path - the kernel wouldn't unmount or delete directories we keep as active directory. And as the slave runs in the background, it's hard to see for the user what the problem would be */ const QString pathBuffer(QDir::currentPath()); if (!QDir::setCurrent(path)) { closedir(dp); error(ERR_CANNOT_ENTER_DIRECTORY, path); return; } const QString sDetails = metaData(QStringLiteral("details")); const int details = sDetails.isEmpty() ? 2 : sDetails.toInt(); //qDebug() << "========= LIST " << url << "details=" << details << " ========="; UDSEntry entry; #ifndef HAVE_DIRENT_D_TYPE QT_STATBUF st; #endif QT_DIRENT *ep; while ((ep = QT_READDIR(dp)) != nullptr) { entry.clear(); const QString filename = QFile::decodeName(ep->d_name); /* * details == 0 (if statement) is the fast code path. * We only get the file name and type. After that we emit * the result. * * The else statement is the slow path that requests all * file information in file.cpp. It executes a stat call * for every entry thus becoming slower. * */ if (details == 0) { entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename); #ifdef HAVE_DIRENT_D_TYPE entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, (ep->d_type == DT_DIR) ? S_IFDIR : S_IFREG); const bool isSymLink = (ep->d_type == DT_LNK); #else // oops, no fast way, we need to stat (e.g. on Solaris) if (QT_LSTAT(ep->d_name, &st) == -1) { continue; // how can stat fail? } entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, ((st.st_mode & QT_STAT_MASK) == QT_STAT_DIR) ? S_IFDIR : S_IFREG); const bool isSymLink = ((st.st_mode & QT_STAT_MASK) == QT_STAT_LNK); #endif if (isSymLink) { // for symlinks obey the UDSEntry contract and provide UDS_LINK_DEST // even if we don't know the link dest (and DeleteJob doesn't care...) entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, QStringLiteral("Dummy Link Target")); } listEntry(entry); } else { if (createUDSEntry(filename, QByteArray(ep->d_name), entry, details)) { #if HAVE_SYS_XATTR_H if (isNtfsHidden(filename)) { bool ntfsHidden = true; // Bug 392913: NTFS root volume is always "hidden", ignore this if (ep->d_type == DT_DIR || ep->d_type == DT_UNKNOWN || ep->d_type == DT_LNK) { const QString fullFilePath = QDir(filename).canonicalPath(); auto mountPoint = KMountPoint::currentMountPoints().findByPath(fullFilePath); if (mountPoint && mountPoint->mountPoint() == fullFilePath) { ntfsHidden = false; } } if (ntfsHidden) { entry.fastInsert(KIO::UDSEntry::UDS_HIDDEN, 1); } } #endif listEntry(entry); } } } closedir(dp); // Restore the path QDir::setCurrent(pathBuffer); finished(); } void FileProtocol::rename(const QUrl &srcUrl, const QUrl &destUrl, KIO::JobFlags _flags) { char off_t_should_be_64_bits[sizeof(off_t) >= 8 ? 1 : -1]; (void) off_t_should_be_64_bits; const QString src = srcUrl.toLocalFile(); const QString dest = destUrl.toLocalFile(); const QByteArray _src(QFile::encodeName(src)); const QByteArray _dest(QFile::encodeName(dest)); QT_STATBUF buff_src; if (QT_LSTAT(_src.data(), &buff_src) == -1) { if (errno == EACCES) { error(KIO::ERR_ACCESS_DENIED, src); } else { error(KIO::ERR_DOES_NOT_EXIST, src); } return; } QT_STATBUF buff_dest; // stat symlinks here (lstat, not stat), to avoid ERR_IDENTICAL_FILES when replacing symlink // with its target (#169547) bool dest_exists = (QT_LSTAT(_dest.data(), &buff_dest) != -1); if (dest_exists) { if (same_inode(buff_dest, buff_src)) { error(KIO::ERR_IDENTICAL_FILES, dest); return; } if ((buff_dest.st_mode & QT_STAT_MASK) == QT_STAT_DIR) { error(KIO::ERR_DIR_ALREADY_EXIST, dest); return; } if (!(_flags & KIO::Overwrite)) { error(KIO::ERR_FILE_ALREADY_EXIST, dest); return; } } if (::rename(_src.data(), _dest.data())) { if (auto err = execWithElevatedPrivilege(RENAME, {_src, _dest}, errno)) { if (!err.wasCanceled()) { if ((err == EACCES) || (err == EPERM)) { error(KIO::ERR_WRITE_ACCESS_DENIED, dest); } else if (err == EXDEV) { error(KIO::ERR_UNSUPPORTED_ACTION, QStringLiteral("rename")); } else if (err == EROFS) { // The file is on a read-only filesystem error(KIO::ERR_CANNOT_DELETE, src); } else { error(KIO::ERR_CANNOT_RENAME, src); } } return; } } finished(); } void FileProtocol::symlink(const QString &target, const QUrl &destUrl, KIO::JobFlags flags) { const QString dest = destUrl.toLocalFile(); // Assume dest is local too (wouldn't be here otherwise) if (::symlink(QFile::encodeName(target).constData(), QFile::encodeName(dest).constData()) == -1) { // Does the destination already exist ? if (errno == EEXIST) { if ((flags & KIO::Overwrite)) { // Try to delete the destination if (unlink(QFile::encodeName(dest).constData()) != 0) { if (auto err = execWithElevatedPrivilege(DEL, {dest}, errno)) { if (!err.wasCanceled()) { error(KIO::ERR_CANNOT_DELETE, dest); } return; } } // Try again - this won't loop forever since unlink succeeded symlink(target, destUrl, flags); return; } else { QT_STATBUF buff_dest; if (QT_LSTAT(QFile::encodeName(dest).constData(), &buff_dest) == 0 && ((buff_dest.st_mode & QT_STAT_MASK) == QT_STAT_DIR)) { error(KIO::ERR_DIR_ALREADY_EXIST, dest); } else { error(KIO::ERR_FILE_ALREADY_EXIST, dest); } return; } } else { if (auto err = execWithElevatedPrivilege(SYMLINK, {dest, target}, errno)) { if (!err.wasCanceled()) { // Some error occurred while we tried to symlink error(KIO::ERR_CANNOT_SYMLINK, dest); } return; } } } finished(); } void FileProtocol::del(const QUrl &url, bool isfile) { const QString path = url.toLocalFile(); const QByteArray _path(QFile::encodeName(path)); /***** * Delete files *****/ if (isfile) { // qDebug() << "Deleting file "<< url; if (unlink(_path.data()) == -1) { if (auto err = execWithElevatedPrivilege(DEL, {_path}, errno)) { if (!err.wasCanceled()) { if ((err == EACCES) || (err == EPERM)) { error(KIO::ERR_ACCESS_DENIED, path); } else if (err == EISDIR) { error(KIO::ERR_IS_DIRECTORY, path); } else { error(KIO::ERR_CANNOT_DELETE, path); } } return; } } } else { /***** * Delete empty directory *****/ // qDebug() << "Deleting directory " << url; if (metaData(QStringLiteral("recurse")) == QLatin1String("true")) { if (!deleteRecursive(path)) { return; } } if (QT_RMDIR(_path.data()) == -1) { if (auto err = execWithElevatedPrivilege(RMDIR, {_path}, errno)) { if (!err.wasCanceled()) { if ((err == EACCES) || (err == EPERM)) { error(KIO::ERR_ACCESS_DENIED, path); } else { // qDebug() << "could not rmdir " << perror; error(KIO::ERR_CANNOT_RMDIR, path); } } return; } } } finished(); } void FileProtocol::chown(const QUrl &url, const QString &owner, const QString &group) { const QString path = url.toLocalFile(); const QByteArray _path(QFile::encodeName(path)); uid_t uid; gid_t gid; // get uid from given owner { struct passwd *p = ::getpwnam(owner.toLocal8Bit().constData()); if (! p) { error(KIO::ERR_SLAVE_DEFINED, i18n("Could not get user id for given user name %1", owner)); return; } uid = p->pw_uid; } // get gid from given group { struct group *p = ::getgrnam(group.toLocal8Bit().constData()); if (! p) { error(KIO::ERR_SLAVE_DEFINED, i18n("Could not get group id for given group name %1", group)); return; } gid = p->gr_gid; } if (::chown(_path.constData(), uid, gid) == -1) { if (auto err = execWithElevatedPrivilege(CHOWN, {_path, uid, gid}, errno)) { if (!err.wasCanceled()) { switch (err) { case EPERM: case EACCES: error(KIO::ERR_ACCESS_DENIED, path); break; case ENOSPC: error(KIO::ERR_DISK_FULL, path); break; default: error(KIO::ERR_CANNOT_CHOWN, path); } } } } else { finished(); } } void FileProtocol::stat(const QUrl &url) { if (!isLocalFileSameHost(url)) { redirect(url); return; } /* directories may not have a slash at the end if * we want to stat() them; it requires that we * change into it .. which may not be allowed * stat("/is/unaccessible") -> rwx------ * stat("/is/unaccessible/") -> EPERM H.Z. * This is the reason for the -1 */ const QString path(url.adjusted(QUrl::StripTrailingSlash).toLocalFile()); const QByteArray _path(QFile::encodeName(path)); const QString sDetails = metaData(QStringLiteral("details")); const int details = sDetails.isEmpty() ? 2 : sDetails.toInt(); UDSEntry entry; if (!createUDSEntry(url.fileName(), _path, entry, details)) { error(KIO::ERR_DOES_NOT_EXIST, path); return; } #if 0 ///////// debug code MetaData::iterator it1 = mOutgoingMetaData.begin(); for (; it1 != mOutgoingMetaData.end(); it1++) { // qDebug() << it1.key() << " = " << it1.data(); } ///////// #endif statEntry(entry); finished(); } PrivilegeOperationReturnValue FileProtocol::execWithElevatedPrivilege(ActionType action, const QVariantList &args, int errcode) { if (privilegeOperationUnitTestMode()) { return PrivilegeOperationReturnValue::success(); } // temporarily disable privilege execution if (true) { return PrivilegeOperationReturnValue::failure(errcode); } if (!(errcode == EACCES || errcode == EPERM)) { return PrivilegeOperationReturnValue::failure(errcode); } const QString operationDetails = actionDetails(action, args); KIO::PrivilegeOperationStatus opStatus = requestPrivilegeOperation(operationDetails); if (opStatus != KIO::OperationAllowed) { if (opStatus == KIO::OperationCanceled) { error(KIO::ERR_USER_CANCELED, QString()); return PrivilegeOperationReturnValue::canceled(); } return PrivilegeOperationReturnValue::failure(errcode); } const QUrl targetUrl = QUrl::fromLocalFile(args.first().toString()); // target is always the first item. const bool useParent = action != CHOWN && action != CHMOD && action != UTIME; const QString targetPath = useParent ? targetUrl.adjusted(QUrl::RemoveFilename).toLocalFile() : targetUrl.toLocalFile(); bool userIsOwner = QFileInfo(targetPath).ownerId() == getuid(); if (action == RENAME) { // for rename check src and dest owner QString dest = QUrl(args[1].toString()).toLocalFile(); userIsOwner = userIsOwner && QFileInfo(dest).ownerId() == getuid(); } if (userIsOwner) { error(KIO::ERR_PRIVILEGE_NOT_REQUIRED, targetPath); return PrivilegeOperationReturnValue::canceled(); } QByteArray helperArgs; QDataStream out(&helperArgs, QIODevice::WriteOnly); out << action; for (const QVariant &arg : args) { out << arg; } const QString actionId = QStringLiteral("org.kde.kio.file.exec"); KAuth::Action execAction(actionId); execAction.setHelperId(QStringLiteral("org.kde.kio.file")); QVariantMap argv; argv.insert(QStringLiteral("arguments"), helperArgs); execAction.setArguments(argv); auto reply = execAction.execute(); if (reply->exec()) { addTemporaryAuthorization(actionId); return PrivilegeOperationReturnValue::success(); } return PrivilegeOperationReturnValue::failure(KIO::ERR_ACCESS_DENIED); }