1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2017 Chinmoy Ranjan Pradhan <chinmoyrp65@gmail.com>
4
5 SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6*/
7
8#include "batchrenamejob.h"
9
10#include "copyjob.h"
11#include "job_p.h"
12
13#include <QMimeDatabase>
14#include <QRegularExpression>
15#include <QTimer>
16
17#include <KLocalizedString>
18
19#include <set>
20
21using namespace KIO;
22
23class KIO::BatchRenameJobPrivate : public KIO::JobPrivate
24{
25public:
26 BatchRenameJobPrivate(const QList<QUrl> &src, const renameFunctionType renamefunction, JobFlags flags)
27 : JobPrivate()
28 , m_srcList(src)
29 , m_renamefunction(renamefunction)
30 , m_listIterator(m_srcList.constBegin())
31 , m_flags(flags)
32 {
33 // There occur four cases when renaming multiple files,
34 // 1. All files have different extension and $newName contains a valid placeholder.
35 // 2. At least two files have same extension and $newName contains a valid placeholder.
36 // In these two cases the placeholder character will be replaced by an integer($index).
37 // 3. All files have different extension and new name contains an invalid placeholder
38 // (this means either $newName doesn't contain the placeholder or the placeholders
39 // are not in a connected sequence).
40 // In this case nothing is substituted and all files have the same $newName.
41 // 4. At least two files have same extension and $newName contains an invalid placeholder.
42 // In this case $index is appended to $newName.
43 }
44
45 QList<QUrl> m_srcList;
46 const renameFunctionType m_renamefunction;
47 QList<QUrl>::const_iterator m_listIterator;
48 QUrl m_oldUrl;
49 QUrl m_newUrl; // for fileRenamed signal
50 const JobFlags m_flags;
51 QTimer m_reportTimer;
52
53 Q_DECLARE_PUBLIC(BatchRenameJob)
54
55 void slotStart();
56 void slotReport();
57
58 static inline BatchRenameJob *newJob(const QList<QUrl> &src, const renameFunctionType renamefunction, JobFlags flags)
59 {
60 BatchRenameJob *job = new BatchRenameJob(*new BatchRenameJobPrivate(src, renamefunction, flags));
61 job->setUiDelegate(KIO::createDefaultJobUiDelegate());
62 if (!(flags & HideProgressInfo)) {
63 KIO::getJobTracker()->registerJob(job);
64 }
65 if (!(flags & NoPrivilegeExecution)) {
66 job->d_func()->m_privilegeExecutionEnabled = true;
67 job->d_func()->m_operationType = Rename;
68 }
69 return job;
70 }
71};
72
73BatchRenameJob::BatchRenameJob(BatchRenameJobPrivate &dd)
74 : Job(dd)
75{
76 Q_D(BatchRenameJob);
77 connect(sender: &d->m_reportTimer, signal: &QTimer::timeout, context: this, slot: [this]() {
78 d_func()->slotReport();
79 });
80 d->m_reportTimer.start(msec: 200);
81
82 QTimer::singleShot(interval: 0, receiver: this, slot: [this] {
83 d_func()->slotStart();
84 });
85}
86
87BatchRenameJob::~BatchRenameJob()
88{
89}
90
91void BatchRenameJobPrivate::slotStart()
92{
93 Q_Q(BatchRenameJob);
94
95 if (m_listIterator == m_srcList.constBegin()) { // emit total
96 q->setTotalAmount(unit: KJob::Items, amount: m_srcList.count());
97 }
98
99 if (m_listIterator == m_srcList.constEnd()) {
100 m_reportTimer.stop();
101 slotReport();
102 q->emitResult();
103 return;
104 }
105
106 QMimeDatabase db;
107 const QUrl oldUrl = *m_listIterator;
108 const QString oldFileName = oldUrl.fileName();
109 const QString extension = db.suffixForFileName(fileName: oldFileName);
110 int lastPoint = oldFileName.lastIndexOf(c: QLatin1Char('.'));
111 QString fileNameNoExt = oldFileName.left(n: lastPoint);
112
113 QString newName = m_renamefunction(fileNameNoExt);
114
115 const QString suffix = QLatin1Char('.') + extension;
116 if (!extension.isEmpty() && !newName.endsWith(s: suffix)) {
117 newName += suffix;
118 }
119
120 m_oldUrl = oldUrl;
121 m_newUrl = oldUrl.adjusted(options: QUrl::RemoveFilename);
122 m_newUrl.setPath(path: m_newUrl.path() + KIO::encodeFileName(str: newName));
123
124 if (m_newUrl == m_oldUrl) {
125 // skip
126
127 // We still must emit fileRenamed so users have
128 // the corresponding number of files in the output
129 Q_EMIT q->fileRenamed(oldUrl: *m_listIterator, newUrl: m_newUrl);
130
131 ++m_listIterator;
132 slotStart();
133 return;
134 }
135
136 KIO::Job *job = KIO::moveAs(src: oldUrl, dest: m_newUrl, flags: KIO::HideProgressInfo);
137 job->setParentJob(q);
138 q->addSubjob(job);
139}
140
141void BatchRenameJobPrivate::slotReport()
142{
143 Q_Q(BatchRenameJob);
144
145 const auto processed = m_listIterator - m_srcList.constBegin();
146
147 q->setProcessedAmount(unit: KJob::Items, amount: processed);
148 q->emitPercent(processedAmount: processed, totalAmount: m_srcList.count());
149
150 emitRenaming(q, src: m_oldUrl, dest: m_newUrl);
151}
152
153void BatchRenameJob::slotResult(KJob *job)
154{
155 Q_D(BatchRenameJob);
156 if (job->error()) {
157 d->m_reportTimer.stop();
158 d->slotReport();
159 KIO::Job::slotResult(job);
160 return;
161 }
162
163 removeSubjob(job);
164
165 Q_EMIT fileRenamed(oldUrl: *d->m_listIterator, newUrl: d->m_newUrl);
166 ++d->m_listIterator;
167 d->slotStart();
168}
169
170BatchRenameJob *KIO::batchRename(const QList<QUrl> &srcList, const QString &newName, int startIndex, QChar placeHolder, KIO::JobFlags flags)
171{
172 bool allExtensionsDifferent = true;
173 // Check for extensions.
174 std::set<QString> extensions;
175 QMimeDatabase db;
176 for (const QUrl &url : std::as_const(t: srcList)) {
177 const QString extension = db.suffixForFileName(fileName: url.path());
178 const auto [it, isInserted] = extensions.insert(x: extension);
179 if (!isInserted) {
180 allExtensionsDifferent = false;
181 break;
182 }
183 }
184
185 // look for consecutive # groups
186 static const QRegularExpression regex(QStringLiteral("%1+").arg(a: placeHolder));
187
188 auto matchDashes = regex.globalMatch(subject: newName);
189 QRegularExpressionMatch lastMatchDashes;
190 int matchCount = 0;
191 while (matchDashes.hasNext()) {
192 lastMatchDashes = matchDashes.next();
193 matchCount++;
194 }
195
196 bool validPlaceholder = matchCount == 1;
197
198 int placeHolderStart = lastMatchDashes.capturedStart(nth: 0);
199 int placeHolderLength = lastMatchDashes.capturedLength(nth: 0);
200
201 QString pattern(newName);
202
203 if (!validPlaceholder) {
204 if (allExtensionsDifferent) {
205 // pattern: my-file
206 // in: file-a.txt file-b.md
207 } else {
208 // pattern: my-file
209 // in: file-a.txt file-b.txt
210 // effective pattern: my-file#
211 placeHolderLength = 1;
212 placeHolderStart = pattern.length();
213 pattern.append(c: placeHolder);
214 }
215 }
216
217 renameFunctionType function =
218 [pattern, allExtensionsDifferent, validPlaceholder, placeHolderStart, placeHolderLength, index = startIndex](const QStringView view) mutable {
219 Q_UNUSED(view);
220
221 QString indexString = QString::number(index);
222
223 if (!validPlaceholder) {
224 if (allExtensionsDifferent) {
225 // pattern: my-file
226 // in: file-a.txt file-b.md
227 return pattern;
228 }
229 }
230
231 // Insert leading zeros if necessary
232 indexString = indexString.prepend(s: QString(placeHolderLength - indexString.length(), QLatin1Char('0')));
233
234 ++index;
235
236 return QString(pattern).replace(i: placeHolderStart, len: placeHolderLength, after: indexString);
237 };
238
239 return BatchRenameJobPrivate::newJob(src: srcList, renamefunction: std::move(function), flags);
240};
241
242BatchRenameJob *KIO::batchRenameWithFunction(const QList<QUrl> &srcList, const renameFunctionType renameFunction, KIO::JobFlags flags)
243{
244 return BatchRenameJobPrivate::newJob(src: srcList, renamefunction: renameFunction, flags);
245}
246
247#include "moc_batchrenamejob.cpp"
248

source code of kio/src/core/batchrenamejob.cpp