diff --git a/autotests/jobtest.cpp b/autotests/jobtest.cpp index 76466f9b..f49f691b 100644 --- a/autotests/jobtest.cpp +++ b/autotests/jobtest.cpp @@ -1,2256 +1,2296 @@ /* 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 #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 const KIO::UDSEntry &entry = job->statResult(); // we only get filename, access, type, size, uid, gid, btime, mtime, atime QVERIFY(entry.contains(KIO::UDSEntry::UDS_NAME)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_SIZE)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_FILE_TYPE)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_USER)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_GROUP)); //QVERIFY(entry.contains(KIO::UDSEntry::UDS_CREATION_TIME)); // only true if st_birthtime or statx is used QVERIFY(entry.contains(KIO::UDSEntry::UDS_MODIFICATION_TIME)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS_TIME)); QCOMPARE(entry.count(), 8 + (entry.contains(KIO::UDSEntry::UDS_CREATION_TIME) ? 1 : 0)); 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 } void JobTest::statDetailsBasic() { const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); const QUrl url(QUrl::fromLocalFile(filePath)); KIO::StatJob *job = KIO::statDetails(url, KIO::StatJob::StatSide::SourceSide, KIO::StatBasic, KIO::HideProgressInfo); QVERIFY(job); QVERIFY2(job->exec(), qPrintable(job->errorString())); // TODO set setSide const KIO::UDSEntry &entry = job->statResult(); // we only get filename, access, type, size, (no linkdest) QVERIFY(entry.contains(KIO::UDSEntry::UDS_NAME)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_SIZE)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_FILE_TYPE)); QCOMPARE(entry.count(), 4); QVERIFY(!entry.isDir()); QVERIFY(!entry.isLink()); QVERIFY(entry.numberValue(KIO::UDSEntry::UDS_ACCESS) > 0); 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 // for the requested fields 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(), ""); QCOMPARE(kioItem.group(), ""); QCOMPARE(kioItem.mimetype(), "application/octet-stream"); QCOMPARE(kioItem.permissions(), 438); QCOMPARE(kioItem.time(KFileItem::ModificationTime), QDateTime()); QCOMPARE(kioItem.time(KFileItem::AccessTime), QDateTime()); } void JobTest::statDetailsBasicSetDetails() { const QString filePath = homeTmpDir() + "fileFromHome"; createTestFile(filePath); const QUrl url(QUrl::fromLocalFile(filePath)); KIO::StatJob *job = KIO::stat(url); job->setDetails(KIO::StatBasic); QVERIFY(job); QVERIFY2(job->exec(), qPrintable(job->errorString())); // TODO set setSide const KIO::UDSEntry &entry = job->statResult(); // we only get filename, access, type, size, (no linkdest) QVERIFY(entry.contains(KIO::UDSEntry::UDS_NAME)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_SIZE)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_FILE_TYPE)); QCOMPARE(entry.count(), 4); QVERIFY(!entry.isDir()); QVERIFY(!entry.isLink()); QVERIFY(entry.numberValue(KIO::UDSEntry::UDS_ACCESS) > 0); 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 // for the requested fields 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(), ""); QCOMPARE(kioItem.group(), ""); QCOMPARE(kioItem.mimetype(), "application/octet-stream"); QCOMPARE(kioItem.permissions(), 438); QCOMPARE(kioItem.time(KFileItem::ModificationTime), QDateTime()); QCOMPARE(kioItem.time(KFileItem::AccessTime), QDateTime()); } +void JobTest::statWithInode() +{ + const QString filePath = homeTmpDir() + "fileFromHome"; + createTestFile(filePath); + const QUrl url(QUrl::fromLocalFile(filePath)); + KIO::StatJob *job = KIO::statDetails(url, KIO::StatJob::SourceSide, KIO::StatInode); + QVERIFY(job); + QVERIFY2(job->exec(), qPrintable(job->errorString())); + + const KIO::UDSEntry &entry = job->statResult(); + QVERIFY(entry.contains(KIO::UDSEntry::UDS_DEVICE_ID)); + QVERIFY(entry.contains(KIO::UDSEntry::UDS_INODE)); + QCOMPARE(entry.count(), 2); + + const QString path = otherTmpDir() + "otherFile"; + createTestFile(path); + const QUrl otherUrl(QUrl::fromLocalFile(path)); + KIO::StatJob *otherJob = KIO::statDetails(otherUrl, KIO::StatJob::SourceSide, KIO::StatInode); + QVERIFY(otherJob); + QVERIFY2(otherJob->exec(), qPrintable(otherJob->errorString())); + + const KIO::UDSEntry otherEntry = otherJob->statResult(); + QVERIFY(otherEntry.contains(KIO::UDSEntry::UDS_DEVICE_ID)); + QVERIFY(otherEntry.contains(KIO::UDSEntry::UDS_INODE)); + QCOMPARE(otherEntry.count(), 2); + + const int device = entry.numberValue(KIO::UDSEntry::UDS_DEVICE_ID); + const int otherDevice = otherEntry.numberValue(KIO::UDSEntry::UDS_DEVICE_ID); + + // this test doesn't make sense on the CI as it's an LXC container with one partition + if (otherTmpDirIsOnSamePartition()) { + // On the CI where the two tmp dirs are on the only parition available + // in the LXC container, the device ID's would be identical + QCOMPARE(device, otherDevice); + } + else { + QVERIFY(device != otherDevice); + } +} + #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::statDetails(url, KIO::StatJob::StatSide::SourceSide, KIO::StatBasic | KIO::StatResolveSymlink | KIO::StatUser | KIO::StatTime, KIO::HideProgressInfo); QVERIFY(job); QVERIFY2(job->exec(), qPrintable(job->errorString())); // TODO set setSide, setDetails const KIO::UDSEntry &entry = job->statResult(); // we only get filename, access, type, size, linkdest, uid, gid, btime, mtime, atime QVERIFY(entry.contains(KIO::UDSEntry::UDS_NAME)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_SIZE)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_FILE_TYPE)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_LINK_DEST)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_USER)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_GROUP)); //QVERIFY(entry.contains(KIO::UDSEntry::UDS_CREATION_TIME)); // only true if st_birthtime or statx is used QVERIFY(entry.contains(KIO::UDSEntry::UDS_MODIFICATION_TIME)); QVERIFY(entry.contains(KIO::UDSEntry::UDS_ACCESS_TIME)); QCOMPARE(entry.count(), 9 + (entry.contains(KIO::UDSEntry::UDS_CREATION_TIME) ? 1 : 0)); QVERIFY(!entry.isDir()); QVERIFY(entry.isLink()); QVERIFY(entry.numberValue(KIO::UDSEntry::UDS_ACCESS) > 0); 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)); QVERIFY(!spyRenamed.isEmpty()); auto list = spyRenamed.takeFirst(); QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(destDir + prefix + "(1)")); QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(file3)); // Normally we'd see renamed(1, 3) and renamed(2, 4) // But across partitions, direct rename fails, and we end up with a task list of // 1->3, 2->3 since renaming 1 to 3 didn't happen yet. // so renamed(2, 3) is emitted, as if the user had chosen that. // And when that fails, we then get (3, 4) if (spyRenamed.count() == 2) { list = spyRenamed.takeFirst(); QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(destDir + prefix + "(2)")); QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(file3)); list = spyRenamed.takeFirst(); QCOMPARE(list.at(1).toUrl(), QUrl::fromLocalFile(file3)); QCOMPARE(list.at(2).toUrl(), QUrl::fromLocalFile(file4)); } else { QCOMPARE(spyRenamed.count(), 1); 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_slow_") + QString::fromLatin1(QTest::currentDataTag()); QTemporaryFile f(srcTemplate); if (!f.open()) { qFatal("Couldn't open %s", qPrintable(f.fileName())); } f.seek(999999); f.write("0"); f.close(); 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); QSignalSpy spyFinished(copyJob, &KIO::Job::finished); connect(copyJob, &KIO::Job::processedSize, this, [destFile, suspend, destToCheck](KJob *job, qulonglong processedSize) { if (processedSize > 0) { QVERIFY2(QFile::exists(destToCheck), qPrintable(destToCheck)); if (suspend) { job->suspend(); } QVERIFY(job->kill()); } }); QVERIFY(!copyJob->exec()); QCOMPARE(spyProcessedSize.count(), 1); 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/autotests/jobtest.h b/autotests/jobtest.h index 1718662d..d8ac33fc 100644 --- a/autotests/jobtest.h +++ b/autotests/jobtest.h @@ -1,158 +1,159 @@ /* This file is part of the KDE project Copyright (C) 2004 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. */ #ifndef JOBTEST_H #define JOBTEST_H #include #include #include class JobTest : public QObject { Q_OBJECT public: JobTest() {} private Q_SLOTS: void initTestCase(); void cleanupTestCase(); // Local tests (kio_file only) void storedGet(); void put(); void storedPut(); void storedPutIODevice(); void storedPutIODeviceFile(); void storedPutIODeviceTempFile(); void storedPutIODeviceFastDevice(); void storedPutIODeviceSlowDevice(); void storedPutIODeviceSlowDeviceBigChunk(); void asyncStoredPutReadyReadAfterFinish(); void copyFileToSamePartition(); void copyDirectoryToSamePartition(); void copyDirectoryToExistingDirectory(); void copyFileToOtherPartition(); void copyDirectoryToOtherPartition(); void copyRelativeSymlinkToSamePartition(); void copyAbsoluteSymlinkToOtherPartition(); void copyFolderWithUnaccessibleSubfolder(); void copyDataUrl(); void suspendFileCopy(); void suspendCopy(); void listRecursive(); void listFile(); void killJob(); void killJobBeforeStart(); void deleteJobBeforeStart(); void directorySize(); void directorySizeError(); void moveFileToSamePartition(); void moveDirectoryToSamePartition(); void moveDirectoryIntoItself(); void moveFileToOtherPartition(); void moveSymlinkToOtherPartition(); void moveDirectoryToOtherPartition(); void moveFileNoPermissions(); void moveDirectoryNoPermissions(); void moveDirectoryToReadonlyFilesystem_data(); void moveDirectoryToReadonlyFilesystem(); void deleteFile(); void deleteDirectory(); void deleteSymlink(); void deleteManyDirs(); void deleteManyFilesIndependently(); void deleteManyFilesTogether(); void rmdirEmpty(); void rmdirNotEmpty(); void stat(); void statDetailsBasic(); void statDetailsBasicSetDetails(); + void statWithInode(); #ifndef Q_OS_WIN void statSymlink(); #endif void mostLocalUrl(); void chmodFile(); #ifdef Q_OS_UNIX void chmodSticky(); #endif void chmodFileError(); void mimeType(); void mimeTypeError(); void calculateRemainingSeconds(); void moveFileDestAlreadyExists(); void moveDestAlreadyExistsAutoRename_data(); void moveDestAlreadyExistsAutoRename(); void copyDirectoryAlreadyExistsSkip(); void copyFileAlreadyExistsRename(); void safeOverwrite(); void safeOverwrite_data(); void moveAndOverwrite(); void moveOverSymlinkToSelf(); void createSymlink(); void createSymlinkTargetDirDoesntExist(); void createSymlinkAsShouldSucceed(); void createSymlinkAsShouldFailDirectoryExists(); void createSymlinkAsShouldFailFileExists(); void createSymlinkWithOverwriteShouldWork(); void createBrokenSymlink(); void cancelCopyAndCleanDest(); void cancelCopyAndCleanDest_data(); // Remote tests //void copyFileToSystem(); void getInvalidUrl(); void multiGet(); Q_SIGNALS: void exitLoop(); protected Q_SLOTS: void slotEntries(KIO::Job *, const KIO::UDSEntryList &lst); void slotGetResult(KJob *); void slotDataReq(KIO::Job *, QByteArray &); void slotResult(KJob *); void slotMimetype(KIO::Job *, const QString &); private: void enterLoop(); enum { AlreadyExists = 1 }; void copyLocalFile(const QString &src, const QString &dest); void copyLocalDirectory(const QString &src, const QString &dest, int flags = 0); void moveLocalFile(const QString &src, const QString &dest); void moveLocalDirectory(const QString &src, const QString &dest); //void copyFileToSystem( bool resolve_local_urls ); void deleteSymlink(bool using_fast_path); void deleteManyDirs(bool using_fast_path); void deleteManyFilesTogether(bool using_fast_path); void moveDestAlreadyExistsAutoRename(const QString &destDir, bool moveDirs); int m_result; QByteArray m_data; QStringList m_names; int m_dataReqCount; QString m_mimetype; }; #endif diff --git a/src/ioslaves/file/file_unix.cpp b/src/ioslaves/file/file_unix.cpp index 5d2072ca..67cfbf81 100644 --- a/src/ioslaves/file/file_unix.cpp +++ b/src/ioslaves/file/file_unix.cpp @@ -1,1388 +1,1389 @@ /* 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 #include #endif #include #include #include #include "fdreceiver.h" #include "statjob.h" #if HAVE_STATX #include +#include // for makedev() #endif //sendfile has different semantics in different platforms #if HAVE_SENDFILE && defined Q_OS_LINUX #define USE_SENDFILE 1 #include #endif using namespace KIO; /* 512 kB */ #define MAX_IPC_SIZE (1024*512) 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); } /************************************* * * ACL handling helpers * *************************************/ #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; } 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 static QHash staticUserCache; static QHash staticGroupCache; static QString getUserName(KUserId uid) { if (Q_UNLIKELY(!uid.isValid())) { return QString(); } auto it = staticUserCache.find(uid); if (it == staticUserCache.end()) { KUser user(uid); QString name = user.loginName(); if (name.isEmpty()) { name = uid.toString(); } it = staticUserCache.insert(uid, name); } return *it; } static QString getGroupName(KGroupId gid) { if (Q_UNLIKELY(!gid.isValid())) { return QString(); } auto it = staticGroupCache.find(gid); if (it == staticGroupCache.end()) { KUserGroup group(gid); QString name = group.name(); if (name.isEmpty()) { name = gid.toString(); } it = staticGroupCache.insert(gid, name); } return *it; } #if HAVE_STATX // statx syscall is available inline int LSTAT(const char* path, struct statx * buff, KIO::StatDetails details) { uint32_t mask = 0; if (details & KIO::StatBasic) { // filename, access, type, size, linkdest mask |= STATX_SIZE | STATX_TYPE; } if (details & KIO::StatUser) { // uid, gid mask |= STATX_UID | STATX_GID; } if (details & KIO::StatTime) { // atime, mtime, btime mask |= STATX_ATIME | STATX_MTIME | STATX_BTIME; } if (details & KIO::StatInode) { // dev, inode mask |= STATX_INO; } return statx(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW, mask, buff); } inline int STAT(const char* path, struct statx * buff, KIO::StatDetails details) { uint32_t mask = 0; if (details & KIO::StatBasic) { // filename, access, type, size, linkdest mask |= STATX_SIZE | STATX_TYPE; } if (details & KIO::StatUser) { // uid, gid mask |= STATX_UID | STATX_GID; } if (details & KIO::StatTime) { // atime, mtime, btime mask |= STATX_ATIME | STATX_MTIME | STATX_BTIME; } // KIO::Inode is ignored as when STAT is called, the entry inode field has already been filled return statx(AT_FDCWD, path, AT_STATX_SYNC_AS_STAT, mask, 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 dev_t stat_dev(struct statx &buf) { return makedev(buf.stx_dev_major, buf.stx_dev_minor); } 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, KIO::StatDetails details) { Q_UNUSED(details) return QT_LSTAT(path, buff); } inline int STAT(const char* path, QT_STATBUF * buff, KIO::StatDetails details) { Q_UNUSED(details) 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; } 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; } 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 static bool createUDSEntry(const QString &filename, const QByteArray &path, UDSEntry &entry, KIO::StatDetails details) { assert(entry.count() == 0); // by contract :-) int entries = 0; if (details & KIO::StatBasic) { // filename, access, type, size, linkdest entries += 5; } if (details & KIO::StatUser) { // uid, gid entries += 2; } if (details & KIO::StatTime) { // atime, mtime, btime entries += 3; } if (details & KIO::StatAcl) { // acl data entries += 3; } if (details & KIO::StatInode) { // dev, inode entries += 2; } entry.reserve(entries); if (details & KIO::StatBasic) { entry.fastInsert(KIO::UDSEntry::UDS_NAME, filename); } bool isBrokenSymLink = false; #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, details) == 0) { if ((stat_mode(buff) & QT_STAT_MASK) == QT_STAT_LNK) { QByteArray linkTargetBuffer; if (details & (KIO::StatBasic|KIO::StatResolveSymlink)) { // 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); 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); entry.fastInsert(KIO::UDSEntry::UDS_LINK_DEST, linkTarget); } // A symlink if (details & KIO::StatResolveSymlink) { if (STAT(path.constData(), &buff, details) == -1) { isBrokenSymLink = true; } else { #if HAVE_POSIX_ACL if (details & KIO::StatAcl) { // 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; } mode_t type = 0; if (details & KIO::StatBasic) { mode_t access; signed long long size; 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 & KIO::StatAcl) { /* 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 & KIO::StatUser) { entry.fastInsert(KIO::UDSEntry::UDS_USER, getUserName(KUserId(stat_uid(buff)))); entry.fastInsert(KIO::UDSEntry::UDS_GROUP, getGroupName(KGroupId(stat_gid(buff)))); } if (details & KIO::StatTime) { entry.fastInsert(KIO::UDSEntry::UDS_MODIFICATION_TIME, stat_mtime(buff)); entry.fastInsert(KIO::UDSEntry::UDS_ACCESS_TIME, stat_atime(buff)); #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 } if (details & KIO::StatInode) { entry.fastInsert(KIO::UDSEntry::UDS_DEVICE_ID, stat_dev(buff)); entry.fastInsert(KIO::UDSEntry::UDS_INODE, stat_ino(buff)); } return true; } 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 = true; #endif bool existing_dest_delete_attempted = false; 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); uint32_t intAttr = 0; constexpr size_t xattr_size = sizeof(intAttr); char strAttr[xattr_size]; #ifdef Q_OS_MACOS auto length = getxattr(filenameEncoded.data(), attrName, strAttr, xattr_size, 0, XATTR_NOFOLLOW); #else auto length = getxattr(filenameEncoded.data(), attrName, strAttr, xattr_size); #endif if (length <= 0) { return false; } char *c = strAttr; for (decltype(length) n = 0; n < length; ++n, ++c) { intAttr <<= 8; intAttr |= static_cast(*c); } 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 KIO::StatDetails details = getStatDetails(); //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 == KIO::StatBasic) { 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(); } } KIO::StatDetails FileProtocol::getStatDetails() { // takes care of converting old metadata details to new StatDetails // TODO KF6 : remove legacy "details" code path KIO::StatDetails details; #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 69) if (hasMetaData(QStringLiteral("statDetails"))) { #endif const QString statDetails = metaData(QStringLiteral("statDetails")); details = statDetails.isEmpty() ? KIO::StatDefaultDetails : static_cast(statDetails.toInt()); #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 69) } else { const QString sDetails = metaData(QStringLiteral("details")); details = sDetails.isEmpty() ? KIO::StatDefaultDetails : KIO::detailsToStatDetails(sDetails.toInt()); } #endif return details; } 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 KIO::StatDetails details = getStatDetails(); 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); } 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; }