diff --git a/autotests/jobremotetest.h b/autotests/jobremotetest.h --- a/autotests/jobremotetest.h +++ b/autotests/jobremotetest.h @@ -46,6 +46,7 @@ void openFileWriting(); void openFileReading(); void openFileRead0Bytes(); + void openFileTruncating(); //void calculateRemainingSeconds(); @@ -73,19 +74,21 @@ void slotFileJob2Open(KIO::Job *job); void slotFileJob2Written(KIO::Job *job, KIO::filesize_t written); void slotFileJob2Position(KIO::Job *job, KIO::filesize_t offset); - void slotFileJob2Close(KIO::Job *job); void slotFileJob3Open(KIO::Job *job); void slotFileJob3Position(KIO::Job *job, KIO::filesize_t offset); void slotFileJob3Data(KIO::Job *job, const QByteArray &data); - void slotFileJob3Close(KIO::Job *job); + + void slotFileJob4Open(KIO::Job *job); + void slotFileJob4Truncated(KIO::Job *job, KIO::filesize_t length); private: void enterLoop(); enum { AlreadyExists = 1 }; int m_result; bool m_closeSignalCalled; + QFile m_truncatedFile; QByteArray m_data; QStringList m_names; int m_dataReqCount; diff --git a/autotests/jobremotetest.cpp b/autotests/jobremotetest.cpp --- a/autotests/jobremotetest.cpp +++ b/autotests/jobremotetest.cpp @@ -276,7 +276,7 @@ { Q_UNUSED(job); m_closeSignalCalled = true; - qDebug() << "+++++++++ closed"; + qDebug() << "+++++++++ filejob closed"; } //// @@ -319,8 +319,9 @@ this, &JobRemoteTest::slotFileJob2Written); connect(fileJob, &KIO::FileJob::position, this, &JobRemoteTest::slotFileJob2Position); + // Can reuse this slot (same for all tests). connect(fileJob, QOverload::of(&KIO::FileJob::close), - this, &JobRemoteTest::slotFileJob2Close); + this, &JobRemoteTest::slotFileJobClose); m_result = -1; m_closeSignalCalled = false; @@ -376,13 +377,6 @@ fileJob->read(5); } -void JobRemoteTest::slotFileJob2Close(KIO::Job *job) -{ - Q_UNUSED(job); - m_closeSignalCalled = true; - qDebug() << "+++++++++ job2 closed"; -} - //// void JobRemoteTest::slotMimetype(KIO::Job *job, const QString &type) @@ -431,7 +425,7 @@ this, &JobRemoteTest::slotFileJob3Position); // Can reuse this as well. connect(fileJob, QOverload::of(&KIO::FileJob::close), - this, &JobRemoteTest::slotFileJob3Close); + this, &JobRemoteTest::slotFileJobClose); m_result = -1; m_closeSignalCalled = false; @@ -461,9 +455,75 @@ fileJob->close(); } -void JobRemoteTest::slotFileJob3Close(KIO::Job *job) +void JobRemoteTest::openFileTruncating() +{ + QUrl u(remoteTmpUrl()); + u.setPath(u.path() + "openFileTruncating"); + + const QByteArray putData("test1"); + + KIO::StoredTransferJob *putJob = KIO::storedPut(putData, + 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 + putJob->setModificationTime(mtime); + putJob->setUiDelegate(nullptr); + connect(putJob, &KJob::result, + this, &JobRemoteTest::slotResult); + m_result = -1; + enterLoop(); + QVERIFY(m_result == 0); // no error + + m_truncatedFile.setFileName(u.toLocalFile()); + QVERIFY(m_truncatedFile.exists()); + QVERIFY(m_truncatedFile.open(QIODevice::ReadOnly)); + fileJob = KIO::open(u, QIODevice::ReadWrite); + + fileJob->setUiDelegate(nullptr); + connect(fileJob, &KJob::result, + this, &JobRemoteTest::slotResult); + connect(fileJob, &KIO::FileJob::open, + this, &JobRemoteTest::slotFileJob4Open); + connect(fileJob, &KIO::FileJob::truncated, + this, &JobRemoteTest::slotFileJob4Truncated); + // Can reuse this slot (same for all tests). + connect(fileJob, QOverload::of(&KIO::FileJob::close), + this, &JobRemoteTest::slotFileJobClose); + m_result = -1; + m_closeSignalCalled = false; + + enterLoop(); + QVERIFY(m_result == 0); // no error + QVERIFY(m_closeSignalCalled); // close signal called. +} + +void JobRemoteTest::slotFileJob4Open(KIO::Job *job) { Q_UNUSED(job); - m_closeSignalCalled = true; - qDebug() << "+++++++++ job2 closed"; + fileJob->truncate(10); + qDebug() << "Truncating file to 10"; +} + +void JobRemoteTest::slotFileJob4Truncated(KIO::Job *job, KIO::filesize_t length) +{ + Q_UNUSED(job); + if(length == 10) { + m_truncatedFile.seek(0); + QCOMPARE(m_truncatedFile.readAll(), QByteArray("test1\x00\x00\x00\x00\x00", 10)); + fileJob->truncate(4); + qDebug() << "Truncating file to 4"; + } else if(length == 4) { + m_truncatedFile.seek(0); + QCOMPARE(m_truncatedFile.readAll(), QByteArray("test")); + fileJob->truncate(0); + qDebug() << "Truncating file to 0"; + } else { + m_truncatedFile.seek(0); + QCOMPARE(m_truncatedFile.readAll(), QByteArray()); + fileJob->close(); + qDebug() << "Truncating file finished"; + } } diff --git a/src/core/commands_p.h b/src/core/commands_p.h --- a/src/core/commands_p.h +++ b/src/core/commands_p.h @@ -64,7 +64,8 @@ CMD_SEEK = 92, CMD_CLOSE = 93, CMD_HOST_INFO = 94, - CMD_FILESYSTEMFREESPACE = 95 + CMD_FILESYSTEMFREESPACE = 95, + CMD_TRUNCATE = 96 // Add new ones here once a release is done, to avoid breaking binary compatibility. // Note that protocol-specific commands shouldn't be added here, but should use special. }; diff --git a/src/core/filejob.h b/src/core/filejob.h --- a/src/core/filejob.h +++ b/src/core/filejob.h @@ -32,7 +32,7 @@ * @class KIO::FileJob filejob.h * * The file-job is an asynchronous version of normal file handling. - * It allows block-wise reading and writing, and allows seeking. Results are returned through signals. + * It allows block-wise reading and writing, and allows seeking and truncation. Results are returned through signals. * * Should always be created using KIO::open(QUrl) */ @@ -98,6 +98,19 @@ */ void seek(KIO::filesize_t offset); + /** + * Truncate + * + * The slave emits truncated() on successful truncation to the specified \p length. + * + * On error the truncated() signal is not emitted. To catch errors please + * connect to the result() signal. + * + * @param length the desired length to truncate to + * @since 5.66 + */ + void truncate(KIO::filesize_t length); + /** * Size * @@ -160,6 +173,14 @@ */ void position(KIO::Job *job, KIO::filesize_t offset); + /** + * The file has been truncated to this point. Emitted after truncate(). + * @param job the job that emitted this signal + * @param length the new length of the file + * @since 5.66 + */ + void truncated(KIO::Job *job, KIO::filesize_t length); + protected: FileJob(FileJobPrivate &dd); @@ -171,6 +192,7 @@ Q_PRIVATE_SLOT(d_func(), void slotWritten(KIO::filesize_t)) Q_PRIVATE_SLOT(d_func(), void slotFinished()) Q_PRIVATE_SLOT(d_func(), void slotPosition(KIO::filesize_t)) + Q_PRIVATE_SLOT(d_func(), void slotTruncated(KIO::filesize_t)) Q_PRIVATE_SLOT(d_func(), void slotTotalSize(KIO::filesize_t)) Q_DECLARE_PRIVATE(FileJob) diff --git a/src/core/filejob.cpp b/src/core/filejob.cpp --- a/src/core/filejob.cpp +++ b/src/core/filejob.cpp @@ -45,6 +45,7 @@ void slotWritten(KIO::filesize_t); void slotFinished(); void slotPosition(KIO::filesize_t); + void slotTruncated(KIO::filesize_t); void slotTotalSize(KIO::filesize_t); /** @@ -108,6 +109,17 @@ d->m_slave->send(CMD_SEEK, packedArgs); } +void FileJob::truncate(KIO::filesize_t length) +{ + Q_D(FileJob); + if (!d->m_open) { + return; + } + + KIO_ARGS << KIO::filesize_t(length); + d->m_slave->send(CMD_TRUNCATE, packedArgs); +} + void FileJob::close() { Q_D(FileJob); @@ -156,6 +168,12 @@ emit q->position(q, pos); } +void FileJobPrivate::slotTruncated(KIO::filesize_t length) +{ + Q_Q(FileJob); + emit q->truncated(q, length); +} + void FileJobPrivate::slotTotalSize(KIO::filesize_t t_size) { m_size = t_size; @@ -209,6 +227,9 @@ q->connect(slave, SIGNAL(position(KIO::filesize_t)), SLOT(slotPosition(KIO::filesize_t))); + q->connect(slave, SIGNAL(truncated(KIO::filesize_t)), + SLOT(slotTruncated(KIO::filesize_t))); + q->connect(slave, SIGNAL(written(KIO::filesize_t)), SLOT(slotWritten(KIO::filesize_t))); diff --git a/src/core/global.h b/src/core/global.h --- a/src/core/global.h +++ b/src/core/global.h @@ -286,6 +286,7 @@ ERR_FILE_TOO_LARGE_FOR_FAT32 = KJob::UserDefinedError + 74, ///< @since 5.54 ERR_OWNER_DIED = KJob::UserDefinedError + 75, ///< Value used between kuiserver and views when the job owner disappears unexpectedly. It should not be emitted by slaves. @since 5.54 ERR_PRIVILEGE_NOT_REQUIRED = KJob::UserDefinedError + 76, ///< used by file ioslave, @since 5.60 + ERR_CANNOT_TRUNCATE = KJob::UserDefinedError + 77, // used by FileJob::truncate, @since 5.66 }; /** diff --git a/src/core/kprotocolinfo.cpp b/src/core/kprotocolinfo.cpp --- a/src/core/kprotocolinfo.cpp +++ b/src/core/kprotocolinfo.cpp @@ -47,6 +47,7 @@ m_supportsLinking = config.readEntry("linking", false); m_supportsMoving = config.readEntry("moving", false); m_supportsOpening = config.readEntry("opening", false); + m_supportsTruncating = config.readEntry("truncating", false); m_canCopyFromFile = config.readEntry("copyFromFile", false); m_canCopyToFile = config.readEntry("copyToFile", false); m_canRenameFromFile = config.readEntry("renameFromFile", false); @@ -136,6 +137,7 @@ m_supportsLinking = json.value(QStringLiteral("linking")).toBool(); m_supportsMoving = json.value(QStringLiteral("moving")).toBool(); m_supportsOpening = json.value(QStringLiteral("opening")).toBool(); + m_supportsTruncating = json.value(QStringLiteral("truncating")).toBool(); m_canCopyFromFile = json.value(QStringLiteral("copyFromFile")).toBool(); m_canCopyToFile = json.value(QStringLiteral("copyToFile")).toBool(); m_canRenameFromFile = json.value(QStringLiteral("renameFromFile")).toBool(); diff --git a/src/core/kprotocolinfo_p.h b/src/core/kprotocolinfo_p.h --- a/src/core/kprotocolinfo_p.h +++ b/src/core/kprotocolinfo_p.h @@ -48,6 +48,7 @@ bool m_supportsLinking : 1; bool m_supportsMoving : 1; bool m_supportsOpening : 1; + bool m_supportsTruncating : 1; bool m_determineMimetypeFromExtension : 1; bool m_canCopyFromFile : 1; bool m_canCopyToFile : 1; diff --git a/src/core/kprotocolmanager.h b/src/core/kprotocolmanager.h --- a/src/core/kprotocolmanager.h +++ b/src/core/kprotocolmanager.h @@ -467,6 +467,18 @@ */ static bool supportsOpening(const QUrl &url); + /** + * Returns whether the protocol can be truncated with FileJob::truncate(KIO::filesize_t length). + * + * This corresponds to the "truncating=" field in the protocol description file. + * Valid values for this field are "true" or "false" (default). + * + * @param url the url to check + * @return true if the protocol supports truncating + * @since 5.66 + */ + static bool supportsTruncating(const QUrl &url); + /** * Returns whether the protocol can copy files/objects directly from the * filesystem itself. If not, the application will read files from the diff --git a/src/core/kprotocolmanager.cpp b/src/core/kprotocolmanager.cpp --- a/src/core/kprotocolmanager.cpp +++ b/src/core/kprotocolmanager.cpp @@ -1223,6 +1223,16 @@ return prot->m_supportsOpening; } +bool KProtocolManager::supportsTruncating(const QUrl &url) +{ + KProtocolInfoPrivate *prot = findProtocol(url); + if (!prot) { + return false; + } + + return prot->m_supportsTruncating; +} + bool KProtocolManager::canCopyFromFile(const QUrl &url) { KProtocolInfoPrivate *prot = findProtocol(url); diff --git a/src/core/slavebase.h b/src/core/slavebase.h --- a/src/core/slavebase.h +++ b/src/core/slavebase.h @@ -206,6 +206,11 @@ void written(KIO::filesize_t _bytes); + /** + * @since 5.66 + */ + void truncated(KIO::filesize_t _length); + /** * Only use this if you can't know in advance the size of the * copied data. For example, if you're doing variable bitrate @@ -1041,7 +1046,8 @@ enum VirtualFunctionId { AppConnectionMade = 0, - GetFileSystemFreeSpace = 1 // KF6 TODO: Turn into a virtual method + GetFileSystemFreeSpace = 1, // KF6 TODO: Turn into a virtual method + Truncate = 2, // KF6 TODO: Turn into a virtual method }; virtual void virtual_hook(int id, void *data); diff --git a/src/core/slavebase.cpp b/src/core/slavebase.cpp --- a/src/core/slavebase.cpp +++ b/src/core/slavebase.cpp @@ -653,6 +653,12 @@ send(INF_POSITION, data); } +void SlaveBase::truncated(KIO::filesize_t _length) +{ + KIO_DATA << KIO_FILESIZE_T(_length); + send(INF_TRUNCATED, data); +} + void SlaveBase::processedPercent(float /* percent */) { //qDebug() << "STUB"; @@ -1392,6 +1398,13 @@ seek(offset); break; } + case CMD_TRUNCATE: { + KIO::filesize_t length; + stream >> length; + void *data = static_cast(&length); + virtual_hook(Truncate, data); + break; + } case CMD_NONE: break; case CMD_CLOSE: @@ -1486,6 +1499,9 @@ case GetFileSystemFreeSpace: { error(ERR_UNSUPPORTED_ACTION, unsupportedActionErrorString(protocolName(), CMD_FILESYSTEMFREESPACE)); } break; + case Truncate: { + error(ERR_UNSUPPORTED_ACTION, unsupportedActionErrorString(protocolName(), CMD_TRUNCATE)); + } break; } } diff --git a/src/core/slaveinterface.h b/src/core/slaveinterface.h --- a/src/core/slaveinterface.h +++ b/src/core/slaveinterface.h @@ -61,7 +61,8 @@ INF_META_DATA, INF_NETWORK_STATUS, INF_MESSAGEBOX, - INF_POSITION + INF_POSITION, + INF_TRUNCATED // add new ones here once a release is done, to avoid breaking binary compatibility }; @@ -175,6 +176,7 @@ void processedSize(KIO::filesize_t); void redirection(const QUrl &); void position(KIO::filesize_t); + void truncated(KIO::filesize_t); void speed(unsigned long); void errorPage(); diff --git a/src/core/slaveinterface.cpp b/src/core/slaveinterface.cpp --- a/src/core/slaveinterface.cpp +++ b/src/core/slaveinterface.cpp @@ -227,6 +227,11 @@ emit position(pos); break; } + case INF_TRUNCATED: { + KIO::filesize_t length = readFilesize_t(stream); + emit truncated(length); + break; + } case INF_SPEED: stream >> ul; d->slave_calcs_speed = true; diff --git a/src/ioslaves/file/file.h b/src/ioslaves/file/file.h --- a/src/ioslaves/file/file.h +++ b/src/ioslaves/file/file.h @@ -72,6 +72,7 @@ void read(KIO::filesize_t size) override; void write(const QByteArray &data) override; void seek(KIO::filesize_t offset) override; + void truncate(KIO::filesize_t length); void close() override; /** diff --git a/src/ioslaves/file/file.cpp b/src/ioslaves/file/file.cpp --- a/src/ioslaves/file/file.cpp +++ b/src/ioslaves/file/file.cpp @@ -564,6 +564,18 @@ } } +void FileProtocol::truncate(KIO::filesize_t length) +{ + Q_ASSERT(mFile && mFile->isOpen()); + + if (mFile->resize(length)) { + truncated(length); + } else { + error(KIO::ERR_CANNOT_TRUNCATE, mFile->fileName()); + closeWithoutFinish(); + } +} + void FileProtocol::closeWithoutFinish() { Q_ASSERT(mFile); @@ -1547,6 +1559,10 @@ QUrl *url = static_cast(data); fileSystemFreeSpace(*url); } break; + case SlaveBase::Truncate: { + auto length = static_cast(data); + truncate(*length); + } break; default: { SlaveBase::virtual_hook(id, data); } break; diff --git a/src/ioslaves/file/file.json b/src/ioslaves/file/file.json --- a/src/ioslaves/file/file.json +++ b/src/ioslaves/file/file.json @@ -25,6 +25,7 @@ "maxInstances": 5, "moving": true, "opening": true, + "truncating": true, "output": "filesystem", "protocol": "file", "reading": true,