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 &reg : 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 &reg : 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