1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2000 Stephan Kulow <coolo@kde.org>
4 SPDX-FileCopyrightText: 2000-2009 David Faure <faure@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-or-later
7*/
8
9#include "filecopyjob.h"
10#include "askuseractioninterface.h"
11#include "job_p.h"
12#include "kprotocolmanager.h"
13#include "scheduler.h"
14#include "worker_p.h"
15#include <kio/jobuidelegatefactory.h>
16
17#include <KLocalizedString>
18
19#include <QFile>
20#include <QTimer>
21
22using namespace KIO;
23
24static inline Worker *jobWorker(SimpleJob *job)
25{
26 return SimpleJobPrivate::get(job)->m_worker;
27}
28
29class KIO::FileCopyJobPrivate : public KIO::JobPrivate
30{
31public:
32 FileCopyJobPrivate(const QUrl &src, const QUrl &dest, int permissions, bool move, JobFlags flags)
33 : m_sourceSize(filesize_t(-1))
34 , m_src(src)
35 , m_dest(dest)
36 , m_moveJob(nullptr)
37 , m_copyJob(nullptr)
38 , m_delJob(nullptr)
39 , m_chmodJob(nullptr)
40 , m_getJob(nullptr)
41 , m_putJob(nullptr)
42 , m_permissions(permissions)
43 , m_move(move)
44 , m_mustChmod(0)
45 , m_bFileCopyInProgress(false)
46 , m_flags(flags)
47 {
48 }
49 KIO::filesize_t m_sourceSize;
50 QDateTime m_modificationTime;
51 QUrl m_src;
52 QUrl m_dest;
53 QByteArray m_buffer;
54 SimpleJob *m_moveJob;
55 SimpleJob *m_copyJob;
56 SimpleJob *m_delJob;
57 SimpleJob *m_chmodJob;
58 TransferJob *m_getJob;
59 TransferJob *m_putJob;
60 int m_permissions;
61 bool m_move : 1;
62 bool m_canResume : 1;
63 bool m_resumeAnswerSent : 1;
64 bool m_mustChmod : 1;
65 bool m_bFileCopyInProgress : 1;
66 JobFlags m_flags;
67
68 void startBestCopyMethod();
69 void startCopyJob();
70 void startCopyJob(const QUrl &workerUrl);
71 void startRenameJob(const QUrl &workerUrl);
72 void startDataPump();
73 void connectSubjob(SimpleJob *job);
74
75 void slotStart();
76 void slotData(KIO::Job *, const QByteArray &data);
77 void slotDataReq(KIO::Job *, QByteArray &data);
78 void slotMimetype(KIO::Job *, const QString &type);
79 /*
80 * Forward signal from subjob
81 * job the job that emitted this signal
82 * offset the offset to resume from
83 */
84 void slotCanResume(KIO::Job *job, KIO::filesize_t offset);
85 void processCanResumeResult(KIO::Job *job, RenameDialog_Result result, KIO::filesize_t offset);
86
87 Q_DECLARE_PUBLIC(FileCopyJob)
88
89 static inline FileCopyJob *newJob(const QUrl &src, const QUrl &dest, int permissions, bool move, JobFlags flags)
90 {
91 // qDebug() << src << "->" << dest;
92 FileCopyJob *job = new FileCopyJob(*new FileCopyJobPrivate(src, dest, permissions, move, flags));
93 job->setProperty(name: "destUrl", value: dest.toString());
94 job->setUiDelegate(KIO::createDefaultJobUiDelegate());
95 if (!(flags & HideProgressInfo)) {
96 KIO::getJobTracker()->registerJob(job);
97 }
98 if (!(flags & NoPrivilegeExecution)) {
99 job->d_func()->m_privilegeExecutionEnabled = true;
100 job->d_func()->m_operationType = move ? Move : Copy;
101 }
102 return job;
103 }
104};
105
106static bool isSrcDestSameWorkerProcess(const QUrl &src, const QUrl &dest)
107{
108 /* clang-format off */
109 return src.scheme() == dest.scheme()
110 && src.host() == dest.host()
111 && src.port() == dest.port()
112 && src.userName() == dest.userName()
113 && src.password() == dest.password();
114 /* clang-format on */
115}
116
117/*
118 * The FileCopyJob works according to the famous Bavarian
119 * 'Alternating Bitburger Protocol': we either drink a beer or we
120 * we order a beer, but never both at the same time.
121 * Translated to KIO workers: We alternate between receiving a block of data
122 * and sending it away.
123 */
124FileCopyJob::FileCopyJob(FileCopyJobPrivate &dd)
125 : Job(dd)
126{
127 Q_D(FileCopyJob);
128 QTimer::singleShot(interval: 0, receiver: this, slot: [d]() {
129 d->slotStart();
130 });
131}
132
133void FileCopyJobPrivate::slotStart()
134{
135 Q_Q(FileCopyJob);
136 if (!m_move) {
137 JobPrivate::emitCopying(q, src: m_src, dest: m_dest);
138 } else {
139 JobPrivate::emitMoving(q, src: m_src, dest: m_dest);
140 }
141
142 if (m_move) {
143 // The if() below must be the same as the one in startBestCopyMethod
144 if (isSrcDestSameWorkerProcess(src: m_src, dest: m_dest)) {
145 startRenameJob(workerUrl: m_src);
146 return;
147 } else if (m_src.isLocalFile() && KProtocolManager::canRenameFromFile(url: m_dest)) {
148 startRenameJob(workerUrl: m_dest);
149 return;
150 } else if (m_dest.isLocalFile() && KProtocolManager::canRenameToFile(url: m_src)) {
151 startRenameJob(workerUrl: m_src);
152 return;
153 }
154 // No fast-move available, use copy + del.
155 }
156 startBestCopyMethod();
157}
158
159void FileCopyJobPrivate::startBestCopyMethod()
160{
161 if (isSrcDestSameWorkerProcess(src: m_src, dest: m_dest)) {
162 startCopyJob();
163 } else if (m_src.isLocalFile() && KProtocolManager::canCopyFromFile(url: m_dest)) {
164 startCopyJob(workerUrl: m_dest);
165 } else if (m_dest.isLocalFile() && KProtocolManager::canCopyToFile(url: m_src) && !KIO::Scheduler::isWorkerOnHoldFor(url: m_src)) {
166 startCopyJob(workerUrl: m_src);
167 } else {
168 startDataPump();
169 }
170}
171
172FileCopyJob::~FileCopyJob()
173{
174}
175
176void FileCopyJob::setSourceSize(KIO::filesize_t size)
177{
178 Q_D(FileCopyJob);
179 d->m_sourceSize = size;
180 if (size != (KIO::filesize_t)-1) {
181 setTotalAmount(unit: KJob::Bytes, amount: size);
182 }
183}
184
185void FileCopyJob::setModificationTime(const QDateTime &mtime)
186{
187 Q_D(FileCopyJob);
188 d->m_modificationTime = mtime;
189}
190
191QUrl FileCopyJob::srcUrl() const
192{
193 return d_func()->m_src;
194}
195
196QUrl FileCopyJob::destUrl() const
197{
198 return d_func()->m_dest;
199}
200
201void FileCopyJobPrivate::startCopyJob()
202{
203 startCopyJob(workerUrl: m_src);
204}
205
206void FileCopyJobPrivate::startCopyJob(const QUrl &workerUrl)
207{
208 Q_Q(FileCopyJob);
209 // qDebug();
210 KIO_ARGS << m_src << m_dest << m_permissions << (qint8)(m_flags & Overwrite);
211 auto job = new DirectCopyJob(workerUrl, packedArgs);
212 m_copyJob = job;
213 m_copyJob->setParentJob(q);
214 if (m_modificationTime.isValid()) {
215 m_copyJob->addMetaData(QStringLiteral("modified"), value: m_modificationTime.toString(format: Qt::ISODate)); // #55804
216 }
217 q->addSubjob(job: m_copyJob);
218 connectSubjob(job: m_copyJob);
219 q->connect(sender: job, signal: &DirectCopyJob::canResume, context: q, slot: [this](KIO::Job *job, KIO::filesize_t offset) {
220 slotCanResume(job, offset);
221 });
222}
223
224void FileCopyJobPrivate::startRenameJob(const QUrl &workerUrl)
225{
226 Q_Q(FileCopyJob);
227 m_mustChmod = true; // CMD_RENAME by itself doesn't change permissions
228 KIO_ARGS << m_src << m_dest << (qint8)(m_flags & Overwrite);
229 m_moveJob = SimpleJobPrivate::newJobNoUi(url: workerUrl, command: CMD_RENAME, packedArgs);
230 m_moveJob->setParentJob(q);
231 if (m_modificationTime.isValid()) {
232 m_moveJob->addMetaData(QStringLiteral("modified"), value: m_modificationTime.toString(format: Qt::ISODate)); // #55804
233 }
234 q->addSubjob(job: m_moveJob);
235 connectSubjob(job: m_moveJob);
236}
237
238void FileCopyJobPrivate::connectSubjob(SimpleJob *job)
239{
240 Q_Q(FileCopyJob);
241 q->connect(sender: job, signal: &KJob::totalSize, context: q, slot: [q](KJob *job, qulonglong totalSize) {
242 Q_UNUSED(job);
243 if (totalSize != q->totalAmount(unit: KJob::Bytes)) {
244 q->setTotalAmount(unit: KJob::Bytes, amount: totalSize);
245 }
246 });
247
248 q->connect(sender: job, signal: &KJob::processedSize, context: q, slot: [q, this](const KJob *job, qulonglong processedSize) {
249 if (job == m_copyJob) {
250 m_bFileCopyInProgress = processedSize > 0;
251 }
252 q->setProcessedAmount(unit: KJob::Bytes, amount: processedSize);
253 });
254
255 q->connect(sender: job, signal: &KJob::percentChanged, context: q, slot: [q](KJob *, ulong percent) {
256 if (percent > q->percent()) {
257 q->setPercent(percent);
258 }
259 });
260
261 if (q->isSuspended()) {
262 job->suspend();
263 }
264}
265
266bool FileCopyJob::doSuspend()
267{
268 Q_D(FileCopyJob);
269 if (d->m_moveJob) {
270 d->m_moveJob->suspend();
271 }
272
273 if (d->m_copyJob) {
274 d->m_copyJob->suspend();
275 }
276
277 if (d->m_getJob) {
278 d->m_getJob->suspend();
279 }
280
281 if (d->m_putJob) {
282 d->m_putJob->suspend();
283 }
284
285 Job::doSuspend();
286 return true;
287}
288
289bool FileCopyJob::doResume()
290{
291 Q_D(FileCopyJob);
292 if (d->m_moveJob) {
293 d->m_moveJob->resume();
294 }
295
296 if (d->m_copyJob) {
297 d->m_copyJob->resume();
298 }
299
300 if (d->m_getJob) {
301 d->m_getJob->resume();
302 }
303
304 if (d->m_putJob) {
305 d->m_putJob->resume();
306 }
307
308 Job::doResume();
309 return true;
310}
311
312void FileCopyJobPrivate::startDataPump()
313{
314 Q_Q(FileCopyJob);
315 // qDebug();
316
317 m_canResume = false;
318 m_resumeAnswerSent = false;
319 m_getJob = nullptr; // for now
320 m_putJob = put(url: m_dest, permissions: m_permissions, flags: (m_flags | HideProgressInfo) /* no GUI */);
321 m_putJob->setParentJob(q);
322 // qDebug() << "m_putJob=" << m_putJob << "m_dest=" << m_dest;
323 if (m_modificationTime.isValid()) {
324 m_putJob->setModificationTime(m_modificationTime);
325 }
326
327 // The first thing the put job will tell us is whether we can
328 // resume or not (this is always emitted)
329 q->connect(sender: m_putJob, signal: &KIO::TransferJob::canResume, context: q, slot: [this](KIO::Job *job, KIO::filesize_t offset) {
330 slotCanResume(job, offset);
331 });
332 q->connect(sender: m_putJob, signal: &KIO::TransferJob::dataReq, context: q, slot: [this](KIO::Job *job, QByteArray &data) {
333 slotDataReq(job, data);
334 });
335 q->addSubjob(job: m_putJob);
336}
337
338void FileCopyJobPrivate::slotCanResume(KIO::Job *job, KIO::filesize_t offset)
339{
340 Q_Q(FileCopyJob);
341
342 if (job == m_getJob) {
343 // Cool, the get job said ok, we can resume
344 m_canResume = true;
345 // qDebug() << "'can resume' from the GET job -> we can resume";
346
347 jobWorker(job: m_getJob)->setOffset(jobWorker(job: m_putJob)->offset());
348 return;
349 }
350
351 if (job == m_putJob || job == m_copyJob) {
352 // qDebug() << "'can resume' from PUT job. offset=" << KIO::number(offset);
353 if (offset == 0) {
354 m_resumeAnswerSent = true; // No need for an answer
355 } else {
356 KIO::Job *kioJob = q->parentJob() ? q->parentJob() : q;
357 auto *askUserActionInterface = KIO::delegateExtension<KIO::AskUserActionInterface *>(job: kioJob);
358 if (!KProtocolManager::autoResume() && !(m_flags & Overwrite) && askUserActionInterface) {
359 auto renameSignal = &AskUserActionInterface::askUserRenameResult;
360
361 q->connect(sender: askUserActionInterface, signal: renameSignal, context: q, slot: [=, this](KIO::RenameDialog_Result result, const QUrl &, const KJob *askJob) {
362 Q_ASSERT(kioJob == askJob);
363
364 // Only receive askUserRenameResult once per rename dialog
365 QObject::disconnect(sender: askUserActionInterface, signal: renameSignal, receiver: q, zero: nullptr);
366
367 processCanResumeResult(job, result, offset);
368 });
369
370 // Ask confirmation about resuming previous transfer
371 askUserActionInterface->askUserRename(job: kioJob,
372 i18n("File Already Exists"),
373 src: m_src,
374 dest: m_dest,
375 options: RenameDialog_Options(RenameDialog_Overwrite | RenameDialog_Resume | RenameDialog_NoRename),
376 sizeSrc: m_sourceSize,
377 sizeDest: offset);
378 return;
379 }
380 }
381
382 processCanResumeResult(job, //
383 result: Result_Resume, // The default is to resume
384 offset);
385
386 return;
387 }
388
389 qCWarning(KIO_CORE) << "unknown job=" << job << "m_getJob=" << m_getJob << "m_putJob=" << m_putJob;
390}
391
392void FileCopyJobPrivate::processCanResumeResult(KIO::Job *job, RenameDialog_Result result, KIO::filesize_t offset)
393{
394 Q_Q(FileCopyJob);
395 if (result == Result_Overwrite || (m_flags & Overwrite)) {
396 offset = 0;
397 } else if (result == Result_Cancel) {
398 if (job == m_putJob) {
399 m_putJob->kill(verbosity: FileCopyJob::Quietly);
400 q->removeSubjob(job: m_putJob);
401 m_putJob = nullptr;
402 } else {
403 m_copyJob->kill(verbosity: FileCopyJob::Quietly);
404 q->removeSubjob(job: m_copyJob);
405 m_copyJob = nullptr;
406 }
407 q->setError(ERR_USER_CANCELED);
408 q->emitResult();
409 return;
410 }
411
412 if (job == m_copyJob) {
413 jobWorker(job: m_copyJob)->sendResumeAnswer(resume: offset != 0);
414 return;
415 }
416
417 if (job == m_putJob) {
418 m_getJob = KIO::get(url: m_src, reload: NoReload, flags: HideProgressInfo /* no GUI */);
419 m_getJob->setParentJob(q);
420 // qDebug() << "m_getJob=" << m_getJob << m_src;
421 m_getJob->addMetaData(QStringLiteral("AllowCompressedPage"), QStringLiteral("false"));
422 // Set size in subjob. This helps if the worker doesn't emit totalSize.
423 if (m_sourceSize != (KIO::filesize_t)-1) {
424 m_getJob->setTotalAmount(unit: KJob::Bytes, amount: m_sourceSize);
425 }
426
427 if (offset) {
428 // qDebug() << "Setting metadata for resume to" << (unsigned long) offset;
429 m_getJob->addMetaData(QStringLiteral("range-start"), value: KIO::number(size: offset));
430
431 // Might or might not get emitted
432 q->connect(sender: m_getJob, signal: &KIO::TransferJob::canResume, context: q, slot: [this](KIO::Job *job, KIO::filesize_t offset) {
433 slotCanResume(job, offset);
434 });
435 }
436 jobWorker(job: m_putJob)->setOffset(offset);
437
438 m_putJob->d_func()->internalSuspend();
439 q->addSubjob(job: m_getJob);
440 connectSubjob(job: m_getJob); // Progress info depends on get
441 m_getJob->d_func()->internalResume(); // Order a beer
442
443 q->connect(sender: m_getJob, signal: &KIO::TransferJob::data, context: q, slot: [this](KIO::Job *job, const QByteArray &data) {
444 slotData(job, data);
445 });
446 q->connect(sender: m_getJob, signal: &KIO::TransferJob::mimeTypeFound, context: q, slot: [this](KIO::Job *job, const QString &type) {
447 slotMimetype(job, type);
448 });
449 }
450}
451
452void FileCopyJobPrivate::slotData(KIO::Job *, const QByteArray &data)
453{
454 // qDebug() << "data size:" << data.size();
455 Q_ASSERT(m_putJob);
456 if (!m_putJob) {
457 return; // Don't crash
458 }
459 m_getJob->d_func()->internalSuspend();
460 m_putJob->d_func()->internalResume(); // Drink the beer
461 m_buffer += data;
462
463 // On the first set of data incoming, we tell the "put" worker about our
464 // decision about resuming
465 if (!m_resumeAnswerSent) {
466 m_resumeAnswerSent = true;
467 // qDebug() << "(first time) -> send resume answer " << m_canResume;
468 jobWorker(job: m_putJob)->sendResumeAnswer(resume: m_canResume);
469 }
470}
471
472void FileCopyJobPrivate::slotDataReq(KIO::Job *, QByteArray &data)
473{
474 Q_Q(FileCopyJob);
475 // qDebug();
476 if (!m_resumeAnswerSent && !m_getJob) {
477 // This can't happen
478 q->setError(ERR_INTERNAL);
479 q->setErrorText(QStringLiteral("'Put' job did not send canResume or 'Get' job did not send data!"));
480 m_putJob->kill(verbosity: FileCopyJob::Quietly);
481 q->removeSubjob(job: m_putJob);
482 m_putJob = nullptr;
483 q->emitResult();
484 return;
485 }
486 if (m_getJob) {
487 m_getJob->d_func()->internalResume(); // Order more beer
488 m_putJob->d_func()->internalSuspend();
489 }
490 data = m_buffer;
491 m_buffer = QByteArray();
492}
493
494void FileCopyJobPrivate::slotMimetype(KIO::Job *, const QString &type)
495{
496 Q_Q(FileCopyJob);
497 Q_EMIT q->mimeTypeFound(job: q, mimeType: type);
498}
499
500void FileCopyJob::slotResult(KJob *job)
501{
502 Q_D(FileCopyJob);
503 // qDebug() << "this=" << this << "job=" << job;
504 removeSubjob(job);
505
506 // If result comes from copyjob then we are not writing anymore.
507 if (job == d->m_copyJob) {
508 d->m_bFileCopyInProgress = false;
509 }
510
511 // Did job have an error ?
512 if (job->error()) {
513 if ((job == d->m_moveJob) && (job->error() == ERR_UNSUPPORTED_ACTION)) {
514 d->m_moveJob = nullptr;
515 d->startBestCopyMethod();
516 return;
517 } else if ((job == d->m_copyJob) && (job->error() == ERR_UNSUPPORTED_ACTION)) {
518 d->m_copyJob = nullptr;
519 d->startDataPump();
520 return;
521 } else if (job == d->m_getJob) {
522 d->m_getJob = nullptr;
523 if (d->m_putJob) {
524 d->m_putJob->kill(verbosity: Quietly);
525 removeSubjob(job: d->m_putJob);
526 }
527 } else if (job == d->m_putJob) {
528 d->m_putJob = nullptr;
529 if (d->m_getJob) {
530 d->m_getJob->kill(verbosity: Quietly);
531 removeSubjob(job: d->m_getJob);
532 }
533 } else if (job == d->m_chmodJob) {
534 d->m_chmodJob = nullptr;
535 if (d->m_delJob) {
536 d->m_delJob->kill(verbosity: Quietly);
537 removeSubjob(job: d->m_delJob);
538 }
539 } else if (job == d->m_delJob) {
540 d->m_delJob = nullptr;
541 if (d->m_chmodJob) {
542 d->m_chmodJob->kill(verbosity: Quietly);
543 removeSubjob(job: d->m_chmodJob);
544 }
545 }
546 setError(job->error());
547 setErrorText(job->errorText());
548 emitResult();
549 return;
550 }
551
552 if (d->m_mustChmod) {
553 // If d->m_permissions == -1, keep the default permissions
554 if (d->m_permissions != -1) {
555 d->m_chmodJob = chmod(url: d->m_dest, permissions: d->m_permissions);
556 addSubjob(job: d->m_chmodJob);
557 }
558 d->m_mustChmod = false;
559 }
560
561 if (job == d->m_moveJob) {
562 d->m_moveJob = nullptr; // Finished
563 }
564
565 if (job == d->m_copyJob) {
566 d->m_copyJob = nullptr;
567 if (d->m_move) {
568 d->m_delJob = file_delete(src: d->m_src, flags: HideProgressInfo /*no GUI*/); // Delete source
569 addSubjob(job: d->m_delJob);
570 }
571 }
572
573 if (job == d->m_getJob) {
574 // qDebug() << "m_getJob finished";
575 d->m_getJob = nullptr; // No action required
576 if (d->m_putJob) {
577 d->m_putJob->d_func()->internalResume();
578 }
579 }
580
581 if (job == d->m_putJob) {
582 // qDebug() << "m_putJob finished";
583 d->m_putJob = nullptr;
584 if (d->m_getJob) {
585 // The get job is still running, probably after emitting data(QByteArray())
586 // and before we receive its finished().
587 d->m_getJob->d_func()->internalResume();
588 }
589 if (d->m_move) {
590 d->m_delJob = file_delete(src: d->m_src, flags: HideProgressInfo /*no GUI*/); // Delete source
591 addSubjob(job: d->m_delJob);
592 }
593 }
594
595 if (job == d->m_delJob) {
596 d->m_delJob = nullptr; // Finished
597 }
598
599 if (job == d->m_chmodJob) {
600 d->m_chmodJob = nullptr; // Finished
601 }
602
603 if (!hasSubjobs()) {
604 emitResult();
605 }
606}
607
608bool FileCopyJob::doKill()
609{
610#ifdef Q_OS_WIN
611 // TODO Use SetConsoleCtrlHandler on Windows or similar behaviour.
612 // https://stackoverflow.com/questions/2007516/is-there-a-posix-sigterm-alternative-on-windows-a-gentle-kill-for-console-ap
613 // https://danielkaes.wordpress.com/2009/06/04/how-to-catch-kill-events-with-python/
614 // https://phabricator.kde.org/D25117#566107
615
616 Q_D(FileCopyJob);
617
618 // If we are interrupted in the middle of file copying,
619 // we may end up with corrupted file at the destination.
620 // It is better to clean up this file. If a copy is being
621 // made as part of move operation then delete the dest only if
622 // source file is intact (m_delJob == NULL).
623 if (d->m_bFileCopyInProgress && d->m_copyJob && d->m_dest.isLocalFile()) {
624 if (d->m_flags & Overwrite) {
625 QFile::remove(d->m_dest.toLocalFile() + QStringLiteral(".part"));
626 } else {
627 QFile::remove(d->m_dest.toLocalFile());
628 }
629 }
630#endif
631 return Job::doKill();
632}
633
634FileCopyJob *KIO::file_copy(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags)
635{
636 return FileCopyJobPrivate::newJob(src, dest, permissions, move: false, flags);
637}
638
639FileCopyJob *KIO::file_move(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags)
640{
641 FileCopyJob *job = FileCopyJobPrivate::newJob(src, dest, permissions, move: true, flags);
642 if (job->uiDelegateExtension()) {
643 job->uiDelegateExtension()->createClipboardUpdater(job, mode: JobUiDelegateExtension::UpdateContent);
644 }
645 return job;
646}
647
648#include "moc_filecopyjob.cpp"
649

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