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 "slave.h"
15 #include <kio/jobuidelegatefactory.h>
16 
17 #include <KLocalizedString>
18 
19 #include <QFile>
20 #include <QTimer>
21 
22 using namespace KIO;
23 
jobSlave(SimpleJob * job)24 static inline Slave *jobSlave(SimpleJob *job)
25 {
26     return SimpleJobPrivate::get(job)->m_slave;
27 }
28 
29 /** @internal */
30 class KIO::FileCopyJobPrivate : public KIO::JobPrivate
31 {
32 public:
FileCopyJobPrivate(const QUrl & src,const QUrl & dest,int permissions,bool move,JobFlags flags)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 &slave_url);
72     void startRenameJob(const QUrl &slave_url);
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 
Q_DECLARE_PUBLIC(FileCopyJob)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("destUrl", 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 
isSrcDestSameSlaveProcess(const QUrl & src,const QUrl & dest)107 static bool isSrcDestSameSlaveProcess(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 io-slaves: We alternate between receiving a block of data
123  * and sending it away.
124  */
FileCopyJob(FileCopyJobPrivate & dd)125 FileCopyJob::FileCopyJob(FileCopyJobPrivate &dd)
126     : Job(dd)
127 {
128     Q_D(FileCopyJob);
129     QTimer::singleShot(0, this, [d]() {
130         d->slotStart();
131     });
132 }
133 
slotStart()134 void FileCopyJobPrivate::slotStart()
135 {
136     Q_Q(FileCopyJob);
137     if (!m_move) {
138         JobPrivate::emitCopying(q, m_src, m_dest);
139     } else {
140         JobPrivate::emitMoving(q, m_src, m_dest);
141     }
142 
143     if (m_move) {
144         // The if() below must be the same as the one in startBestCopyMethod
145         if (isSrcDestSameSlaveProcess(m_src, m_dest)) {
146             startRenameJob(m_src);
147             return;
148         } else if (m_src.isLocalFile() && KProtocolManager::canRenameFromFile(m_dest)) {
149             startRenameJob(m_dest);
150             return;
151         } else if (m_dest.isLocalFile() && KProtocolManager::canRenameToFile(m_src)) {
152             startRenameJob(m_src);
153             return;
154         }
155         // No fast-move available, use copy + del.
156     }
157     startBestCopyMethod();
158 }
159 
startBestCopyMethod()160 void FileCopyJobPrivate::startBestCopyMethod()
161 {
162     if (isSrcDestSameSlaveProcess(m_src, m_dest)) {
163         startCopyJob();
164     } else if (m_src.isLocalFile() && KProtocolManager::canCopyFromFile(m_dest)) {
165         startCopyJob(m_dest);
166     } else if (m_dest.isLocalFile() && KProtocolManager::canCopyToFile(m_src) && !KIO::Scheduler::isSlaveOnHoldFor(m_src)) {
167         startCopyJob(m_src);
168     } else {
169         startDataPump();
170     }
171 }
172 
~FileCopyJob()173 FileCopyJob::~FileCopyJob()
174 {
175 }
176 
setSourceSize(KIO::filesize_t size)177 void 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(KJob::Bytes, size);
183     }
184 }
185 
setModificationTime(const QDateTime & mtime)186 void FileCopyJob::setModificationTime(const QDateTime &mtime)
187 {
188     Q_D(FileCopyJob);
189     d->m_modificationTime = mtime;
190 }
191 
srcUrl() const192 QUrl FileCopyJob::srcUrl() const
193 {
194     return d_func()->m_src;
195 }
196 
destUrl() const197 QUrl FileCopyJob::destUrl() const
198 {
199     return d_func()->m_dest;
200 }
201 
startCopyJob()202 void FileCopyJobPrivate::startCopyJob()
203 {
204     startCopyJob(m_src);
205 }
206 
startCopyJob(const QUrl & slave_url)207 void FileCopyJobPrivate::startCopyJob(const QUrl &slave_url)
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(slave_url, packedArgs);
213     m_copyJob = job;
214     m_copyJob->setParentJob(q);
215     if (m_modificationTime.isValid()) {
216         m_copyJob->addMetaData(QStringLiteral("modified"), m_modificationTime.toString(Qt::ISODate)); // #55804
217     }
218     q->addSubjob(m_copyJob);
219     connectSubjob(m_copyJob);
220     q->connect(job, &DirectCopyJob::canResume, q, [this](KIO::Job *job, KIO::filesize_t offset) {
221         slotCanResume(job, offset);
222     });
223 }
224 
startRenameJob(const QUrl & slave_url)225 void FileCopyJobPrivate::startRenameJob(const QUrl &slave_url)
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(slave_url, CMD_RENAME, packedArgs);
231     m_moveJob->setParentJob(q);
232     if (m_modificationTime.isValid()) {
233         m_moveJob->addMetaData(QStringLiteral("modified"), m_modificationTime.toString(Qt::ISODate)); // #55804
234     }
235     q->addSubjob(m_moveJob);
236     connectSubjob(m_moveJob);
237 }
238 
connectSubjob(SimpleJob * job)239 void FileCopyJobPrivate::connectSubjob(SimpleJob *job)
240 {
241     Q_Q(FileCopyJob);
242     q->connect(job, &KJob::totalSize, q, [q](KJob *job, qulonglong totalSize) {
243         Q_UNUSED(job);
244         if (totalSize != q->totalAmount(KJob::Bytes)) {
245             q->setTotalAmount(KJob::Bytes, totalSize);
246         }
247     });
248 
249     q->connect(job, &KJob::processedSize, q, [q, this](KJob *job, qulonglong processedSize) {
250         if (job == m_copyJob) {
251             m_bFileCopyInProgress = processedSize > 0;
252         }
253         q->setProcessedAmount(KJob::Bytes, processedSize);
254     });
255 
256     q->connect(job, &KJob::percentChanged, q, [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 
doSuspend()267 bool 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 
doResume()290 bool 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 
startDataPump()313 void 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(m_dest, m_permissions, (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(m_putJob, &KIO::TransferJob::canResume, q, [this](KIO::Job *job, KIO::filesize_t offset) {
331         slotCanResume(job, offset);
332     });
333     q->connect(m_putJob, &KIO::TransferJob::dataReq, q, [this](KIO::Job *job, QByteArray &data) {
334         slotDataReq(job, data);
335     });
336     q->addSubjob(m_putJob);
337 }
338 
slotCanResume(KIO::Job * job,KIO::filesize_t offset)339 void 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         jobSlave(m_getJob)->setOffset(jobSlave(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 *>(kioJob);
359             if (!KProtocolManager::autoResume() && !(m_flags & Overwrite) && askUserActionInterface) {
360                 auto renameSignal = &AskUserActionInterface::askUserRenameResult;
361 
362                 q->connect(askUserActionInterface, renameSignal, q, [=](KIO::RenameDialog_Result result, const QUrl &, KJob *askJob) {
363                     Q_ASSERT(kioJob == askJob);
364 
365                     // Only receive askUserRenameResult once per rename dialog
366                     QObject::disconnect(askUserActionInterface, renameSignal, q, nullptr);
367 
368                     processCanResumeResult(job, result, offset);
369                 });
370 
371                 // Ask confirmation about resuming previous transfer
372                 askUserActionInterface->askUserRename(kioJob,
373                                                       i18n("File Already Exists"),
374                                                       m_src,
375                                                       m_dest,
376                                                       RenameDialog_Options(RenameDialog_Overwrite | RenameDialog_Resume | RenameDialog_NoRename),
377                                                       m_sourceSize,
378                                                       offset);
379                 return;
380             }
381         }
382 
383         processCanResumeResult(job, //
384                                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 
processCanResumeResult(KIO::Job * job,RenameDialog_Result result,KIO::filesize_t offset)393 void 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(FileCopyJob::Quietly);
401             q->removeSubjob(m_putJob);
402             m_putJob = nullptr;
403         } else {
404             m_copyJob->kill(FileCopyJob::Quietly);
405             q->removeSubjob(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         jobSlave(m_copyJob)->sendResumeAnswer(offset != 0);
415         return;
416     }
417 
418     if (job == m_putJob) {
419         m_getJob = KIO::get(m_src, NoReload, 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 slave doesn't emit totalSize.
425         if (m_sourceSize != (KIO::filesize_t)-1) {
426             m_getJob->setTotalAmount(KJob::Bytes, m_sourceSize);
427         }
428 
429         if (offset) {
430             // qDebug() << "Setting metadata for resume to" << (unsigned long) offset;
431             m_getJob->addMetaData(QStringLiteral("range-start"), KIO::number(offset));
432 
433             // Might or might not get emitted
434             q->connect(m_getJob, &KIO::TransferJob::canResume, q, [this](KIO::Job *job, KIO::filesize_t offset) {
435                 slotCanResume(job, offset);
436             });
437         }
438         jobSlave(m_putJob)->setOffset(offset);
439 
440         m_putJob->d_func()->internalSuspend();
441         q->addSubjob(m_getJob);
442         connectSubjob(m_getJob); // Progress info depends on get
443         m_getJob->d_func()->internalResume(); // Order a beer
444 
445         q->connect(m_getJob, &KIO::TransferJob::data, q, [this](KIO::Job *job, const QByteArray &data) {
446             slotData(job, data);
447         });
448         q->connect(m_getJob, &KIO::TransferJob::mimeTypeFound, q, [this](KIO::Job *job, const QString &type) {
449             slotMimetype(job, type);
450         });
451     }
452 }
453 
slotData(KIO::Job *,const QByteArray & data)454 void 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" slave about our
466     // decision about resuming
467     if (!m_resumeAnswerSent) {
468         m_resumeAnswerSent = true;
469         // qDebug() << "(first time) -> send resume answer " << m_canResume;
470         jobSlave(m_putJob)->sendResumeAnswer(m_canResume);
471     }
472 }
473 
slotDataReq(KIO::Job *,QByteArray & data)474 void 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(FileCopyJob::Quietly);
483         q->removeSubjob(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 
slotMimetype(KIO::Job *,const QString & type)496 void FileCopyJobPrivate::slotMimetype(KIO::Job *, const QString &type)
497 {
498     Q_Q(FileCopyJob);
499 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 78)
500     Q_EMIT q->mimetype(q, type);
501 #endif
502     Q_EMIT q->mimeTypeFound(q, type);
503 }
504 
slotResult(KJob * job)505 void FileCopyJob::slotResult(KJob *job)
506 {
507     Q_D(FileCopyJob);
508     // qDebug() << "this=" << this << "job=" << job;
509     removeSubjob(job);
510 
511     // If result comes from copyjob then we are not writing anymore.
512     if (job == d->m_copyJob) {
513         d->m_bFileCopyInProgress = false;
514     }
515 
516     // Did job have an error ?
517     if (job->error()) {
518         if ((job == d->m_moveJob) && (job->error() == ERR_UNSUPPORTED_ACTION)) {
519             d->m_moveJob = nullptr;
520             d->startBestCopyMethod();
521             return;
522         } else if ((job == d->m_copyJob) && (job->error() == ERR_UNSUPPORTED_ACTION)) {
523             d->m_copyJob = nullptr;
524             d->startDataPump();
525             return;
526         } else if (job == d->m_getJob) {
527             d->m_getJob = nullptr;
528             if (d->m_putJob) {
529                 d->m_putJob->kill(Quietly);
530                 removeSubjob(d->m_putJob);
531             }
532         } else if (job == d->m_putJob) {
533             d->m_putJob = nullptr;
534             if (d->m_getJob) {
535                 d->m_getJob->kill(Quietly);
536                 removeSubjob(d->m_getJob);
537             }
538         } else if (job == d->m_chmodJob) {
539             d->m_chmodJob = nullptr;
540             if (d->m_delJob) {
541                 d->m_delJob->kill(Quietly);
542                 removeSubjob(d->m_delJob);
543             }
544         } else if (job == d->m_delJob) {
545             d->m_delJob = nullptr;
546             if (d->m_chmodJob) {
547                 d->m_chmodJob->kill(Quietly);
548                 removeSubjob(d->m_chmodJob);
549             }
550         }
551         setError(job->error());
552         setErrorText(job->errorText());
553         emitResult();
554         return;
555     }
556 
557     if (d->m_mustChmod) {
558         // If d->m_permissions == -1, keep the default permissions
559         if (d->m_permissions != -1) {
560             d->m_chmodJob = chmod(d->m_dest, d->m_permissions);
561             addSubjob(d->m_chmodJob);
562         }
563         d->m_mustChmod = false;
564     }
565 
566     if (job == d->m_moveJob) {
567         d->m_moveJob = nullptr; // Finished
568     }
569 
570     if (job == d->m_copyJob) {
571         d->m_copyJob = nullptr;
572         if (d->m_move) {
573             d->m_delJob = file_delete(d->m_src, HideProgressInfo /*no GUI*/); // Delete source
574             addSubjob(d->m_delJob);
575         }
576     }
577 
578     if (job == d->m_getJob) {
579         // qDebug() << "m_getJob finished";
580         d->m_getJob = nullptr; // No action required
581         if (d->m_putJob) {
582             d->m_putJob->d_func()->internalResume();
583         }
584     }
585 
586     if (job == d->m_putJob) {
587         // qDebug() << "m_putJob finished";
588         d->m_putJob = nullptr;
589         if (d->m_getJob) {
590             // The get job is still running, probably after emitting data(QByteArray())
591             // and before we receive its finished().
592             d->m_getJob->d_func()->internalResume();
593         }
594         if (d->m_move) {
595             d->m_delJob = file_delete(d->m_src, HideProgressInfo /*no GUI*/); // Delete source
596             addSubjob(d->m_delJob);
597         }
598     }
599 
600     if (job == d->m_delJob) {
601         d->m_delJob = nullptr; // Finished
602     }
603 
604     if (job == d->m_chmodJob) {
605         d->m_chmodJob = nullptr; // Finished
606     }
607 
608     if (!hasSubjobs()) {
609         emitResult();
610     }
611 }
612 
doKill()613 bool FileCopyJob::doKill()
614 {
615 #ifdef Q_OS_WIN
616     // TODO Use SetConsoleCtrlHandler on Windows or similar behaviour.
617     // https://stackoverflow.com/questions/2007516/is-there-a-posix-sigterm-alternative-on-windows-a-gentle-kill-for-console-ap
618     // https://danielkaes.wordpress.com/2009/06/04/how-to-catch-kill-events-with-python/
619     // https://phabricator.kde.org/D25117#566107
620 
621     Q_D(FileCopyJob);
622 
623     // If we are interrupted in the middle of file copying,
624     // we may end up with corrupted file at the destination.
625     // It is better to clean up this file. If a copy is being
626     // made as part of move operation then delete the dest only if
627     // source file is intact (m_delJob == NULL).
628     if (d->m_bFileCopyInProgress && d->m_copyJob && d->m_dest.isLocalFile()) {
629         if (d->m_flags & Overwrite) {
630             QFile::remove(d->m_dest.toLocalFile() + QStringLiteral(".part"));
631         } else {
632             QFile::remove(d->m_dest.toLocalFile());
633         }
634     }
635 #endif
636     return Job::doKill();
637 }
638 
file_copy(const QUrl & src,const QUrl & dest,int permissions,JobFlags flags)639 FileCopyJob *KIO::file_copy(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags)
640 {
641     return FileCopyJobPrivate::newJob(src, dest, permissions, false, flags);
642 }
643 
file_move(const QUrl & src,const QUrl & dest,int permissions,JobFlags flags)644 FileCopyJob *KIO::file_move(const QUrl &src, const QUrl &dest, int permissions, JobFlags flags)
645 {
646     FileCopyJob *job = FileCopyJobPrivate::newJob(src, dest, permissions, true, flags);
647     if (job->uiDelegateExtension()) {
648         job->uiDelegateExtension()->createClipboardUpdater(job, JobUiDelegateExtension::UpdateContent);
649     }
650     return job;
651 }
652 
653 #include "moc_filecopyjob.cpp"
654