diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -32,6 +32,7 @@ udsentry_benchmark.cpp deletejobtest.cpp urlutiltest.cpp + batchrenamejobtest.cpp NAME_PREFIX "kiocore-" LINK_LIBRARIES KF5::KIOCore KF5::I18n Qt5::Test Qt5::Network ) diff --git a/autotests/batchrenamejobtest.cpp b/autotests/batchrenamejobtest.cpp new file mode 100644 --- /dev/null +++ b/autotests/batchrenamejobtest.cpp @@ -0,0 +1,147 @@ +/* This file is part of the KDE libraries + Copyright (C) 2017 by Chinmoy Ranjan Pradhan + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include +#include + +#include + +#include "kiotesthelper.h" + +class BatchRenameJobTest : public QObject +{ + Q_OBJECT + +private: + void createTestFiles(const QStringList &fileList) + { + foreach (const QString &filename, fileList) { + createTestFile(m_homeDir + filename); + } + } + + bool checkFileExistence(const QStringList &fileList) + { + foreach (const QString &filename, fileList) { + const QString filePath = m_homeDir + filename; + if (!QFile::exists(filePath)) { + return false; + } + } + return true; + } + + QList createUrlList(const QStringList &fileList) + { + QList srcList; + srcList.reserve(fileList.count()); + foreach (const QString &filename, fileList) { + const QString filePath = m_homeDir + filename; + srcList.append(QUrl::fromLocalFile(filePath)); + } + return srcList; + } + +private Q_SLOTS: + void initTestCase() + { + QStandardPaths::enableTestMode(true); + + // To avoid a runtime dependency on klauncher + qputenv("KDE_FORK_SLAVES", "yes"); + + cleanupTestCase(); + + // Create temporary home directory + m_homeDir = homeTmpDir(); + + } + + void cleanupTestCase() + { + QDir(homeTmpDir()).removeRecursively(); + } + + void batchRenameJobTest_data() + { + QTest::addColumn("oldFilenames"); + QTest::addColumn("baseName"); + QTest::addColumn("index"); + QTest::addColumn("indexPlaceholder"); + QTest::addColumn("newFilenames"); + + QTest::newRow("different-extensions-single-placeholder") << (QStringList() << "old_file_without_extension" + << "old_file.txt" + << "old_file.zip") + << "#-new_name" << 1 << QChar('#') + << (QStringList() << "1-new_name" + << "2-new_name.txt" + << "3-new_name.zip"); + + QTest::newRow("same-extensions-placeholder-sequence") << (QStringList() << "first_source.cpp" + << "second_source.cpp" + << "third_source.java") + << "new_source###" << 8 << QChar('#') + << (QStringList() << "new_source008.cpp" + << "new_source009.cpp" + << "new_source010.java"); + + QTest::newRow("different-extensions-invalid-placeholder") << (QStringList() << "audio.mp3" + << "video.mp4" + << "movie.mkv") + << "me#d#ia" << 0 << QChar('#') + << (QStringList() << "me#d#ia.mp3" + << "me#d#ia.mp4" + << "me#d#ia.mkv"); + + QTest::newRow("same-extensions-invalid-placeholder") << (QStringList() << "random_headerfile.h" + << "another_headerfile.h" + << "random_sourcefile.c") + << "##file#" << 4 << QChar('#') + << (QStringList() << "##file#4.h" + << "##file#5.h" + << "##file#6.c"); + + } + + void batchRenameJobTest() + { + QFETCH(QStringList, oldFilenames); + QFETCH(QString, baseName); + QFETCH(int, index); + QFETCH(QChar, indexPlaceholder); + QFETCH(QStringList, newFilenames); + createTestFiles(oldFilenames); + QVERIFY(checkFileExistence(oldFilenames)); + KIO::Job *job = KIO::batchRename(createUrlList(oldFilenames), baseName, index, indexPlaceholder); + job->setUiDelegate(nullptr); + QSignalSpy spy(job, SIGNAL(fileRenamed(QUrl, QUrl))); + QVERIFY2(job->exec(), qPrintable(job->errorString())); + QCOMPARE(spy.count(), oldFilenames.count()); + QVERIFY(!checkFileExistence(oldFilenames)); + QVERIFY(checkFileExistence(newFilenames)); + } + +private: + QString m_homeDir; +}; + +QTEST_MAIN(BatchRenameJobTest) + +#include "batchrenamejobtest.moc" diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -93,6 +93,7 @@ kiocoredebug.cpp kioglobal_p.cpp + batchrenamejob.cpp ) if (UNIX) @@ -212,6 +213,7 @@ DavJob DesktopExecParser FileSystemFreeSpaceJob + BatchRenameJob PREFIX KIO REQUIRED_HEADERS KIO_namespaced_HEADERS diff --git a/src/core/batchrenamejob.h b/src/core/batchrenamejob.h new file mode 100644 --- /dev/null +++ b/src/core/batchrenamejob.h @@ -0,0 +1,89 @@ +/* This file is part of the KDE libraries + Copyright (C) 2017 by Chinmoy Ranjan Pradhan + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef BATCHRENAMEJOB_H +#define BATCHRENAMEJOB_H + +#include "kiocore_export.h" +#include "job_base.h" + +namespace KIO +{ + +class BatchRenameJobPrivate; + +/** + * @class KIO::BatchRenameJob batchrenamejob.h + * + * A KIO job that renames multiple files in one go. + * + * @since 5.42 + */ +class KIOCORE_EXPORT BatchRenameJob : public Job +{ + Q_OBJECT + +public: + virtual ~BatchRenameJob(); + +Q_SIGNALS: + /** + * Signals that a file was renamed. + */ + void fileRenamed(const QUrl &oldUrl, const QUrl &newUrl); + +protected Q_SLOTS: + void slotResult(KJob *job) Q_DECL_OVERRIDE; + +protected: + /// @internal + BatchRenameJob(BatchRenameJobPrivate &dd); + +private: + Q_PRIVATE_SLOT(d_func(), void slotStart()) + Q_DECLARE_PRIVATE(BatchRenameJob) +}; + +/** + * Renames multiple files at once. + * + * The new filename is obtained by replacing the characters represented by + * @p placeHolder by the index @p index. + * E.g. Calling batchRename({"file:///Test.jpg"}, "Test #" 12, '#') renames + * the file to "Test 12.jpg". A connected sequence of placeholders results in + * leading zeros. batchRename({"file:///Test.jpg"}, "Test ####" 12, '#') renames + * the file to "Test 0012.jpg". And if no placeholder is there then @p index is + * appended to @p newName. Calling batchRename({"file:///Test.jpg"}, "NewTest" 12, '#') + * renames the file to "NewTest12.jpg". + * + * @param src The list of items to rename. + * @param newName The base name to use in all new filenames. + * @param index The integer(incremented after renaming a file) to add to the base name. + * @param placeHolder The character(s) which @p index will replace. + * + * @return A pointer to the job handling the operation. + * @since 5.42 + */ +KIOCORE_EXPORT BatchRenameJob *batchRename(const QList &src, const QString &newName, + int index, QChar placeHolder, + JobFlags flags = DefaultFlags); + +} + +#endif diff --git a/src/core/batchrenamejob.cpp b/src/core/batchrenamejob.cpp new file mode 100644 --- /dev/null +++ b/src/core/batchrenamejob.cpp @@ -0,0 +1,209 @@ +/* This file is part of the KDE libraries + Copyright (C) 2017 by Chinmoy Ranjan Pradhan + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "batchrenamejob.h" + +#include "job_p.h" +#include "copyjob.h" + +#include +#include +#include +#include + +using namespace KIO; + +class KIO::BatchRenameJobPrivate : public KIO::JobPrivate +{ +public: + BatchRenameJobPrivate(const QList &src, const QString &newName, + int index, QChar placeHolder, JobFlags flags) + : JobPrivate(), + m_srcList(src), + m_newName(newName), + m_index(index), + m_placeHolder(placeHolder), + m_listIterator(m_srcList.constBegin()), + m_allExtensionsDifferent(true), + m_useIndex(true), + m_appendIndex(false), + m_flags(flags) + { + // There occur four cases when renaming multiple files, + // 1. All files have different extension and $newName contains a valid placeholder. + // 2. At least two files have same extension and $newName contains a valid placeholder. + // In these two cases the placeholder character will be replaced by an integer($index). + // 3. All files have different extension and new name contains an invalid placeholder + // (this means either $newName doesn't contain the placeholder or the placeholders + // are not in a connected sequence). + // In this case nothing is substituted and all files have the same $newName. + // 4. At least two files have same extension and $newName contains an invalid placeholder. + // In this case $index is appended to $newName. + + + // Check for extensions. + QSet extensions; + QMimeDatabase db; + foreach (const QUrl &url, m_srcList) { + const QString extension = db.suffixForFileName(url.toDisplayString().toLower()); + if (extensions.contains(extension)) { + m_allExtensionsDifferent = false; + break; + } + + extensions.insert(extension); + } + + // Check for exactly one placeholder character or exactly one sequence of placeholders. + int pos = newName.indexOf(placeHolder); + if (pos != -1) { + while (pos < newName.size() && newName.at(pos) == placeHolder) { + pos++; + } + } + const bool validPlaceholder = (newName.indexOf(placeHolder, pos) == -1); + + if (!validPlaceholder) { + if (!m_allExtensionsDifferent) { + m_appendIndex = true; + } else { + m_useIndex = false; + } + } + } + + QList m_srcList; + QString m_newName; + int m_index; + QChar m_placeHolder; + QList::const_iterator m_listIterator; + bool m_allExtensionsDifferent; + bool m_useIndex; + bool m_appendIndex; + QUrl m_newUrl; // for fileRenamed signal + const JobFlags m_flags; + + Q_DECLARE_PUBLIC(BatchRenameJob) + + void slotStart(); + + QString indexedName(const QString& name, int index, QChar placeHolder) const; + + static inline BatchRenameJob *newJob(const QList &src, const QString &newName, + int index, QChar placeHolder, JobFlags flags) + { + BatchRenameJob *job = new BatchRenameJob(*new BatchRenameJobPrivate(src, newName, index, placeHolder, flags)); + job->setUiDelegate(KIO::createDefaultJobUiDelegate()); + if (!(flags & HideProgressInfo)) { + KIO::getJobTracker()->registerJob(job); + } + return job; + } + +}; + +BatchRenameJob::BatchRenameJob(BatchRenameJobPrivate &dd) + : Job(dd) +{ + QTimer::singleShot(0, this, SLOT(slotStart())); +} + +BatchRenameJob::~BatchRenameJob() +{ +} + +QString BatchRenameJobPrivate::indexedName(const QString& name, int index, QChar placeHolder) const +{ + if (!m_useIndex) { + return name; + } + + QString newName = name; + QString indexString = QString::number(index); + + if (m_appendIndex) { + newName.append(indexString); + return newName; + } + + // Insert leading zeros if necessary + const int minIndexLength = name.count(placeHolder); + indexString.prepend(QString(minIndexLength - indexString.length(), QLatin1Char('0'))); + + // Replace the index placeholders by the indexString + const int placeHolderStart = newName.indexOf(placeHolder); + newName.replace(placeHolderStart, minIndexLength, indexString); + + return newName; +} + +void BatchRenameJobPrivate::slotStart() +{ + Q_Q(BatchRenameJob); + + if (m_listIterator == m_srcList.constBegin()) { // emit total + q->setTotalAmount(KJob::Files, m_srcList.count()); + } + + if (m_listIterator != m_srcList.constEnd()) { + QString newName = indexedName(m_newName, m_index, m_placeHolder); + const QUrl oldUrl = *m_listIterator; + QMimeDatabase db; + const QString extension = db.suffixForFileName(oldUrl.path().toLower()); + if (!extension.isEmpty()) { + newName.append(QLatin1Char('.')); + newName.append(extension); + } + + m_newUrl = oldUrl.adjusted(QUrl::RemoveFilename); + m_newUrl.setPath(m_newUrl.path() + KIO::encodeFileName(newName)); + + KIO::Job * job = KIO::moveAs(oldUrl, m_newUrl, KIO::HideProgressInfo); + q->addSubjob(job); + q->setProcessedAmount(KJob::Files, q->processedAmount(KJob::Files) + 1); + } else { + q->emitResult(); + } +} + +void BatchRenameJob::slotResult(KJob *job) +{ + Q_D(BatchRenameJob); + if (job->error()) { + KIO::Job::slotResult(job); + return; + } + + removeSubjob(job); + + emit fileRenamed(*d->m_listIterator, d->m_newUrl); + ++d->m_listIterator; + ++d->m_index; + emitPercent(d->m_listIterator - d->m_srcList.constBegin(), d->m_srcList.count()); + d->slotStart(); +} + +BatchRenameJob * KIO::batchRename(const QList &src, const QString &newName, + int index, QChar placeHolder, KIO::JobFlags flags) +{ + return BatchRenameJobPrivate::newJob(src, newName, index, placeHolder, flags); +} + + +#include "moc_batchrenamejob.cpp"