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