1 //**************************************************************************
2 // Copyright 2006 - 2018 Martin Koller, kollix@aon.at
3 //
4 // This program is free software; you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, version 2 of the License
7 //
8 //**************************************************************************
9
10 #include <Archiver.hxx>
11
12 #include <kio_version.h>
13 #include <KTar>
14 #include <KFilterBase>
15 #include <kio/job.h>
16 #include <kio/jobuidelegate.h>
17 #include <KProcess>
18 #include <KMountPoint>
19 #include <KLocalizedString>
20 #include <KMessageBox>
21
22 #include <QApplication>
23 #include <QDir>
24 #include <QFileInfo>
25 #include <QCursor>
26 #include <QTextStream>
27 #include <QFileDialog>
28 #include <QTemporaryFile>
29 #include <QTimer>
30 #include <QElapsedTimer>
31
32 #include <sys/types.h>
33 #include <sys/stat.h>
34 #include <unistd.h>
35 #include <string.h>
36 #include <cerrno>
37 #include <sys/statvfs.h>
38
39 // For INT64_MAX:
40 // The ISO C99 standard specifies that in C++ implementations these
41 // macros (stdint.h,inttypes.h) should only be defined if explicitly requested.
42
43 // ISO C99: 7.18 Integer types
44 #ifndef __STDC_LIMIT_MACROS
45 #define __STDC_LIMIT_MACROS
46 #endif
47 #include <stdint.h>
48
49
50 #include <iostream>
51
52 //--------------------------------------------------------------------------------
53
54 QString Archiver::sliceScript;
55 Archiver *Archiver::instance;
56
57 const KIO::filesize_t MAX_SLICE = INT64_MAX; // 64bit max value
58
59 //--------------------------------------------------------------------------------
60
Archiver(QWidget * parent)61 Archiver::Archiver(QWidget *parent)
62 : QObject(parent),
63 archive(nullptr), totalBytes(0), totalFiles(0), filteredFiles(0), sliceNum(0), mediaNeedsChange(false),
64 fullBackupInterval(1), incrementalBackup(false), forceFullBackup(false),
65 sliceCapacity(MAX_SLICE), compressionType(KCompressionDevice::None), interactive(parent != nullptr),
66 cancelled(false), runs(false), skippedFiles(false), verbose(false), jobResult(0)
67 {
68 instance = this;
69
70 maxSliceMBs = Archiver::UNLIMITED;
71 numKeptBackups = Archiver::UNLIMITED;
72
73 setCompressFiles(false);
74
75 if ( !interactive )
76 {
77 connect(this, &Archiver::logging, this, &Archiver::loggingSlot);
78 connect(this, &Archiver::warning, this, &Archiver::warningSlot);
79 }
80 }
81
82 //--------------------------------------------------------------------------------
83
setCompressFiles(bool b)84 void Archiver::setCompressFiles(bool b)
85 {
86 if ( b )
87 {
88 ext = QStringLiteral(".xz");
89 compressionType = KCompressionDevice::Xz;
90 KFilterBase *base = KCompressionDevice::filterForCompressionType(compressionType);
91 if ( !base )
92 {
93 ext = QStringLiteral(".bz2");
94 compressionType = KCompressionDevice::BZip2;
95 base = KCompressionDevice::filterForCompressionType(compressionType);
96 if ( !base )
97 {
98 ext = QStringLiteral(".gz");
99 compressionType = KCompressionDevice::GZip;
100 }
101 }
102
103 delete base;
104 }
105 else
106 {
107 ext = QString();
108 }
109 }
110
111 //--------------------------------------------------------------------------------
112
setTarget(const QUrl & target)113 void Archiver::setTarget(const QUrl &target)
114 {
115 targetURL = target;
116 calculateCapacity();
117 }
118
119 //--------------------------------------------------------------------------------
120
setMaxSliceMBs(int mbs)121 void Archiver::setMaxSliceMBs(int mbs)
122 {
123 maxSliceMBs = mbs;
124 calculateCapacity();
125 }
126
127 //--------------------------------------------------------------------------------
128
setKeptBackups(int num)129 void Archiver::setKeptBackups(int num)
130 {
131 numKeptBackups = num;
132 }
133
134 //--------------------------------------------------------------------------------
135
setFilter(const QString & filter)136 void Archiver::setFilter(const QString &filter)
137 {
138 filters.clear();
139 const QStringList list = filter.split(QLatin1Char(' '), Qt::SkipEmptyParts);
140 filters.reserve(list.count());
141 for (const QString &str : list)
142 filters.append(QRegExp(str, Qt::CaseSensitive, QRegExp::Wildcard));
143 }
144
145 //--------------------------------------------------------------------------------
146
getFilter() const147 QString Archiver::getFilter() const
148 {
149 QString filter;
150 for (const QRegExp ® : std::as_const(filters))
151 {
152 filter += reg.pattern();
153 filter += QLatin1Char(' ');
154 }
155 return filter;
156 }
157
158 //--------------------------------------------------------------------------------
159
setDirFilter(const QString & filter)160 void Archiver::setDirFilter(const QString &filter)
161 {
162 dirFilters.clear();
163 const QStringList list = filter.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
164 for (const QString &str : list)
165 {
166 QString expr = str.trimmed();
167 if ( !expr.isEmpty() )
168 dirFilters.append(QRegExp(expr, Qt::CaseSensitive, QRegExp::Wildcard));
169 }
170 }
171
172 //--------------------------------------------------------------------------------
173
getDirFilter() const174 QString Archiver::getDirFilter() const
175 {
176 QString filter;
177 for (const QRegExp ® : std::as_const(dirFilters))
178 {
179 filter += reg.pattern();
180 filter += QLatin1Char('\n');
181 }
182 return filter;
183 }
184
185 //--------------------------------------------------------------------------------
186
setFullBackupInterval(int days)187 void Archiver::setFullBackupInterval(int days)
188 {
189 fullBackupInterval = days;
190
191 if ( fullBackupInterval == 1 )
192 {
193 setIncrementalBackup(false);
194 lastFullBackup = QDateTime();
195 lastBackup = QDateTime();
196 }
197 }
198
199 //--------------------------------------------------------------------------------
200
setForceFullBackup(bool force)201 void Archiver::setForceFullBackup(bool force)
202 {
203 forceFullBackup = force;
204 Q_EMIT backupTypeChanged(isIncrementalBackup());
205 }
206
207 //--------------------------------------------------------------------------------
208
setIncrementalBackup(bool inc)209 void Archiver::setIncrementalBackup(bool inc)
210 {
211 incrementalBackup = inc;
212 Q_EMIT backupTypeChanged(isIncrementalBackup());
213 }
214
215 //--------------------------------------------------------------------------------
216
setFilePrefix(const QString & prefix)217 void Archiver::setFilePrefix(const QString &prefix)
218 {
219 filePrefix = prefix;
220 }
221
222 //--------------------------------------------------------------------------------
223
calculateCapacity()224 void Archiver::calculateCapacity()
225 {
226 if ( targetURL.isEmpty() ) return;
227
228 // calculate how large a slice can actually be
229 // - limited by the target directory (when we store directly into a local dir)
230 // - limited by the "tmp" dir when we create a tmp file for later upload via KIO
231 // - limited by Qt (64bit int)
232 // - limited by user defined maxSliceMBs
233
234 KIO::filesize_t totalBytes = 0;
235
236 if ( targetURL.isLocalFile() )
237 {
238 if ( ! getDiskFree(targetURL.path(), totalBytes, sliceCapacity) )
239 return;
240 }
241 else
242 {
243 getDiskFree(QDir::tempPath() + QLatin1Char('/'), totalBytes, sliceCapacity);
244 // as "tmp" is also used by others and by us when compressing a file,
245 // don't eat it up completely. Reserve 10%
246 sliceCapacity = sliceCapacity * 9 / 10;
247 }
248
249 // limit to what Qt can handle
250 sliceCapacity = qMin(sliceCapacity, MAX_SLICE);
251
252 if ( maxSliceMBs != UNLIMITED )
253 {
254 KIO::filesize_t max = static_cast<KIO::filesize_t>(maxSliceMBs) * 1024 * 1024;
255 sliceCapacity = qMin(sliceCapacity, max);
256 }
257
258 sliceBytes = 0;
259
260 // if the disk is full (capacity == 0), don't tell the user "unlimited"
261 // sliceCapacity == 0 has a special meaning as "unlimited"; see MainWidget.cxx
262 if ( sliceCapacity == 0 ) sliceCapacity = 1;
263 Q_EMIT targetCapacity(sliceCapacity);
264 }
265
266 //--------------------------------------------------------------------------------
267
loadProfile(const QString & fileName,QStringList & includes,QStringList & excludes,QString & error)268 bool Archiver::loadProfile(const QString &fileName, QStringList &includes, QStringList &excludes, QString &error)
269 {
270 QFile file(fileName);
271 if ( ! file.open(QIODevice::ReadOnly) )
272 {
273 error = file.errorString();
274 return false;
275 }
276
277 loadedProfile = fileName;
278
279 QString target;
280 QChar type, blank;
281 QTextStream stream(&file);
282
283 // back to default (in case old profile read which does not include these)
284 setFilePrefix(QString());
285 setMaxSliceMBs(Archiver::UNLIMITED);
286 setFullBackupInterval(1); // default as in previous versions
287 filters.clear();
288 dirFilters.clear();
289
290 while ( ! stream.atEnd() )
291 {
292 stream.skipWhiteSpace();
293 stream >> type; // read a QChar without skipping whitespace
294 stream >> blank; // read a QChar without skipping whitespace
295
296 if ( type == QLatin1Char('M') )
297 {
298 target = stream.readLine(); // include white space
299 }
300 else if ( type == QLatin1Char('P') )
301 {
302 QString prefix = stream.readLine(); // include white space
303 setFilePrefix(prefix);
304 }
305 else if ( type == QLatin1Char('R') )
306 {
307 int max;
308 stream >> max;
309 setKeptBackups(max);
310 }
311 else if ( type == QLatin1Char('F') )
312 {
313 int days;
314 stream >> days;
315 setFullBackupInterval(days);
316 }
317 else if ( type == QLatin1Char('B') ) // last dateTime for backup
318 {
319 QString dateTime;
320 stream >> dateTime;
321 lastBackup = QDateTime::fromString(dateTime, Qt::ISODate);
322 }
323 else if ( type == QLatin1Char('L') ) // last dateTime for full backup
324 {
325 QString dateTime;
326 stream >> dateTime;
327 lastFullBackup = QDateTime::fromString(dateTime, Qt::ISODate);
328 }
329 else if ( type == QLatin1Char('S') )
330 {
331 int max;
332 stream >> max;
333 setMaxSliceMBs(max);
334 }
335 else if ( type == QLatin1Char('C') )
336 {
337 int change;
338 stream >> change;
339 setMediaNeedsChange(change);
340 }
341 else if ( type == QLatin1Char('X') )
342 {
343 setFilter(stream.readLine()); // include white space
344 }
345 else if ( type == QLatin1Char('x') )
346 {
347 dirFilters.append(QRegExp(stream.readLine(), Qt::CaseSensitive, QRegExp::Wildcard));
348 }
349 else if ( type == QLatin1Char('Z') )
350 {
351 int compress;
352 stream >> compress;
353 setCompressFiles(compress);
354 }
355 else if ( type == QLatin1Char('I') )
356 {
357 includes.append(stream.readLine());
358 }
359 else if ( type == QLatin1Char('E') )
360 {
361 excludes.append(stream.readLine());
362 }
363 else
364 stream.readLine(); // skip unknown key and rest of line
365 }
366
367 file.close();
368
369 setTarget(QUrl::fromUserInput(target));
370
371 setIncrementalBackup(
372 (fullBackupInterval > 1) && lastFullBackup.isValid() &&
373 (lastFullBackup.daysTo(QDateTime::currentDateTime()) < fullBackupInterval));
374
375 return true;
376 }
377
378 //--------------------------------------------------------------------------------
379
saveProfile(const QString & fileName,const QStringList & includes,const QStringList & excludes,QString & error)380 bool Archiver::saveProfile(const QString &fileName, const QStringList &includes, const QStringList &excludes, QString &error)
381 {
382 QFile file(fileName);
383
384 if ( ! file.open(QIODevice::WriteOnly) )
385 {
386 error = file.errorString();
387 return false;
388 }
389
390 QTextStream stream(&file);
391
392 stream << "M " << targetURL.toString(QUrl::PreferLocalFile) << QLatin1Char('\n');
393 stream << "P " << getFilePrefix() << QLatin1Char('\n');
394 stream << "S " << getMaxSliceMBs() << QLatin1Char('\n');
395 stream << "R " << getKeptBackups() << QLatin1Char('\n');
396 stream << "F " << getFullBackupInterval() << QLatin1Char('\n');
397
398 if ( getLastFullBackup().isValid() )
399 stream << "L " << getLastFullBackup().toString(Qt::ISODate) << QLatin1Char('\n');
400
401 if ( getLastBackup().isValid() )
402 stream << "B " << getLastBackup().toString(Qt::ISODate) << QLatin1Char('\n');
403
404 stream << "C " << static_cast<int>(getMediaNeedsChange()) << QLatin1Char('\n');
405 stream << "Z " << static_cast<int>(getCompressFiles()) << QLatin1Char('\n');
406
407 if ( !filters.isEmpty() )
408 stream << "X " << getFilter() << QLatin1Char('\n');
409
410 for (const QRegExp &exp : std::as_const(dirFilters))
411 stream << "x " << exp.pattern() << QLatin1Char('\n');
412
413 for (const QString &str : includes)
414 stream << "I " << str << QLatin1Char('\n');
415
416 for (const QString &str : excludes)
417 stream << "E " << str << QLatin1Char('\n');
418
419 file.close();
420 return true;
421 }
422
423 //--------------------------------------------------------------------------------
424
createArchive(const QStringList & includes,const QStringList & excludes)425 bool Archiver::createArchive(const QStringList &includes, const QStringList &excludes)
426 {
427 if ( includes.isEmpty() )
428 {
429 Q_EMIT warning(i18n("Nothing selected for backup"));
430 return false;
431 }
432
433 if ( !targetURL.isValid() )
434 {
435 Q_EMIT warning(i18n("The target dir '%1' is not valid", targetURL.toString()));
436 return false;
437 }
438
439 // non-interactive mode only allows local targets as KIO needs $DISPLAY
440 if ( !interactive && !targetURL.isLocalFile() )
441 {
442 Q_EMIT warning(i18n("The target dir '%1' must be a local file system dir and no remote URL",
443 targetURL.toString()));
444 return false;
445 }
446
447 // check if the target dir exists and optionally create it
448 if ( targetURL.isLocalFile() )
449 {
450 QDir dir(targetURL.path());
451 if ( !dir.exists() )
452 {
453 if ( !interactive ||
454 (KMessageBox::warningYesNo(static_cast<QWidget*>(parent()),
455 i18n("The target directory '%1' does not exist.\n\n"
456 "Shall I create it?", dir.absolutePath())) == KMessageBox::Yes) )
457 {
458 if ( !dir.mkpath(QStringLiteral(".")) )
459 {
460 Q_EMIT warning(i18n("Could not create the target directory '%1'.\n"
461 "The operating system reports: %2", dir.absolutePath(), QString::fromLatin1(strerror(errno))));
462 return false;
463 }
464 }
465 else
466 {
467 Q_EMIT warning(i18n("The target dir does not exist"));
468 return false;
469 }
470 }
471 }
472
473 excludeDirs.clear();
474 excludeFiles.clear();
475
476 // build map for directories and files to be excluded for fast lookup
477 for (const QString &name : excludes)
478 {
479 QFileInfo info(name);
480
481 if ( !info.isSymLink() && info.isDir() )
482 excludeDirs.insert(name);
483 else
484 excludeFiles.insert(name);
485 }
486
487 baseName = QString();
488 sliceNum = 0;
489 totalBytes = 0;
490 totalFiles = 0;
491 filteredFiles = 0;
492 cancelled = false;
493 skippedFiles = false;
494 sliceList.clear();
495
496 QDateTime startTime = QDateTime::currentDateTime();
497
498 runs = true;
499 Q_EMIT inProgress(true);
500
501 QTimer runTimer;
502 if ( interactive ) // else we do not need to be interrupted during the backup
503 {
504 connect(&runTimer, &QTimer::timeout, this, &Archiver::updateElapsed);
505 runTimer.start(1000);
506 }
507 elapsed.start();
508
509 if ( ! getNextSlice() )
510 {
511 runs = false;
512 Q_EMIT inProgress(false);
513
514 return false;
515 }
516
517 for (QStringList::const_iterator it = includes.constBegin(); !cancelled && (it != includes.constEnd()); ++it)
518 {
519 QString entry = *it;
520
521 if ( (entry.length() > 1) && entry.endsWith(QLatin1Char('/')) )
522 entry.chop(1);
523
524 QFileInfo info(entry);
525
526 if ( !info.isSymLink() && info.isDir() )
527 {
528 QDir dir(info.absoluteFilePath());
529 addDirFiles(dir);
530 }
531 else
532 addFile(info.absoluteFilePath());
533 }
534
535 finishSlice();
536
537 // reduce the number of old backups to the defined number
538 if ( !cancelled && (numKeptBackups != UNLIMITED) )
539 {
540 Q_EMIT logging(i18n("...reducing number of kept archives to max. %1", numKeptBackups));
541
542 if ( !targetURL.isLocalFile() ) // KIO needs $DISPLAY; non-interactive only allowed for local targets
543 {
544 QPointer<KIO::ListJob> listJob;
545 listJob = KIO::listDir(targetURL, KIO::DefaultFlags, false);
546
547 connect(listJob.data(), &KIO::ListJob::entries,
548 this, &Archiver::slotListResult);
549
550 while ( listJob )
551 qApp->processEvents(QEventLoop::WaitForMoreEvents);
552 }
553 else // non-intercative. create UDSEntryList on our own
554 {
555 QDir dir(targetURL.path());
556 targetDirList.clear();
557 const auto entryList = dir.entryList();
558 for (const QString &fileName : entryList)
559 {
560 KIO::UDSEntry entry;
561 entry.fastInsert(KIO::UDSEntry::UDS_NAME, fileName);
562 targetDirList.append(entry);
563 }
564 jobResult = 0;
565 }
566
567 if ( jobResult == 0 )
568 {
569 std::sort(targetDirList.begin(), targetDirList.end(), Archiver::UDSlessThan);
570 QString prefix = filePrefix.isEmpty() ? QStringLiteral("backup_") : (filePrefix + QLatin1String("_"));
571
572 QString sliceName;
573 int num = 0;
574
575 for (const KIO::UDSEntry &entry : std::as_const(targetDirList))
576 {
577 QString entryName = entry.stringValue(KIO::UDSEntry::UDS_NAME);
578
579 if ( entryName.startsWith(prefix) && // only matching current profile
580 entryName.endsWith(QLatin1String(".tar")) ) // just to be sure
581 {
582 if ( (num < numKeptBackups) &&
583 (sliceName.isEmpty() ||
584 !entryName.startsWith(sliceName)) ) // whenever a new backup set (different time) is found
585 {
586 sliceName = entryName.left(prefix.length() + strlen("yyyy.MM.dd-hh.mm.ss_"));
587 if ( !entryName.endsWith(QLatin1String("_inc.tar")) ) // do not count partial (differential) backup files
588 num++;
589 if ( num == numKeptBackups ) num++; // from here on delete all others
590 }
591
592 if ( (num > numKeptBackups) && // delete all other files
593 !entryName.startsWith(sliceName) ) // keep complete last matching archive set
594 {
595 QUrl url = targetURL;
596 url = url.adjusted(QUrl::StripTrailingSlash);
597 url.setPath(url.path() + QLatin1Char('/') + entryName);
598 Q_EMIT logging(i18n("...deleting %1", entryName));
599
600 // delete the file using KIO
601 if ( !targetURL.isLocalFile() ) // KIO needs $DISPLAY; non-interactive only allowed for local targets
602 {
603 QPointer<KIO::SimpleJob> delJob;
604 delJob = KIO::file_delete(url, KIO::DefaultFlags);
605
606 connect(delJob.data(), &KJob::result, this, &Archiver::slotResult);
607
608 while ( delJob )
609 qApp->processEvents(QEventLoop::WaitForMoreEvents);
610 }
611 else
612 {
613 QDir dir(targetURL.path());
614 dir.remove(entryName);
615 }
616 }
617 }
618 }
619 }
620 else
621 {
622 Q_EMIT warning(i18n("fetching directory listing of target failed. Can not reduce kept archives."));
623 }
624 }
625
626 runs = false;
627 Q_EMIT inProgress(false);
628 runTimer.stop();
629 updateElapsed(); // to catch the last partly second
630
631 if ( !cancelled )
632 {
633 lastBackup = startTime;
634 if ( !isIncrementalBackup() )
635 {
636 lastFullBackup = lastBackup;
637 setIncrementalBackup(fullBackupInterval > 1); // after a full backup, the next will be incremental
638 }
639
640 if ( (fullBackupInterval > 1) && !loadedProfile.isEmpty() )
641 {
642 QString error;
643 if ( !saveProfile(loadedProfile, includes, excludes, error) )
644 {
645 Q_EMIT warning(i18n("Could not write backup timestamps into profile %1: %2", loadedProfile, error));
646 }
647 }
648
649 Q_EMIT logging(i18n("-- Filtered Files: %1", filteredFiles));
650
651 if ( skippedFiles )
652 Q_EMIT logging(i18n("!! Backup finished <b>but files were skipped</b> !!"));
653 else
654 Q_EMIT logging(i18n("-- Backup successfully finished --"));
655
656 if ( interactive )
657 {
658 int ret = KMessageBox::questionYesNoList(static_cast<QWidget*>(parent()),
659 skippedFiles ?
660 i18n("The backup has finished but files were skipped.\n"
661 "What do you want to do now?") :
662 i18n("The backup has finished successfully.\n"
663 "What do you want to do now?"),
664 sliceList,
665 QString(),
666 KStandardGuiItem::cont(), KStandardGuiItem::quit(),
667 QStringLiteral("showDoneInfo"));
668
669 if ( ret == KMessageBox::No ) // quit
670 qApp->quit();
671 }
672 else
673 {
674 std::cerr << "-------" << std::endl;
675 for (const QString &slice : std::as_const(sliceList)) {
676 std::cerr << slice.toUtf8().constData() << std::endl;
677 }
678 std::cerr << "-------" << std::endl;
679
680 std::cerr << i18n("Totals: Files: %1, Size: %2, Duration: %3",
681 totalFiles,
682 KIO::convertSize(totalBytes),
683 QTime(0, 0).addMSecs(elapsed.elapsed()).toString(QStringLiteral("HH:mm:ss")))
684 .toUtf8().constData() << std::endl;
685 }
686
687 return true;
688 }
689 else
690 {
691 Q_EMIT logging(i18n("...Backup aborted!"));
692 return false;
693 }
694 }
695
696 //--------------------------------------------------------------------------------
697
cancel()698 void Archiver::cancel()
699 {
700 if ( !runs ) return;
701
702 if ( job )
703 {
704 job->kill();
705 job = nullptr;
706 }
707 if ( !cancelled )
708 {
709 cancelled = true;
710
711 if ( archive )
712 {
713 archive->close(); // else I can not remove the file - don't know why
714 delete archive;
715 archive = nullptr;
716 }
717
718 QFile(archiveName).remove(); // remove the unfinished tar file (which is now corrupted)
719 Q_EMIT warning(i18n("Backup cancelled"));
720 }
721 }
722
723 //--------------------------------------------------------------------------------
724
finishSlice()725 void Archiver::finishSlice()
726 {
727 if ( archive )
728 archive->close();
729
730 if ( ! cancelled )
731 {
732 runScript(QStringLiteral("slice_closed"));
733
734 if ( targetURL.isLocalFile() )
735 {
736 Q_EMIT logging(i18n("...finished slice %1", archiveName));
737 sliceList << archiveName; // store name for display at the end
738 }
739 else
740 {
741 QUrl source = QUrl::fromLocalFile(archiveName);
742 QUrl target = targetURL;
743
744 while ( true )
745 {
746 // copy to have the archive for the script later down
747 job = KIO::copy(source, target, KIO::DefaultFlags);
748
749 connect(job.data(), &KJob::result, this, &Archiver::slotResult);
750
751 Q_EMIT logging(i18n("...uploading archive %1 to %2", source.fileName(), target.toString()));
752
753 while ( job )
754 qApp->processEvents(QEventLoop::WaitForMoreEvents);
755
756 if ( jobResult == 0 )
757 {
758 target = target.adjusted(QUrl::StripTrailingSlash);
759 target.setPath(target.path() + QLatin1Char('/') + source.fileName());
760 sliceList << target.toLocalFile(); // store name for display at the end
761 break;
762 }
763 else
764 {
765 enum { ASK, CANCEL, RETRY } action = ASK;
766 while ( action == ASK )
767 {
768 int ret = KMessageBox::warningYesNoCancel(static_cast<QWidget*>(parent()),
769 i18n("How shall we proceed with the upload?"), i18n("Upload Failed"),
770 KGuiItem(i18n("Retry")), KGuiItem(i18n("Change Target")));
771
772 if ( ret == KMessageBox::Cancel )
773 {
774 action = CANCEL;
775 break;
776 }
777 else if ( ret == KMessageBox::No ) // change target
778 {
779 target = QFileDialog::getExistingDirectoryUrl(static_cast<QWidget*>(parent()));
780 if ( target.isEmpty() )
781 action = ASK;
782 else
783 action = RETRY;
784 }
785 else
786 action = RETRY;
787 }
788
789 if ( action == CANCEL )
790 break;
791 }
792 }
793
794 if ( jobResult != 0 )
795 cancel();
796 }
797 }
798
799 if ( ! cancelled )
800 runScript(QStringLiteral("slice_finished"));
801
802 if ( !targetURL.isLocalFile() )
803 QFile(archiveName).remove(); // remove the tmp file
804
805 delete archive;
806 archive = nullptr;
807 }
808
809 //--------------------------------------------------------------------------------
810
slotResult(KJob * theJob)811 void Archiver::slotResult(KJob *theJob)
812 {
813 if ( (jobResult = theJob->error()) )
814 {
815 theJob->uiDelegate()->showErrorMessage();
816
817 Q_EMIT warning(theJob->errorString());
818 }
819 }
820
821 //--------------------------------------------------------------------------------
822
slotListResult(KIO::Job * theJob,const KIO::UDSEntryList & entries)823 void Archiver::slotListResult(KIO::Job *theJob, const KIO::UDSEntryList &entries)
824 {
825 if ( (jobResult = theJob->error()) )
826 {
827 theJob->uiDelegate()->showErrorMessage();
828
829 Q_EMIT warning(theJob->errorString());
830 }
831
832 targetDirList = entries;
833 }
834
835 //--------------------------------------------------------------------------------
836
runScript(const QString & mode)837 void Archiver::runScript(const QString &mode)
838 {
839 // do some extra action via external script (program)
840 if ( sliceScript.length() )
841 {
842 QString mountPoint;
843 if ( targetURL.isLocalFile() )
844 {
845 KMountPoint::Ptr ptr = KMountPoint::currentMountPoints().findByPath(targetURL.path());
846 if ( ptr )
847 mountPoint = ptr->mountPoint();
848 }
849
850 KProcess proc;
851 proc << sliceScript
852 << mode
853 << archiveName
854 << targetURL.toString(QUrl::PreferLocalFile)
855 << mountPoint;
856
857 connect(&proc, &KProcess::readyReadStandardOutput,
858 this, &Archiver::receivedOutput);
859
860 proc.setOutputChannelMode(KProcess::MergedChannels);
861
862 if ( proc.execute() == -2 )
863 {
864 QString message = i18n("The script '%1' could not be started.", sliceScript);
865 if ( interactive )
866 KMessageBox::error(static_cast<QWidget*>(parent()), message);
867 else
868 Q_EMIT warning(message);
869 }
870 }
871 }
872
873 //--------------------------------------------------------------------------------
874
receivedOutput()875 void Archiver::receivedOutput()
876 {
877 KProcess *proc = static_cast<KProcess*>(sender());
878
879 QByteArray buffer = proc->readAllStandardOutput();
880
881 QString msg = QString::fromUtf8(buffer);
882 if ( msg.endsWith(QLatin1Char('\n')) )
883 msg.chop(1);
884
885 Q_EMIT warning(msg);
886 }
887
888 //--------------------------------------------------------------------------------
889
getNextSlice()890 bool Archiver::getNextSlice()
891 {
892 sliceNum++;
893
894 if ( archive )
895 {
896 Q_EMIT sliceProgress(100);
897
898 finishSlice();
899 if ( cancelled ) return false;
900
901 if ( interactive && mediaNeedsChange &&
902 KMessageBox::warningContinueCancel(static_cast<QWidget*>(parent()),
903 i18n("The medium is full. Please insert medium Nr. %1", sliceNum)) ==
904 KMessageBox::Cancel )
905 {
906 cancel();
907 return false;
908 }
909 }
910
911 Q_EMIT newSlice(sliceNum);
912
913 if ( baseName.isEmpty() )
914 {
915 QString prefix = filePrefix.isEmpty() ? QStringLiteral("backup") : filePrefix;
916
917 if ( targetURL.isLocalFile() )
918 baseName = targetURL.path() + QLatin1Char('/') + prefix + QDateTime::currentDateTime().toString(QStringLiteral("_yyyy.MM.dd-hh.mm.ss"));
919 else
920 baseName = QDir::tempPath() + QLatin1Char('/') + prefix + QDateTime::currentDateTime().toString(QStringLiteral("_yyyy.MM.dd-hh.mm.ss"));
921 }
922
923 archiveName = baseName + QStringLiteral("_%1").arg(sliceNum);
924 if ( isIncrementalBackup() )
925 archiveName += QStringLiteral("_inc.tar"); // mark the file as being not a full backup
926 else
927 archiveName += QStringLiteral(".tar");
928
929 runScript(QStringLiteral("slice_init"));
930
931 calculateCapacity();
932
933 // don't create a bz2 compressed file as we compress each file on its own
934 archive = new KTar(archiveName, QStringLiteral("application/x-tar"));
935
936 while ( (sliceCapacity < 1024) || !archive->open(QIODevice::WriteOnly) ) // disk full ?
937 {
938 if ( !interactive )
939 Q_EMIT warning(i18n("The file '%1' can not be opened for writing.", archiveName));
940
941 if ( !interactive ||
942 (KMessageBox::warningYesNo(static_cast<QWidget*>(parent()),
943 i18n("The file '%1' can not be opened for writing.\n\n"
944 "Do you want to retry?", archiveName)) == KMessageBox::No) )
945 {
946 delete archive;
947 archive = nullptr;
948
949 cancel();
950 return false;
951 }
952 calculateCapacity(); // try again; maybe the user freed up some space
953 }
954
955 return true;
956 }
957
958 //--------------------------------------------------------------------------------
959
emitArchiveError()960 void Archiver::emitArchiveError()
961 {
962 QString err;
963
964 if ( archive->device() )
965 err = archive->device()->errorString();
966
967 if ( err.isEmpty() )
968 {
969 Q_EMIT warning(i18n("Could not write to archive. Maybe the medium is full."));
970 }
971 else
972 {
973 Q_EMIT warning(i18n("Could not write to archive.\n"
974 "The operating system reports: %1", err));
975 }
976 }
977
978 //--------------------------------------------------------------------------------
979
addDirFiles(QDir & dir)980 void Archiver::addDirFiles(QDir &dir)
981 {
982 QString absolutePath = dir.absolutePath();
983
984 if ( excludeDirs.contains(absolutePath) )
985 return;
986
987 for (const QRegExp &exp : std::as_const(dirFilters))
988 {
989 if ( exp.exactMatch(absolutePath) )
990 {
991 if ( interactive || verbose )
992 Q_EMIT logging(i18n("...skipping filtered directory %1", absolutePath));
993
994 return;
995 }
996 }
997
998 // add the dir itself
999 struct stat status;
1000 memset(&status, 0, sizeof(status));
1001 if ( ::stat(QFile::encodeName(absolutePath).constData(), &status) == -1 )
1002 {
1003 Q_EMIT warning(i18n("Could not get information of directory: %1\n"
1004 "The operating system reports: %2",
1005 absolutePath,
1006 QString::fromLatin1(strerror(errno))));
1007 return;
1008 }
1009 QFileInfo dirInfo(absolutePath);
1010
1011 if ( ! dirInfo.isReadable() )
1012 {
1013 Q_EMIT warning(i18n("Directory '%1' is not readable. Skipping.", absolutePath));
1014 skippedFiles = true;
1015 return;
1016 }
1017
1018 totalFiles++;
1019 Q_EMIT totalFilesChanged(totalFiles);
1020 if ( interactive || verbose )
1021 Q_EMIT logging(absolutePath);
1022
1023 qApp->processEvents(QEventLoop::AllEvents, 5);
1024 if ( cancelled ) return;
1025
1026 if ( ! archive->writeDir(QStringLiteral(".") + absolutePath, dirInfo.owner(), dirInfo.group(),
1027 status.st_mode, dirInfo.lastRead(), dirInfo.lastModified(), dirInfo.birthTime()) )
1028 {
1029 Q_EMIT warning(i18n("Could not write directory '%1' to archive.\n"
1030 "Maybe the medium is full.", absolutePath));
1031 return;
1032 }
1033
1034 dir.setFilter(QDir::AllEntries | QDir::Hidden | QDir::System | QDir::NoDotAndDotDot);
1035
1036 const QFileInfoList list = dir.entryInfoList();
1037
1038 for (int i = 0; !cancelled && (i < list.count()); i++)
1039 {
1040 if ( !list[i].isSymLink() && list[i].isDir() )
1041 {
1042 QDir dir(list[i].absoluteFilePath());
1043 addDirFiles(dir);
1044 }
1045 else
1046 addFile(list[i].absoluteFilePath());
1047 }
1048 }
1049
1050 //--------------------------------------------------------------------------------
1051
fileIsFiltered(const QString & fileName) const1052 bool Archiver::fileIsFiltered(const QString &fileName) const
1053 {
1054 for (const QRegExp &exp : std::as_const(filters))
1055 if ( exp.exactMatch(fileName) )
1056 return true;
1057
1058 return false;
1059 }
1060
1061 //--------------------------------------------------------------------------------
1062
addFile(const QFileInfo & info)1063 void Archiver::addFile(const QFileInfo &info)
1064 {
1065 if ( (isIncrementalBackup() && (info.lastModified() < lastBackup)) ||
1066 fileIsFiltered(info.fileName()) )
1067 {
1068 filteredFiles++;
1069 return;
1070 }
1071
1072 if ( excludeFiles.contains(info.absoluteFilePath()) )
1073 return;
1074
1075 // avoid including my own archive file
1076 // (QFileInfo to have correct path comparison even in case archiveName contains // etc.)
1077 // startsWith() is needed as KDE4 KTar does not create directly the .tar file but until it's closed
1078 // the file is named "...tarXXXX.new"
1079 if ( info.absoluteFilePath().startsWith(QFileInfo(archiveName).absoluteFilePath()) )
1080 return;
1081
1082 if ( cancelled ) return;
1083
1084 /* don't skip. We probably do not need to read it anyway, since it might be empty
1085 if ( ! info.isReadable() )
1086 {
1087 Q_EMIT warning(i18n("File '%1' is not readable. Skipping.").arg(info.absoluteFilePath()));
1088 skippedFiles = true;
1089 return;
1090 }
1091 */
1092
1093 // Q_EMIT before we do the compression, so that the receiver can already show
1094 // with which file we work
1095
1096 // show filename + size
1097 if ( interactive || verbose )
1098 Q_EMIT logging(info.absoluteFilePath() + QStringLiteral(" (%1)").arg(KIO::convertSize(info.size())));
1099
1100 qApp->processEvents(QEventLoop::AllEvents, 5);
1101 if ( cancelled ) return;
1102
1103 if ( info.isSymLink() )
1104 {
1105 archive->addLocalFile(info.absoluteFilePath(), QStringLiteral(".") + info.absoluteFilePath());
1106 totalFiles++;
1107 Q_EMIT totalFilesChanged(totalFiles);
1108 return;
1109 }
1110
1111 if ( !getCompressFiles() )
1112 {
1113 AddFileStatus ret = addLocalFile(info); // this also increases totalBytes
1114
1115 if ( ret == Error )
1116 {
1117 cancel(); // we must cancel as the tar-file is now corrupt (file was only partly written)
1118 return;
1119 }
1120 else if ( ret == Skipped )
1121 {
1122 skippedFiles = true;
1123 return;
1124 }
1125 }
1126 else // add the file compressed
1127 {
1128 // as we can't know which size the file will have after compression,
1129 // we create a compressed file and put this into the archive
1130 QTemporaryFile tmpFile;
1131
1132 if ( ! compressFile(info.absoluteFilePath(), tmpFile) || cancelled )
1133 return;
1134
1135 // here we have the compressed file in tmpFile
1136
1137 tmpFile.open(); // size() only works if open
1138
1139 if ( (sliceBytes + tmpFile.size()) > sliceCapacity )
1140 if ( ! getNextSlice() ) return;
1141
1142 // to be able to create the exact same metadata (permission, date, owner) we need
1143 // to fill the file into the archive with the following:
1144 {
1145 struct stat status;
1146 memset(&status, 0, sizeof(status));
1147
1148 if ( ::stat(QFile::encodeName(info.absoluteFilePath()).constData(), &status) == -1 )
1149 {
1150 Q_EMIT warning(i18n("Could not get information of file: %1\n"
1151 "The operating system reports: %2",
1152 info.absoluteFilePath(),
1153 QString::fromLatin1(strerror(errno))));
1154
1155 skippedFiles = true;
1156 return;
1157 }
1158
1159 if ( ! archive->prepareWriting(QStringLiteral(".") + info.absoluteFilePath() + ext,
1160 info.owner(), info.group(), tmpFile.size(),
1161 status.st_mode, info.lastRead(), info.lastModified(), info.birthTime()) )
1162 {
1163 emitArchiveError();
1164 cancel();
1165 return;
1166 }
1167
1168 const int BUFFER_SIZE = 8*1024;
1169 static char buffer[BUFFER_SIZE];
1170 qint64 len;
1171 int count = 0;
1172 while ( ! tmpFile.atEnd() )
1173 {
1174 len = tmpFile.read(buffer, BUFFER_SIZE);
1175
1176 if ( len < 0 ) // error in reading
1177 {
1178 Q_EMIT warning(i18n("Could not read from file '%1'\n"
1179 "The operating system reports: %2",
1180 info.absoluteFilePath(),
1181 tmpFile.errorString()));
1182 cancel();
1183 return;
1184 }
1185
1186 if ( ! archive->writeData(buffer, len) )
1187 {
1188 emitArchiveError();
1189 cancel();
1190 return;
1191 }
1192
1193 count = (count + 1) % 50;
1194 if ( count == 0 )
1195 {
1196 qApp->processEvents(QEventLoop::AllEvents, 5);
1197 if ( cancelled ) return;
1198 }
1199 }
1200 if ( ! archive->finishWriting(tmpFile.size()) )
1201 {
1202 emitArchiveError();
1203 cancel();
1204 return;
1205 }
1206 }
1207
1208 // get filesize
1209 sliceBytes = archive->device()->pos(); // account for tar overhead
1210 totalBytes += tmpFile.size();
1211
1212 Q_EMIT sliceProgress(static_cast<int>(sliceBytes * 100 / sliceCapacity));
1213 }
1214
1215 totalFiles++;
1216 Q_EMIT totalFilesChanged(totalFiles);
1217 Q_EMIT totalBytesChanged(totalBytes);
1218
1219 qApp->processEvents(QEventLoop::AllEvents, 5);
1220 }
1221
1222 //--------------------------------------------------------------------------------
1223
addLocalFile(const QFileInfo & info)1224 Archiver::AddFileStatus Archiver::addLocalFile(const QFileInfo &info)
1225 {
1226 struct stat sourceStat;
1227 memset(&sourceStat, 0, sizeof(sourceStat));
1228
1229 if ( ::stat(QFile::encodeName(info.absoluteFilePath()).constData(), &sourceStat) == -1 )
1230 {
1231 Q_EMIT warning(i18n("Could not get information of file: %1\n"
1232 "The operating system reports: %2",
1233 info.absoluteFilePath(),
1234 QString::fromLatin1(strerror(errno))));
1235
1236 return Skipped;
1237 }
1238
1239 QFile sourceFile(info.absoluteFilePath());
1240
1241 // if the size is 0 (e.g. a pipe), don't open it since we will not read any content
1242 // and Qt hangs when opening a pipe
1243 if ( (info.size() > 0) && !sourceFile.open(QIODevice::ReadOnly) )
1244 {
1245 Q_EMIT warning(i18n("Could not open file '%1' for reading.", info.absoluteFilePath()));
1246 return Skipped;
1247 }
1248
1249 if ( (sliceBytes + info.size()) > sliceCapacity )
1250 if ( ! getNextSlice() ) return Error;
1251
1252 if ( ! archive->prepareWriting(QStringLiteral(".") + info.absoluteFilePath(),
1253 info.owner(), info.group(), info.size(),
1254 sourceStat.st_mode, info.lastRead(), info.lastModified(), info.birthTime()) )
1255 {
1256 emitArchiveError();
1257 return Error;
1258 }
1259
1260 const int BUFFER_SIZE = 8*1024;
1261 static char buffer[BUFFER_SIZE];
1262 qint64 len;
1263 int count = 0, progress;
1264 QElapsedTimer timer;
1265 timer.start();
1266 bool msgShown = false;
1267 qint64 written = 0;
1268
1269 while ( info.size() && !sourceFile.atEnd() && !cancelled )
1270 {
1271 len = sourceFile.read(buffer, BUFFER_SIZE);
1272
1273 if ( len < 0 ) // error in reading
1274 {
1275 Q_EMIT warning(i18n("Could not read from file '%1'\n"
1276 "The operating system reports: %2",
1277 info.absoluteFilePath(),
1278 sourceFile.errorString()));
1279 return Error;
1280 }
1281
1282 if ( ! archive->writeData(buffer, len) )
1283 {
1284 emitArchiveError();
1285 return Error;
1286 }
1287
1288 totalBytes += len;
1289 written += len;
1290
1291 progress = static_cast<int>(written * 100 / info.size());
1292
1293 // stay responsive
1294 count = (count + 1) % 50;
1295 if ( count == 0 )
1296 {
1297 if ( msgShown )
1298 Q_EMIT fileProgress(progress);
1299
1300 Q_EMIT totalBytesChanged(totalBytes);
1301 qApp->processEvents(QEventLoop::AllEvents, 5);
1302 }
1303
1304 if ( !msgShown && (timer.elapsed() > 3000) && (progress < 50) )
1305 {
1306 Q_EMIT fileProgress(progress);
1307 if ( interactive || verbose )
1308 Q_EMIT logging(i18n("...archiving file %1", info.absoluteFilePath()));
1309
1310 if ( interactive )
1311 QApplication::setOverrideCursor(QCursor(Qt::BusyCursor));
1312 qApp->processEvents(QEventLoop::AllEvents, 5);
1313 msgShown = true;
1314 }
1315 }
1316 Q_EMIT fileProgress(100);
1317 sourceFile.close();
1318
1319 if ( !cancelled )
1320 {
1321 // get filesize
1322 sliceBytes = archive->device()->pos(); // account for tar overhead
1323
1324 Q_EMIT sliceProgress(static_cast<int>(sliceBytes * 100 / sliceCapacity));
1325 }
1326
1327 if ( msgShown && interactive )
1328 QApplication::restoreOverrideCursor();
1329
1330 if ( !cancelled && !archive->finishWriting(info.size()) )
1331 {
1332 emitArchiveError();
1333 return Error;
1334 }
1335
1336 return cancelled ? Error : Added;
1337 }
1338
1339 //--------------------------------------------------------------------------------
1340
compressFile(const QString & origName,QFile & comprFile)1341 bool Archiver::compressFile(const QString &origName, QFile &comprFile)
1342 {
1343 QFile origFile(origName);
1344 if ( ! origFile.open(QIODevice::ReadOnly) )
1345 {
1346 Q_EMIT warning(i18n("Could not read file: %1\n"
1347 "The operating system reports: %2",
1348 origName,
1349 origFile.errorString()));
1350
1351 skippedFiles = true;
1352 return false;
1353 }
1354 else
1355 {
1356 KCompressionDevice filter(&comprFile, false, compressionType);
1357
1358 if ( !filter.open(QIODevice::WriteOnly) )
1359 {
1360 Q_EMIT warning(i18n("Could not create temporary file for compressing: %1\n"
1361 "The operating system reports: %2",
1362 origName,
1363 filter.errorString()));
1364 return false;
1365 }
1366
1367 const int BUFFER_SIZE = 8*1024;
1368 static char buffer[BUFFER_SIZE];
1369 qint64 len;
1370 int count = 0, progress;
1371 QElapsedTimer timer;
1372 timer.start();
1373 bool msgShown = false;
1374
1375 KIO::filesize_t fileSize = origFile.size();
1376 KIO::filesize_t written = 0;
1377
1378 while ( fileSize && !origFile.atEnd() && !cancelled )
1379 {
1380 len = origFile.read(buffer, BUFFER_SIZE);
1381 qint64 wrote = filter.write(buffer, len);
1382
1383 if ( len != wrote )
1384 {
1385 Q_EMIT warning(i18n("Could not write to temporary file"));
1386 return false;
1387 }
1388
1389 written += len;
1390
1391 progress = static_cast<int>(written * 100 / fileSize);
1392
1393 // keep the ui responsive
1394 count = (count + 1) % 50;
1395 if ( count == 0 )
1396 {
1397 if ( msgShown )
1398 Q_EMIT fileProgress(progress);
1399
1400 qApp->processEvents(QEventLoop::AllEvents, 5);
1401 }
1402
1403 if ( !msgShown && (timer.elapsed() > 3000) && (progress < 50) )
1404 {
1405 Q_EMIT fileProgress(progress);
1406 Q_EMIT logging(i18n("...compressing file %1", origName));
1407 if ( interactive )
1408 QApplication::setOverrideCursor(QCursor(Qt::BusyCursor));
1409 qApp->processEvents(QEventLoop::AllEvents, 5);
1410 msgShown = true;
1411 }
1412 }
1413 Q_EMIT fileProgress(100);
1414 origFile.close();
1415
1416 if ( msgShown && interactive )
1417 QApplication::restoreOverrideCursor();
1418 }
1419
1420 return true;
1421 }
1422
1423 //--------------------------------------------------------------------------------
1424
getDiskFree(const QString & path,KIO::filesize_t & capacityB,KIO::filesize_t & freeB)1425 bool Archiver::getDiskFree(const QString &path, KIO::filesize_t &capacityB, KIO::filesize_t &freeB)
1426 {
1427 struct statvfs vfs;
1428 memset(&vfs, 0, sizeof(vfs));
1429
1430 if ( ::statvfs(QFile::encodeName(path).constData(), &vfs) == -1 )
1431 return false;
1432
1433 capacityB = static_cast<KIO::filesize_t>(vfs.f_blocks) * static_cast<KIO::filesize_t>(vfs.f_frsize);
1434 freeB = static_cast<KIO::filesize_t>(vfs.f_bavail) * static_cast<KIO::filesize_t>(vfs.f_frsize);
1435
1436 return true;
1437 }
1438
1439 //--------------------------------------------------------------------------------
1440
loggingSlot(const QString & message)1441 void Archiver::loggingSlot(const QString &message)
1442 {
1443 std::cerr << message.toUtf8().constData() << std::endl;
1444 }
1445
1446 //--------------------------------------------------------------------------------
1447
warningSlot(const QString & message)1448 void Archiver::warningSlot(const QString &message)
1449 {
1450 std::cerr << i18n("WARNING:").toUtf8().constData() << message.toUtf8().constData() << std::endl;
1451 }
1452
1453 //--------------------------------------------------------------------------------
1454
updateElapsed()1455 void Archiver::updateElapsed()
1456 {
1457 Q_EMIT elapsedChanged(QTime(0, 0).addMSecs(elapsed.elapsed()));
1458 }
1459
1460 //--------------------------------------------------------------------------------
1461 // sort by name of entries in descending order (younger names are first)
1462
UDSlessThan(const KIO::UDSEntry & left,const KIO::UDSEntry & right)1463 bool Archiver::UDSlessThan(const KIO::UDSEntry &left, const KIO::UDSEntry &right)
1464 {
1465 return left.stringValue(KIO::UDSEntry::UDS_NAME) > right.stringValue(KIO::UDSEntry::UDS_NAME);
1466 }
1467
1468 //--------------------------------------------------------------------------------
1469