1 /*
2 * Copyright (C) 2010, 2011 Daniele E. Domenichelli <daniele.domenichelli@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17 */
18 
19 
20 #include "handle-incoming-file-transfer-channel-job.h"
21 #include "telepathy-base-job_p.h"
22 #include "ktp-fth-debug.h"
23 
24 #include <QTimer>
25 #include <QUrl>
26 #include <QPointer>
27 #include <QDebug>
28 #include <QFileDialog>
29 
30 #include <KLocalizedString>
31 #include <kio/renamedialog.h>
32 #include <kio/global.h>
33 #include <KIOFileWidgets/KFileWidget>
34 #include <KIOFileWidgets/KRecentDirs>
35 #include <kjobtrackerinterface.h>
36 
37 #include <TelepathyQt/IncomingFileTransferChannel>
38 #include <TelepathyQt/PendingReady>
39 #include <TelepathyQt/PendingOperation>
40 #include <TelepathyQt/Contact>
41 
42 
43 class HandleIncomingFileTransferChannelJobPrivate : public KTp::TelepathyBaseJobPrivate
44 {
45     Q_DECLARE_PUBLIC(HandleIncomingFileTransferChannelJob)
46 
47 public:
48     HandleIncomingFileTransferChannelJobPrivate();
49     virtual ~HandleIncomingFileTransferChannelJobPrivate();
50 
51     Tp::IncomingFileTransferChannelPtr channel;
52     QString downloadDirectory;
53     bool askForDownloadDirectory;
54     QFile* file;
55     QUrl url, partUrl;
56     qulonglong offset;
57     bool isResuming;
58     QPointer<KIO::RenameDialog> renameDialog;
59 
60     void init();
61     void start();
62     bool kill();
63     void checkFileExists();
64     void checkPartFile();
65     void receiveFile();
66 
67     void __k__onRenameDialogFinished(int result);
68     void __k__onResumeDialogFinished(int result);
69     void __k__onSetUriOperationFinished(Tp::PendingOperation* op);
70     void __k__onInitialOffsetDefined(qulonglong offset);
71     void __k__onFileTransferChannelStateChanged(Tp::FileTransferState state, Tp::FileTransferStateChangeReason reason);
72     void __k__onFileTransferChannelTransferredBytesChanged(qulonglong count);
73     void __k__acceptFile();
74     void __k__onAcceptFileFinished(Tp::PendingOperation* op);
75     void __k__onCancelOperationFinished(Tp::PendingOperation* op);
76     void __k__onInvalidated();
77 };
78 
HandleIncomingFileTransferChannelJob(Tp::IncomingFileTransferChannelPtr channel,const QString downloadDirectory,bool askForDownloadDirectory,QObject * parent)79 HandleIncomingFileTransferChannelJob::HandleIncomingFileTransferChannelJob(Tp::IncomingFileTransferChannelPtr channel,
80                                                                            const QString downloadDirectory,
81                                                                            bool askForDownloadDirectory,
82                                                                            QObject* parent)
83     : TelepathyBaseJob(*new HandleIncomingFileTransferChannelJobPrivate(), parent)
84 {
85     qCDebug(KTP_FTH_MODULE);
86     Q_D(HandleIncomingFileTransferChannelJob);
87 
88     d->channel = channel;
89     d->downloadDirectory = downloadDirectory;
90     d->askForDownloadDirectory = askForDownloadDirectory;
91     d->init();
92 }
93 
~HandleIncomingFileTransferChannelJob()94 HandleIncomingFileTransferChannelJob::~HandleIncomingFileTransferChannelJob()
95 {
96     qCDebug(KTP_FTH_MODULE);
97     KIO::getJobTracker()->unregisterJob(this);
98 }
99 
start()100 void HandleIncomingFileTransferChannelJob::start()
101 {
102     qCDebug(KTP_FTH_MODULE);
103     Q_D(HandleIncomingFileTransferChannelJob);
104     d->start();
105 }
106 
doKill()107 bool HandleIncomingFileTransferChannelJob::doKill()
108 {
109     qCDebug(KTP_FTH_MODULE) << "Incoming file transfer killed.";
110     Q_D(HandleIncomingFileTransferChannelJob);
111     return d->kill();
112 }
113 
HandleIncomingFileTransferChannelJobPrivate()114 HandleIncomingFileTransferChannelJobPrivate::HandleIncomingFileTransferChannelJobPrivate()
115     : askForDownloadDirectory(true),
116       file(0),
117       offset(0),
118       isResuming(false)
119 {
120     qCDebug(KTP_FTH_MODULE);
121 }
122 
~HandleIncomingFileTransferChannelJobPrivate()123 HandleIncomingFileTransferChannelJobPrivate::~HandleIncomingFileTransferChannelJobPrivate()
124 {
125     qCDebug(KTP_FTH_MODULE);
126 }
127 
init()128 void HandleIncomingFileTransferChannelJobPrivate::init()
129 {
130     qCDebug(KTP_FTH_MODULE);
131     Q_Q(HandleIncomingFileTransferChannelJob);
132 
133     if (channel.isNull()) {
134         qCritical() << "Channel cannot be NULL";
135         q->setError(KTp::NullChannel);
136         q->setErrorText(i18n("Invalid channel"));
137         QTimer::singleShot(0, q, SLOT(__k__doEmitResult()));
138         return;
139     }
140 
141     Tp::Features features = Tp::Features() << Tp::FileTransferChannel::FeatureCore;
142     if (!channel->isReady(Tp::Features() << Tp::FileTransferChannel::FeatureCore)) {
143         qCritical() << "Channel must be ready with Tp::FileTransferChannel::FeatureCore";
144         q->setError(KTp::FeatureNotReady);
145         q->setErrorText(i18n("Channel is not ready"));
146         QTimer::singleShot(0, q, SLOT(__k__doEmitResult()));
147         return;
148     }
149 
150     q->setCapabilities(KJob::Killable);
151     q->setTotalAmount(KJob::Bytes, channel->size());
152     q->setProcessedAmountAndCalculateSpeed(0);
153 
154     q->connect(channel.data(),
155                SIGNAL(invalidated(Tp::DBusProxy*,QString,QString)),
156                SLOT(__k__onInvalidated()));
157     q->connect(channel.data(),
158                SIGNAL(initialOffsetDefined(qulonglong)),
159                SLOT(__k__onInitialOffsetDefined(qulonglong)));
160     q->connect(channel.data(),
161                SIGNAL(stateChanged(Tp::FileTransferState,Tp::FileTransferStateChangeReason)),
162                SLOT(__k__onFileTransferChannelStateChanged(Tp::FileTransferState,Tp::FileTransferStateChangeReason)));
163     q->connect(channel.data(),
164                SIGNAL(transferredBytesChanged(qulonglong)),
165                SLOT(__k__onFileTransferChannelTransferredBytesChanged(qulonglong)));
166 }
167 
start()168 void HandleIncomingFileTransferChannelJobPrivate::start()
169 {
170     qCDebug(KTP_FTH_MODULE);
171     Q_Q(HandleIncomingFileTransferChannelJob);
172 
173     Q_ASSERT(!q->error());
174     if (q->error()) {
175         qCWarning(KTP_FTH_MODULE) << "Job was started in error state. Something wrong happened." << q->errorString();
176         QTimer::singleShot(0, q, SLOT(__k__doEmitResult()));
177         return;
178     }
179 
180     if (askForDownloadDirectory) {
181 
182         QString recentDirClass;
183 
184         url = QFileDialog::getSaveFileUrl(0, QString(),
185                                           KFileWidget::getStartUrl(QUrl(QLatin1String("kfiledialog:///FileTransferLastDirectory/") + channel->fileName()), recentDirClass));
186 
187         if (!recentDirClass.isEmpty()) {
188             KRecentDirs::add(recentDirClass, url.toLocalFile());
189         }
190 
191         partUrl = url;
192         partUrl.setPath(url.path() + QLatin1String(".part"));
193 
194         checkPartFile();
195         return;
196     }
197 
198     checkFileExists();
199 }
200 
checkFileExists()201 void HandleIncomingFileTransferChannelJobPrivate::checkFileExists()
202 {
203     Q_Q(HandleIncomingFileTransferChannelJob);
204 
205     url = QUrl::fromLocalFile(downloadDirectory + QLatin1Char('/') + channel->fileName());
206 
207     partUrl = url;
208     partUrl.setPath(url.path() + QLatin1String(".part"));
209 
210     QFileInfo fileInfo(url.toLocalFile()); // TODO check if it is a dir?
211     if (fileInfo.exists()) {
212         renameDialog = new KIO::RenameDialog(0,
213                                              i18n("Incoming file exists"),
214                                              QUrl(), //TODO
215                                              url,
216                                              KIO::RenameDialog_Overwrite,
217                                              fileInfo.size(),
218                                              channel->size(),
219                                              fileInfo.created(),
220                                              QDateTime(),
221                                              fileInfo.lastModified(),
222                                              channel->lastModificationTime());
223 
224         q->connect(q, SIGNAL(finished(KJob*)),
225                    renameDialog.data(), SLOT(reject()));
226 
227         q->connect(renameDialog.data(),
228                    SIGNAL(finished(int)),
229                    SLOT(__k__onRenameDialogFinished(int)));
230 
231         renameDialog.data()->show();
232         return;
233     }
234 
235     checkPartFile();
236 }
237 
__k__onRenameDialogFinished(int result)238 void HandleIncomingFileTransferChannelJobPrivate::__k__onRenameDialogFinished(int result)
239 {
240     qCDebug(KTP_FTH_MODULE);
241     Q_Q(HandleIncomingFileTransferChannelJob);
242 
243     if (!renameDialog) {
244         qCWarning(KTP_FTH_MODULE) << "Rename dialog was deleted during event loop.";
245         QTimer::singleShot(0, q, SLOT(__k__doEmitResult()));
246         return;
247     }
248 
249     Q_ASSERT(renameDialog.data()->result() == result);
250 
251     switch (result) {
252     case KIO::R_CANCEL:
253         // TODO Cancel file transfer and close channel
254         channel->cancel();
255         QTimer::singleShot(0, q, SLOT(__k__doEmitResult()));
256         return;
257     case KIO::R_RENAME:
258         url = renameDialog.data()->newDestUrl();
259         break;
260     case KIO::R_OVERWRITE:
261     {
262         // Delete the old file if exists
263         QFile oldFile(url.toLocalFile(), 0);
264         if (oldFile.exists()) {
265             oldFile.remove();
266         }
267     }
268         break;
269     default:
270         qCWarning(KTP_FTH_MODULE) << "Unknown Error";
271         q->setError(KTp::KTpError);
272         q->setErrorText(i18n("Unknown Error"));
273         renameDialog.data()->deleteLater();
274         QTimer::singleShot(0, q, SLOT(__k__doEmitResult()));
275         return;
276     }
277     renameDialog.data()->deleteLater();
278     renameDialog.clear();
279     checkPartFile();
280 }
281 
checkPartFile()282 void HandleIncomingFileTransferChannelJobPrivate::checkPartFile()
283 {
284     qCDebug(KTP_FTH_MODULE);
285     Q_Q(HandleIncomingFileTransferChannelJob);
286 
287     QFileInfo fileInfo(partUrl.toLocalFile());
288     if (fileInfo.exists()) {
289         renameDialog = new KIO::RenameDialog(0,
290                                              i18n("Would you like to resume partial download?"),
291                                              QUrl(),
292                                              partUrl,
293                                              KIO::RenameDialog_Resume,
294                                              fileInfo.size(),
295                                              channel->size(),
296                                              fileInfo.created(),
297                                              QDateTime(),
298                                              fileInfo.lastModified(),
299                                              channel->lastModificationTime());
300 
301         q->connect(q, SIGNAL(finished(KJob*)),
302                    renameDialog.data(), SLOT(reject()));
303 
304         q->connect(renameDialog.data(),
305                    SIGNAL(finished(int)),
306                    SLOT(__k__onResumeDialogFinished(int)));
307 
308         renameDialog.data()->show();
309         return;
310     }
311     receiveFile();
312 }
313 
314 
__k__onResumeDialogFinished(int result)315 void HandleIncomingFileTransferChannelJobPrivate::__k__onResumeDialogFinished(int result)
316 {
317     qCDebug(KTP_FTH_MODULE);
318     Q_Q(HandleIncomingFileTransferChannelJob);
319 
320     if (!renameDialog) {
321         qCWarning(KTP_FTH_MODULE) << "Rename dialog was deleted during event loop.";
322         QTimer::singleShot(0, q, SLOT(__k__doEmitResult()));
323         return;
324     }
325 
326     Q_ASSERT(renameDialog.data()->result() == result);
327 
328     switch (result) {
329     case KIO::R_RESUME:
330     {
331         QFileInfo fileInfo(partUrl.toLocalFile());
332         offset = fileInfo.size();
333         isResuming = true;
334         break;
335     }
336     case KIO::R_RENAME:
337         // If the user hits rename, we use the new name as the .part file
338         partUrl = renameDialog.data()->newDestUrl();
339         break;
340     case KIO::R_CANCEL:
341         // If user hits cancel .part file will be overwritten
342     default:
343         break;
344     }
345 
346     receiveFile();
347 }
348 
receiveFile()349 void HandleIncomingFileTransferChannelJobPrivate::receiveFile()
350 {
351     qCDebug(KTP_FTH_MODULE);
352     Q_Q(HandleIncomingFileTransferChannelJob);
353 
354     // Open the .part file in append mode
355     file = new QFile(partUrl.toLocalFile(), q->parent());
356     file->open(isResuming ? QIODevice::Append : QIODevice::WriteOnly);
357 
358     // Create an empty file with the definitive file name
359     QFile realFile(url.toLocalFile(), 0);
360     realFile.open(QIODevice::WriteOnly);
361 
362     Tp::PendingOperation* setUriOperation = channel->setUri(url.url());
363     q->connect(setUriOperation,
364                SIGNAL(finished(Tp::PendingOperation*)),
365                SLOT(__k__onSetUriOperationFinished(Tp::PendingOperation*)));
366 }
367 
kill()368 bool HandleIncomingFileTransferChannelJobPrivate::kill()
369 {
370     qCDebug(KTP_FTH_MODULE);
371     Q_Q(HandleIncomingFileTransferChannelJob);
372 
373     if (channel->state() != Tp::FileTransferStateCancelled) {
374         Tp::PendingOperation *cancelOperation = channel->cancel();
375         q->connect(cancelOperation,
376                    SIGNAL(finished(Tp::PendingOperation*)),
377                    SLOT(__k__onCancelOperationFinished(Tp::PendingOperation*)));
378     } else {
379         QTimer::singleShot(0, q, SLOT(__k__doEmitResult()));
380     }
381 
382     return true;
383 }
384 
__k__onSetUriOperationFinished(Tp::PendingOperation * op)385 void HandleIncomingFileTransferChannelJobPrivate::__k__onSetUriOperationFinished(Tp::PendingOperation* op)
386 {
387     qCDebug(KTP_FTH_MODULE);
388     Q_Q(HandleIncomingFileTransferChannelJob);
389 
390     if (op->isError()) {
391         // We do not want to exit if setUri failed, but we try to send the file
392         // anyway. Anyway we print a message for debugging purposes.
393         qCWarning(KTP_FTH_MODULE) << "Unable to set the URI -" << op->errorName() << ":" << op->errorMessage();
394     }
395 
396     KIO::getJobTracker()->registerJob(q);
397     // KWidgetJobTracker has an internal timer of 500 ms, if we don't wait here
398     // when the job description is emitted it won't be ready
399     // We set the description here and then whe update it if path is changed.
400 
401     QTimer::singleShot(500, q, SLOT(__k__acceptFile()));
402 }
403 
__k__acceptFile()404 void HandleIncomingFileTransferChannelJobPrivate::__k__acceptFile()
405 {
406     qCDebug(KTP_FTH_MODULE);
407     Q_Q(HandleIncomingFileTransferChannelJob);
408 
409     Q_EMIT q->description(q, i18n("Incoming file transfer"),
410                           qMakePair<QString, QString>(i18n("From"), channel->targetContact()->alias()),
411                           qMakePair<QString, QString>(i18n("Filename"), url.toLocalFile()));
412 
413     Tp::PendingOperation* acceptFileOperation = channel->acceptFile(offset, file);
414     q->connect(acceptFileOperation,
415                SIGNAL(finished(Tp::PendingOperation*)),
416                SLOT(__k__onAcceptFileFinished(Tp::PendingOperation*)));
417 }
418 
__k__onInitialOffsetDefined(qulonglong offset)419 void HandleIncomingFileTransferChannelJobPrivate::__k__onInitialOffsetDefined(qulonglong offset)
420 {
421     qCDebug(KTP_FTH_MODULE) << "__k__onInitialOffsetDefined" << offset;
422     Q_Q(HandleIncomingFileTransferChannelJob);
423 
424     // Some protocols do not support resuming file transfers, therefore we need
425     // to use to this method to set the real offset
426     if (isResuming && offset == 0) {
427         qCDebug(KTP_FTH_MODULE) << "Impossible to resume file. Restarting.";
428         Q_EMIT q->infoMessage(q, i18n("Impossible to resume file transfer. Restarting."));
429     }
430 
431     this->offset = offset;
432 
433     file->seek(offset);
434     q->setProcessedAmountAndCalculateSpeed(offset);
435 }
436 
__k__onFileTransferChannelStateChanged(Tp::FileTransferState state,Tp::FileTransferStateChangeReason stateReason)437 void HandleIncomingFileTransferChannelJobPrivate::__k__onFileTransferChannelStateChanged(Tp::FileTransferState state,
438                                                                                          Tp::FileTransferStateChangeReason stateReason)
439 {
440     qCDebug(KTP_FTH_MODULE);
441     Q_Q(HandleIncomingFileTransferChannelJob);
442 
443     qCDebug(KTP_FTH_MODULE) << "Incoming file transfer channel state changed to" << state << "with reason" << stateReason;
444 
445     switch (state) {
446     case Tp::FileTransferStateNone:
447         // This is bad
448         qCWarning(KTP_FTH_MODULE) << "An unknown error occurred.";
449         q->setError(KTp::TelepathyErrorError);
450         q->setErrorText(i18n("An unknown error occurred"));
451         QTimer::singleShot(0, q, SLOT(__k__doEmitResult()));
452         break;
453     case Tp::FileTransferStateCompleted:
454     {
455         QFileInfo fileinfo(url.toLocalFile());
456         if (fileinfo.exists()) {
457             QFile::remove(url.toLocalFile());
458         }
459         file->rename(url.toLocalFile());
460         file->flush();
461         file->close();
462         qCDebug(KTP_FTH_MODULE) << "Incoming file transfer completed, saved at" << file->fileName();
463         Q_EMIT q->infoMessage(q, i18n("Incoming file transfer")); // [Finished] is added automatically to the notification
464         QTimer::singleShot(0, q, SLOT(__k__doEmitResult()));
465         break;
466     }
467     case Tp::FileTransferStateCancelled:
468     {
469         q->setError(KTp::FileTransferCancelled);
470         q->setErrorText(i18n("Incoming file transfer was canceled."));
471         // Close .part file if open
472         if (file && file->isOpen()) {
473             file->close();
474         }
475         q->kill(KJob::Quietly);
476         break;
477     }
478     case Tp::FileTransferStateAccepted:
479     case Tp::FileTransferStatePending:
480     case Tp::FileTransferStateOpen:
481     default:
482         break;
483     }
484 }
485 
__k__onFileTransferChannelTransferredBytesChanged(qulonglong count)486 void HandleIncomingFileTransferChannelJobPrivate::__k__onFileTransferChannelTransferredBytesChanged(qulonglong count)
487 {
488     qCDebug(KTP_FTH_MODULE);
489     Q_Q(HandleIncomingFileTransferChannelJob);
490 
491     qCDebug(KTP_FTH_MODULE).nospace() << "Receiving " << channel->fileName() << " - "
492                        << "transferred bytes" << " = " << offset + count << " ("
493                        << ((int)(((double)(offset + count) / channel->size()) * 100)) << "% done)";
494     q->setProcessedAmountAndCalculateSpeed(offset + count);
495 }
496 
__k__onAcceptFileFinished(Tp::PendingOperation * op)497 void HandleIncomingFileTransferChannelJobPrivate::__k__onAcceptFileFinished(Tp::PendingOperation* op)
498 {
499     // This method is called when the "acceptFile" operation is finished,
500     // therefore the file was not received yet.
501     qCDebug(KTP_FTH_MODULE);
502     Q_Q(HandleIncomingFileTransferChannelJob);
503 
504     if (op->isError()) {
505         qCWarning(KTP_FTH_MODULE) << "Unable to accept file -" << op->errorName() << ":" << op->errorMessage();
506         q->setError(KTp::AcceptFileError);
507         q->setErrorText(i18n("Unable to accept file"));
508         QTimer::singleShot(0, q, SLOT(__k__doEmitResult()));
509     }
510 }
511 
__k__onCancelOperationFinished(Tp::PendingOperation * op)512 void HandleIncomingFileTransferChannelJobPrivate::__k__onCancelOperationFinished(Tp::PendingOperation* op)
513 {
514     qCDebug(KTP_FTH_MODULE);
515     Q_Q(HandleIncomingFileTransferChannelJob);
516 
517     if (op->isError()) {
518         qCWarning(KTP_FTH_MODULE) << "Unable to cancel file transfer - " << op->errorName() << ":" << op->errorMessage();
519         q->setError(KTp::CancelFileTransferError);
520         q->setErrorText(i18n("Cannot cancel incoming file transfer"));
521     }
522 
523     qCDebug(KTP_FTH_MODULE) << "File transfer cancelled";
524     QTimer::singleShot(0, q, SLOT(__k__doEmitResult()));
525 }
526 
__k__onInvalidated()527 void HandleIncomingFileTransferChannelJobPrivate::__k__onInvalidated()
528 {
529     qCDebug(KTP_FTH_MODULE);
530     Q_Q(HandleIncomingFileTransferChannelJob);
531 
532     qCWarning(KTP_FTH_MODULE) << "File transfer invalidated!" << channel->invalidationMessage() << "reason" << channel->invalidationReason();
533     Q_EMIT q->infoMessage(q, i18n("File transfer invalidated. %1", channel->invalidationMessage()));
534 
535     QTimer::singleShot(0, q, SLOT(__k__doEmitResult()));
536 }
537 
538 #include "moc_handle-incoming-file-transfer-channel-job.cpp"
539