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

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