1 #include "taskmanager.h"
2 
3 #include "debug.h"
4 #include "utils.h"
5 
6 #include <QDir>
7 #include <QFileInfo>
8 #include <QSettings>
9 #include <QTimer>
10 
11 #define SUCCESS 0
12 #define SCHEDULED_JOBS_SLEEP 3
13 
TaskManager()14 TaskManager::TaskManager() : _threadPool(QThreadPool::globalInstance())
15 {
16 }
17 
~TaskManager()18 TaskManager::~TaskManager()
19 {
20 }
21 
getTarsnapVersion(QString tarsnapPath)22 void TaskManager::getTarsnapVersion(QString tarsnapPath)
23 {
24     TarsnapTask *tarsnap = new TarsnapTask();
25     if(tarsnapPath.isEmpty())
26         tarsnap->setCommand(CMD_TARSNAP);
27     else
28         tarsnap->setCommand(tarsnapPath + QDir::separator() + CMD_TARSNAP);
29     tarsnap->setArguments(QStringList("--version"));
30     connect(tarsnap, &TarsnapTask::finished, this,
31             &TaskManager::getTarsnapVersionFinished, QUEUED);
32     queueTask(tarsnap);
33 }
34 
registerMachine(QString user,QString password,QString machine,QString key,QString tarsnapPath,QString cachePath)35 void TaskManager::registerMachine(QString user, QString password,
36                                   QString machine, QString key,
37                                   QString tarsnapPath, QString cachePath)
38 {
39     TarsnapTask *registerTask = new TarsnapTask();
40     QStringList  args;
41     if(QFileInfo(key).exists())
42     {
43         // existing key, attempt to rebuild cache & verify archive integrity
44         args << "--fsck-prune"
45              << "--keyfile" << key << "--cachedir" << cachePath;
46         registerTask->setCommand(tarsnapPath + QDir::separator() + CMD_TARSNAP);
47         registerTask->setArguments(args);
48     }
49     else
50     {
51         // generate a new key and register machine with tarsnap-keygen
52         args << "--user" << user << "--machine" << machine << "--keyfile" << key;
53         registerTask->setCommand(tarsnapPath + QDir::separator()
54                                  + CMD_TARSNAPKEYGEN);
55         registerTask->setArguments(args);
56         registerTask->setStdIn(password);
57     }
58     connect(registerTask, &TarsnapTask::finished, this,
59             &TaskManager::registerMachineFinished, QUEUED);
60     queueTask(registerTask);
61 }
62 
backupNow(BackupTaskPtr backupTask)63 void TaskManager::backupNow(BackupTaskPtr backupTask)
64 {
65     if(backupTask == nullptr)
66     {
67         DEBUG << "Null BackupTaskPtr passed.";
68         return;
69     }
70 
71     _backupTaskMap[backupTask->uuid()] = backupTask;
72     TarsnapTask *bTask = new TarsnapTask();
73     QStringList  args;
74     initTarsnapArgs(args);
75     QSettings settings;
76     if(settings.value("tarsnap/aggressive_networking",
77                       DEFAULT_AGGRESSIVE_NETWORKING).toBool())
78         args << "--aggressive-networking";
79     if(backupTask->optionDryRun())
80         args << "--dry-run";
81     if(backupTask->optionSkipNoDump())
82         args << "--nodump";
83     if(backupTask->optionPreservePaths())
84         args << "-P";
85     if(!backupTask->optionTraverseMount())
86         args << "--one-file-system";
87     if(backupTask->optionFollowSymLinks())
88         args << "-L";
89     if(Utils::tarsnapVersionMinimum("1.0.36"))
90         args << "--creationtime"
91              << QString::number(backupTask->timestamp().toTime_t());
92     args << "--quiet"
93          << "--print-stats"
94          << "--no-humanize-numbers"
95          << "-c"
96          << "-f" << backupTask->name();
97     foreach(QString exclude, backupTask->getExcludesList())
98     {
99         args << "--exclude" << exclude;
100     }
101     foreach(QUrl url, backupTask->urls())
102     {
103         args << url.toLocalFile();
104     }
105     bTask->setCommand(makeTarsnapCommand(CMD_TARSNAP));
106     bTask->setArguments(args);
107     backupTask->setCommand(bTask->command() + " " + bTask->arguments().join(" "));
108     bTask->setData(backupTask->uuid());
109     connect(bTask, &TarsnapTask::finished, this,
110             &TaskManager::backupTaskFinished, QUEUED);
111     connect(bTask, &TarsnapTask::started, this, &TaskManager::backupTaskStarted,
112             QUEUED);
113     connect(backupTask.data(), &BackupTask::statusUpdate, this,
114             &TaskManager::notifyBackupTaskUpdate, QUEUED);
115     backupTask->setStatus(TaskStatus::Queued);
116     queueTask(bTask, true);
117 }
118 
getArchives()119 void TaskManager::getArchives()
120 {
121     TarsnapTask *listArchivesTask = new TarsnapTask();
122     QStringList  args;
123     initTarsnapArgs(args);
124     args << "--list-archives" << "-vv";
125     listArchivesTask->setCommand(makeTarsnapCommand(CMD_TARSNAP));
126     listArchivesTask->setArguments(args);
127     listArchivesTask->setTruncateLogOutput(true);
128     connect(listArchivesTask, &TarsnapTask::finished, this,
129             &TaskManager::getArchiveListFinished, QUEUED);
130     connect(listArchivesTask, &TarsnapTask::started, this,
131             [&]() { emit message(tr("Updating archives list from remote...")); },
132             QUEUED);
133     queueTask(listArchivesTask);
134 }
135 
loadArchives()136 void TaskManager::loadArchives()
137 {
138     _archiveMap.clear();
139     PersistentStore &store = PersistentStore::instance();
140     if(!store.initialized())
141     {
142         DEBUG << "PersistentStore was not initialized properly.";
143         return;
144     }
145     QSqlQuery query = store.createQuery();
146     if(!query.prepare(QLatin1String("select name from archives")))
147     {
148         DEBUG << query.lastError().text();
149         return;
150     }
151     if(store.runQuery(query) && query.next())
152     {
153         do
154         {
155             ArchivePtr archive(new Archive);
156             archive->setName(
157                 query.value(query.record().indexOf("name")).toString());
158             archive->load();
159             _archiveMap[archive->name()] = archive;
160         } while(query.next());
161     }
162     emit archiveList(_archiveMap.values());
163 }
164 
getArchiveStats(ArchivePtr archive)165 void TaskManager::getArchiveStats(ArchivePtr archive)
166 {
167     if(archive.isNull())
168     {
169         DEBUG << "Null ArchivePtr passed.";
170         return;
171     }
172 
173     TarsnapTask *statsTask = new TarsnapTask();
174     QStringList  args;
175     initTarsnapArgs(args);
176     args << "--print-stats"
177          << "--no-humanize-numbers"
178          << "-f" << archive->name();
179     statsTask->setCommand(makeTarsnapCommand(CMD_TARSNAP));
180     statsTask->setArguments(args);
181     statsTask->setData(QVariant::fromValue(archive));
182     connect(statsTask, &TarsnapTask::finished, this,
183             &TaskManager::getArchiveStatsFinished, QUEUED);
184     connect(statsTask, &TarsnapTask::started, this,
185             [=]() {
186                 emit message(tr("Fetching stats for archive <i>%1</i>...")
187                                  .arg(archive->name()));
188             },
189             QUEUED);
190     queueTask(statsTask);
191 }
192 
getArchiveContents(ArchivePtr archive)193 void TaskManager::getArchiveContents(ArchivePtr archive)
194 {
195     if(archive.isNull())
196     {
197         DEBUG << "Null ArchivePtr passed.";
198         return;
199     }
200 
201     TarsnapTask *contentsTask = new TarsnapTask();
202     QStringList  args;
203     initTarsnapArgs(args);
204     QSettings settings;
205     if(settings.value("tarsnap/preserve_pathnames",
206                       DEFAULT_PRESERVE_PATHNAMES).toBool())
207         args << "-P";
208     args << "-tv"
209          << "-f" << archive->name();
210     contentsTask->setCommand(makeTarsnapCommand(CMD_TARSNAP));
211     contentsTask->setArguments(args);
212     contentsTask->setData(QVariant::fromValue(archive));
213     contentsTask->setTruncateLogOutput(true);
214     connect(contentsTask, &TarsnapTask::finished, this,
215             &TaskManager::getArchiveContentsFinished, QUEUED);
216     connect(contentsTask, &TarsnapTask::started, this,
217             [=]() {
218                 emit message(tr("Fetching contents for archive <i>%1</i>...")
219                                  .arg(archive->name()));
220             },
221             QUEUED);
222     queueTask(contentsTask);
223 }
224 
deleteArchives(QList<ArchivePtr> archives)225 void TaskManager::deleteArchives(QList<ArchivePtr> archives)
226 {
227     if(archives.isEmpty())
228     {
229         DEBUG << "Empty QList<ArchivePtr> passed.";
230         return;
231     }
232 
233     foreach(ArchivePtr archive, archives)
234         archive->setDeleteScheduled(true);
235 
236     TarsnapTask *delArchives = new TarsnapTask();
237     QStringList  args;
238     initTarsnapArgs(args);
239     args << "--print-stats"
240          << "-d";
241     foreach(ArchivePtr archive, archives)
242     {
243         args << "-f" << archive->name();
244     }
245     delArchives->setCommand(makeTarsnapCommand(CMD_TARSNAP));
246     delArchives->setArguments(args);
247     delArchives->setData(QVariant::fromValue(archives));
248     connect(delArchives, &TarsnapTask::finished, this,
249             &TaskManager::deleteArchivesFinished, QUEUED);
250     connect(delArchives, &TarsnapTask::canceled, this,
251             [=](QVariant data) {
252                 QList<ArchivePtr> archives = data.value<QList<ArchivePtr>>();
253                 foreach(ArchivePtr archive, archives)
254                     archive->setDeleteScheduled(false);
255             },
256             QUEUED);
257     connect(delArchives, &TarsnapTask::started, this,
258             [=](QVariant data) {
259                 QList<ArchivePtr> archives = data.value<QList<ArchivePtr>>();
260                 notifyArchivesDeleted(archives, false);
261             },
262             QUEUED);
263     queueTask(delArchives, true);
264 }
265 
getOverallStats()266 void TaskManager::getOverallStats()
267 {
268     TarsnapTask *overallStats = new TarsnapTask();
269     QStringList  args;
270     initTarsnapArgs(args);
271     args << "--print-stats"
272          << "--no-humanize-numbers";
273     overallStats->setCommand(makeTarsnapCommand(CMD_TARSNAP));
274     overallStats->setArguments(args);
275     connect(overallStats, &TarsnapTask::finished, this,
276             &TaskManager::overallStatsFinished, QUEUED);
277     queueTask(overallStats);
278 }
279 
fsck(bool prune)280 void TaskManager::fsck(bool prune)
281 {
282     TarsnapTask *fsck = new TarsnapTask();
283     QStringList  args;
284     initTarsnapArgs(args);
285     if(prune)
286         args << "--fsck-prune";
287     else
288         args << "--fsck";
289     fsck->setCommand(makeTarsnapCommand(CMD_TARSNAP));
290     fsck->setArguments(args);
291     connect(fsck, &TarsnapTask::finished, this, &TaskManager::fsckFinished,
292             QUEUED);
293     connect(fsck, &TarsnapTask::started, this,
294             [=]() { emit message(tr("Cache repair initiated.")); }, QUEUED);
295     queueTask(fsck, true);
296 }
297 
nuke()298 void TaskManager::nuke()
299 {
300     TarsnapTask *nuke = new TarsnapTask();
301     QStringList  args;
302     initTarsnapArgs(args);
303     args << "--nuke";
304     nuke->setCommand(makeTarsnapCommand(CMD_TARSNAP));
305     nuke->setStdIn("No Tomorrow\n");
306     nuke->setArguments(args);
307     connect(nuke, &TarsnapTask::finished, this, &TaskManager::nukeFinished,
308             QUEUED);
309     connect(nuke, &TarsnapTask::started, this,
310             [=]() { emit message(tr("Archives nuke initiated...")); }, QUEUED);
311     queueTask(nuke, true);
312 }
313 
restoreArchive(ArchivePtr archive,ArchiveRestoreOptions options)314 void TaskManager::restoreArchive(ArchivePtr archive, ArchiveRestoreOptions options)
315 {
316     if(archive.isNull())
317     {
318         DEBUG << "Null ArchivePtr passed.";
319         return;
320     }
321 
322     TarsnapTask *restore = new TarsnapTask();
323     QStringList  args;
324     initTarsnapArgs(args);
325     if(options.optionRestore)
326     {
327         QSettings settings;
328         args << "-x"
329              << "-P"
330              << "-C"
331              << settings.value("app/downloads_dir", DEFAULT_DOWNLOADS).toString();
332     }
333     if(options.optionRestoreDir)
334         args << "-x"
335              << "-C" << options.path;
336     if((options.optionRestore || options.optionRestoreDir))
337     {
338         if(!options.overwriteFiles)
339             args << "-k";
340         if(options.keepNewerFiles)
341             args << "--keep-newer-files";
342         if(options.preservePerms)
343             args << "-p";
344     }
345     if(options.optionTarArchive)
346     {
347         args << "-r";
348         restore->setStdOutFile(options.path);
349     }
350     if(!options.files.isEmpty())
351     {
352         args << "-T" << "-";
353         restore->setStdIn(options.files.join(QChar('\n')));
354     }
355     args << "-f" << archive->name();
356     restore->setCommand(makeTarsnapCommand(CMD_TARSNAP));
357     restore->setArguments(args);
358     restore->setData(QVariant::fromValue(archive));
359     connect(restore, &TarsnapTask::finished, this,
360             &TaskManager::restoreArchiveFinished, QUEUED);
361     connect(restore, &TarsnapTask::started, this,
362             [=]() {
363                 emit message(
364                     tr("Restoring from archive <i>%1</i>...").arg(archive->name()));
365             },
366             QUEUED);
367     queueTask(restore);
368 }
369 
getKeyId(QString key)370 void TaskManager::getKeyId(QString key)
371 {
372     QFileInfo keyFile(key);
373     if(!keyFile.exists() || !Utils::tarsnapVersionMinimum("1.0.37"))
374     {
375         DEBUG << "Invalid key path or tarsnap version lower than 1.0.37.";
376         return;
377     }
378     TarsnapTask *keymgmtTask = new TarsnapTask();
379     QStringList  args;
380     args << "--print-key-id" << key;
381     keymgmtTask->setCommand(makeTarsnapCommand(CMD_TARSNAPKEYMGMT));
382     keymgmtTask->setArguments(args);
383     keymgmtTask->setData(key);
384     connect(keymgmtTask, &TarsnapTask::finished, this,
385             &TaskManager::getKeyIdFinished, QUEUED);
386     queueTask(keymgmtTask);
387 }
388 
initializeCache()389 void TaskManager::initializeCache()
390 {
391     QSettings settings;
392     QString   tarsnapCacheDir = settings.value("tarsnap/cache").toString();
393     QDir      cacheDir(tarsnapCacheDir);
394     if(!tarsnapCacheDir.isEmpty()
395        && !cacheDir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries).count())
396     {
397         if(!Utils::tarsnapVersionMinimum("1.0.38"))
398         {
399             DEBUG << "Tarsnap CLI version 1.0.38 or higher required to use "
400                      "--initialize-cachedir.";
401             return;
402         }
403         TarsnapTask *initTask = new TarsnapTask();
404         QStringList  args;
405         initTarsnapArgs(args);
406         args << "--initialize-cachedir";
407         initTask->setCommand(makeTarsnapCommand(CMD_TARSNAP));
408         initTask->setArguments(args);
409         queueTask(initTask);
410     }
411     else
412     {
413         fsck(true);
414     }
415 }
416 
findMatchingArchives(QString jobPrefix)417 void TaskManager::findMatchingArchives(QString jobPrefix)
418 {
419     QList<ArchivePtr> matching;
420     foreach(ArchivePtr archive, _archiveMap)
421     {
422         if(archive->name().startsWith(jobPrefix + QChar('_'))
423            && archive->jobRef().isEmpty())
424             matching << archive;
425     }
426     emit matchingArchives(matching);
427 }
428 
runScheduledJobs()429 void TaskManager::runScheduledJobs()
430 {
431     // sleep for a short while just to take an extra assurance that network is
432     // up
433     QThread::sleep(SCHEDULED_JOBS_SLEEP);
434     loadJobs();
435     QSettings settings;
436     QDate     now(QDate::currentDate());
437     QDate     nextDaily  = settings.value("app/next_daily_timestamp").toDate();
438     QDate     nextWeekly = settings.value("app/next_weekly_timestamp").toDate();
439     QDate nextMonthly = settings.value("app/next_monthly_timestamp").toDate();
440     bool  doDaily     = false;
441     bool  doWeekly    = false;
442     bool  doMonthly   = false;
443     if(!nextDaily.isValid() || (nextDaily <= now))
444     {
445         doDaily = true;
446         settings.setValue("app/next_daily_timestamp", now.addDays(1));
447     }
448     if(!nextWeekly.isValid() || (nextWeekly <= now))
449     {
450         doWeekly         = true;
451         QDate nextSunday = now.addDays(1);
452         for(; nextSunday.dayOfWeek() != 7; nextSunday = nextSunday.addDays(1));
453         settings.setValue("app/next_weekly_timestamp", nextSunday);
454     }
455     if(!nextMonthly.isValid() || (nextMonthly <= now))
456     {
457         doMonthly       = true;
458         QDate nextMonth = now.addMonths(1);
459         nextMonth.setDate(nextMonth.year(), nextMonth.month(), 1);
460         settings.setValue("app/next_monthly_timestamp", nextMonth);
461     }
462     settings.sync();
463     DEBUG << "Daily: " << doDaily;
464     DEBUG << "Next daily: "
465           << settings.value("app/next_daily_timestamp").toDate().toString();
466     DEBUG << "Weekly: " << doWeekly;
467     DEBUG << "Next weekly: "
468           << settings.value("app/next_weekly_timestamp").toDate().toString();
469     DEBUG << "Monthly: " << doWeekly;
470     DEBUG << "Next monthly: "
471           << settings.value("app/next_monthly_timestamp").toDate().toString();
472     bool nothingToDo = true;
473     foreach(JobPtr job, _jobMap)
474     {
475         if((doDaily && (job->optionScheduledEnabled() == JobSchedule::Daily))
476            || (doWeekly && (job->optionScheduledEnabled() == JobSchedule::Weekly))
477            || (doMonthly
478                && (job->optionScheduledEnabled() == JobSchedule::Monthly)))
479         {
480             backupNow(job->createBackupTask());
481             nothingToDo = false;
482         }
483     }
484     if(nothingToDo)
485         qApp->quit();
486 }
487 
stopTasks(bool interrupt,bool running,bool queued)488 void TaskManager::stopTasks(bool interrupt, bool running, bool queued)
489 {
490     if(queued) // queued should be cleared first to avoid race
491     {
492         while(!_taskQueue.isEmpty())
493         {
494             TarsnapTask *task = _taskQueue.dequeue();
495             if(task)
496             {
497                 task->cancel();
498                 task->deleteLater();
499             }
500         }
501         emit message("Cleared queued tasks.");
502     }
503     if(interrupt)
504     {
505         if(!_runningTasks.isEmpty())
506             _runningTasks.first()->interrupt();
507         emit message("Interrupting current backup.");
508     }
509     if(running)
510     {
511         foreach(TarsnapTask *task, _runningTasks)
512         {
513             if(task)
514                 task->stop();
515         }
516         emit message("Stopped running tasks.");
517     }
518 }
519 
backupTaskFinished(QVariant data,int exitCode,QString stdOut,QString stdErr)520 void TaskManager::backupTaskFinished(QVariant data, int exitCode,
521                                      QString stdOut, QString stdErr)
522 {
523     BackupTaskPtr backupTask = _backupTaskMap[data.toUuid()];
524     if(!backupTask)
525     {
526         DEBUG << "Task not found: " << data.toUuid();
527         return;
528     }
529     backupTask->setExitCode(exitCode);
530     backupTask->setOutput(stdOut + stdErr);
531     bool truncated = false;
532     if(exitCode != SUCCESS)
533     {
534         int lastIndex =
535             stdErr.lastIndexOf(QLatin1String("tarsnap: Archive truncated"), -1,
536                                Qt::CaseSensitive);
537         if(lastIndex == -1)
538         {
539             backupTask->setStatus(TaskStatus::Failed);
540             parseError(stdErr);
541             return;
542         }
543         else
544         {
545             truncated = true;
546         }
547     }
548 
549     ArchivePtr archive(new Archive);
550     archive->setName(backupTask->name());
551     if(truncated)
552     {
553         archive->setName(archive->name().append(".part"));
554         archive->setTruncated(true);
555     }
556     archive->setCommand(backupTask->command());
557     // Lose milliseconds precision by converting to Unix timestamp and back.
558     // So that a subsequent comparison in getArchiveListFinished won't fail.
559     archive->setTimestamp(
560         QDateTime::fromTime_t(backupTask->timestamp().toTime_t()));
561     archive->setJobRef(backupTask->jobRef());
562     parseArchiveStats(stdErr, true, archive);
563     archive->save();
564     backupTask->setArchive(archive);
565     backupTask->setStatus(TaskStatus::Completed);
566     _archiveMap.insert(archive->name(), archive);
567     foreach(JobPtr job, _jobMap)
568     {
569         if(job->objectKey() == archive->jobRef())
570             emit job->loadArchives();
571     }
572     emit addArchive(archive);
573     parseGlobalStats(stdErr);
574 }
575 
backupTaskStarted(QVariant data)576 void TaskManager::backupTaskStarted(QVariant data)
577 {
578     BackupTaskPtr backupTask = _backupTaskMap[data.toString()];
579     backupTask->setStatus(TaskStatus::Running);
580 }
581 
registerMachineFinished(QVariant data,int exitCode,QString stdOut,QString stdErr)582 void TaskManager::registerMachineFinished(QVariant data, int exitCode,
583                                           QString stdOut, QString stdErr)
584 {
585     Q_UNUSED(data)
586     if(exitCode == SUCCESS)
587         emit registerMachineStatus(TaskStatus::Completed, stdOut);
588     else
589         emit registerMachineStatus(TaskStatus::Failed, stdErr);
590 }
591 
getArchiveListFinished(QVariant data,int exitCode,QString stdOut,QString stdErr)592 void TaskManager::getArchiveListFinished(QVariant data, int exitCode,
593                                          QString stdOut, QString stdErr)
594 {
595     Q_UNUSED(data)
596 
597     if(exitCode == SUCCESS)
598     {
599         emit message(tr("Updating archives list from remote... done."));
600     }
601     else
602     {
603         emit message(tr("Error: Failed to list archives from remote."),
604                      tr("Tarsnap exited with code %1 and output:\n%2")
605                          .arg(exitCode)
606                          .arg(stdErr));
607         parseError(stdErr);
608         return;
609     }
610 
611     QMap<QString, ArchivePtr> _newArchiveMap;
612     QStringList lines = stdOut.split('\n', QString::SkipEmptyParts);
613     foreach(QString line, lines)
614     {
615         QRegExp archiveDetailsRX("^(.+)\\t+(\\S+\\s+\\S+)\\t+(.+)$");
616         if(-1 != archiveDetailsRX.indexIn(line))
617         {
618             QStringList archiveDetails = archiveDetailsRX.capturedTexts();
619             archiveDetails.removeFirst();
620             QDateTime timestamp =
621                 QDateTime::fromString(archiveDetails[1], Qt::ISODate);
622             ArchivePtr archive =
623                 _archiveMap.value(archiveDetails[0], ArchivePtr(new Archive));
624             if(!archive->objectKey().isEmpty()
625                && (archive->timestamp() != timestamp))
626             {
627                 // There is a different archive with the same name on the remote
628                 archive->purge();
629                 archive.clear();
630                 archive = archive.create();
631             }
632             if(archive->objectKey().isEmpty())
633             {
634                 // New archive
635                 archive->setName(archiveDetails[0]);
636                 archive->setTimestamp(timestamp);
637                 archive->setCommand(archiveDetails[2]);
638                 // Automagically set Job ownership
639                 foreach(JobPtr job, _jobMap)
640                 {
641                     if(archive->name().startsWith(job->archivePrefix()))
642                         archive->setJobRef(job->objectKey());
643                 }
644                 archive->save();
645                 emit addArchive(archive);
646                 getArchiveStats(archive);
647             }
648             _newArchiveMap.insert(archive->name(), archive);
649             _archiveMap.remove(archive->name());
650         }
651     }
652     // Purge archives left in old _archiveMap (not mirrored by the remote)
653     foreach(ArchivePtr archive, _archiveMap)
654     {
655         archive->purge();
656     }
657     _archiveMap.clear();
658     _archiveMap = _newArchiveMap;
659     foreach(JobPtr job, _jobMap)
660     {
661         emit job->loadArchives();
662     }
663     getOverallStats();
664 }
665 
getArchiveStatsFinished(QVariant data,int exitCode,QString stdOut,QString stdErr)666 void TaskManager::getArchiveStatsFinished(QVariant data, int exitCode,
667                                           QString stdOut, QString stdErr)
668 {
669     ArchivePtr archive = data.value<ArchivePtr>();
670     if(!archive)
671     {
672         DEBUG << "Archive not found.";
673         return;
674     }
675     if(exitCode == SUCCESS)
676     {
677         emit message(tr("Fetching stats for archive <i>%1</i>... done.")
678                          .arg(archive->name()));
679     }
680     else
681     {
682         emit message(tr("Error: Failed to get archive stats from remote."),
683                      tr("Tarsnap exited with code %1 and output:\n%2")
684                          .arg(exitCode)
685                          .arg(stdErr));
686         parseError(stdErr);
687         return;
688     }
689 
690     parseArchiveStats(stdOut, false, archive);
691     parseGlobalStats(stdOut);
692 }
693 
getArchiveContentsFinished(QVariant data,int exitCode,QString stdOut,QString stdErr)694 void TaskManager::getArchiveContentsFinished(QVariant data, int exitCode,
695                                              QString stdOut, QString stdErr)
696 {
697     ArchivePtr archive = data.value<ArchivePtr>();
698 
699     if(!archive)
700     {
701         DEBUG << "Archive not found.";
702         return;
703     }
704 
705     QString detailText;
706     if(exitCode != SUCCESS)
707     {
708         bool truncated = stdErr.contains(QLatin1String("tarsnap: Truncated"
709                                                        " input file"));
710         if(archive->name().endsWith(".part", Qt::CaseSensitive) && truncated)
711         {
712             detailText = stdErr;
713             archive->setTruncated(true);
714             archive->setTruncatedInfo(stdErr);
715         }
716         else if(stdOut.isEmpty())
717         {
718             emit message(
719                 tr("Error: Failed to get archive contents from remote."),
720                 tr("Tarsnap exited with code %1 and output:\n%2")
721                     .arg(exitCode)
722                     .arg(stdErr));
723             parseError(stdErr);
724             return;
725         }
726     }
727 
728     emit message(tr("Fetching contents for archive <i>%1</i>... done.")
729                  .arg(archive->name()), detailText);
730 
731     archive->setContents(stdOut);
732     archive->save();
733 }
734 
deleteArchivesFinished(QVariant data,int exitCode,QString stdOut,QString stdErr)735 void TaskManager::deleteArchivesFinished(QVariant data, int exitCode,
736                                          QString stdOut, QString stdErr)
737 {
738     Q_UNUSED(stdOut)
739     QList<ArchivePtr> archives = data.value<QList<ArchivePtr>>();
740 
741     if(exitCode != SUCCESS)
742     {
743         emit message(tr("Error: Failed to delete archive(s) from remote."),
744                      tr("Tarsnap exited with code %1 and output:\n%2")
745                          .arg(exitCode)
746                          .arg(stdErr));
747         parseError(stdErr);
748         foreach(ArchivePtr archive, archives)
749             archive->setDeleteScheduled(false);
750         return;
751     }
752 
753     if(!archives.empty())
754     {
755         foreach(ArchivePtr archive, archives)
756         {
757             _archiveMap.remove(archive->name());
758             archive->purge();
759         }
760         notifyArchivesDeleted(archives, true);
761     }
762     // We are only interested in the output of the last archive deleted for
763     // parsing the final global stats
764     QStringList lines = stdErr.split('\n', QString::SkipEmptyParts);
765     QStringList lastFive;
766     int         count = lines.count();
767     for(int i = 0; i < std::min(5, count); ++i)
768         lastFive.prepend(lines.takeLast());
769     parseGlobalStats(lastFive.join('\n'));
770 }
771 
overallStatsFinished(QVariant data,int exitCode,QString stdOut,QString stdErr)772 void TaskManager::overallStatsFinished(QVariant data, int exitCode,
773                                        QString stdOut, QString stdErr)
774 {
775     Q_UNUSED(data);
776 
777     if(exitCode != SUCCESS)
778     {
779         emit message(tr("Error: Failed to get stats from remote."),
780                      tr("Tarsnap exited with code %1 and output:\n%2")
781                          .arg(exitCode)
782                          .arg(stdErr));
783         parseError(stdErr);
784         return;
785     }
786 
787     parseGlobalStats(stdOut);
788 }
789 
fsckFinished(QVariant data,int exitCode,QString stdOut,QString stdErr)790 void TaskManager::fsckFinished(QVariant data, int exitCode, QString stdOut,
791                                QString stdErr)
792 {
793     Q_UNUSED(data)
794     if(exitCode == SUCCESS)
795     {
796         emit message(tr("Cache repair succeeded."), stdOut);
797     }
798     else
799     {
800         emit message(tr("Cache repair failed. Hover mouse for details."), stdErr);
801         parseError(stdErr);
802     }
803     getArchives();
804 }
805 
nukeFinished(QVariant data,int exitCode,QString stdOut,QString stdErr)806 void TaskManager::nukeFinished(QVariant data, int exitCode, QString stdOut,
807                                QString stdErr)
808 {
809     Q_UNUSED(data)
810     if(exitCode == SUCCESS)
811     {
812         emit message(tr("All archives nuked successfully."), stdOut);
813         fsck();
814     }
815     else
816     {
817         emit message(tr("Archives nuke failed. Hover mouse for details."),
818                      stdErr);
819         parseError(stdErr);
820         return;
821     }
822 }
823 
restoreArchiveFinished(QVariant data,int exitCode,QString stdOut,QString stdErr)824 void TaskManager::restoreArchiveFinished(QVariant data, int exitCode,
825                                          QString stdOut, QString stdErr)
826 {
827     Q_UNUSED(stdOut)
828     ArchivePtr archive = data.value<ArchivePtr>();
829     if(!archive)
830     {
831         DEBUG << "Archive not found.";
832         return;
833     }
834     if(exitCode == SUCCESS)
835     {
836         emit message(
837             tr("Restoring from archive <i>%1</i>... done.").arg(archive->name()));
838     }
839     else
840     {
841         emit message(tr("Restoring from archive <i>%1</i> failed."
842                         " Hover mouse for details.")
843                          .arg(archive->name()),
844                      stdErr);
845         parseError(stdErr);
846         return;
847     }
848 }
849 
notifyBackupTaskUpdate(QUuid uuid,const TaskStatus & status)850 void TaskManager::notifyBackupTaskUpdate(QUuid uuid, const TaskStatus &status)
851 {
852     BackupTaskPtr backupTask = _backupTaskMap[uuid];
853     if(!backupTask)
854     {
855         DEBUG << "Backup task update for invalid task";
856         return;
857     }
858     switch(status)
859     {
860     case TaskStatus::Initialized:
861         DEBUG << "Backup task undefined";
862         break;
863     case TaskStatus::Completed:
864     {
865         QString msg = tr("Backup <i>%1</i> completed. (%2 new data on Tarsnap)")
866                           .arg(backupTask->name())
867                           .arg(Utils::humanBytes(
868                               backupTask->archive()->sizeUniqueCompressed()));
869         emit message(msg, backupTask->archive()->archiveStats());
870         emit displayNotification(msg);
871         _backupTaskMap.remove(backupTask->uuid());
872         break;
873     }
874     case TaskStatus::Queued:
875         emit message(tr("Backup <i>%1</i> queued.").arg(backupTask->name()));
876         break;
877     case TaskStatus::Running:
878     {
879         QString msg = tr("Backup <i>%1</i> is running.").arg(backupTask->name());
880         emit    message(msg);
881         emit    displayNotification(msg);
882         break;
883     }
884     case TaskStatus::Failed:
885     {
886         QString msg =
887             tr("Backup <i>%1</i> failed: %2")
888                 .arg(backupTask->name())
889                 .arg(backupTask->output()
890                          .section(QChar('\n'), 0, 0, QString::SectionSkipEmpty)
891                          .simplified());
892         emit message(msg, backupTask->output());
893         emit displayNotification(msg);
894         _backupTaskMap.remove(backupTask->uuid());
895         break;
896     }
897     case TaskStatus::Paused:
898         emit message(tr("Backup <i>%1</i> paused.").arg(backupTask->name()));
899         break;
900     }
901 }
902 
notifyArchivesDeleted(QList<ArchivePtr> archives,bool done)903 void TaskManager::notifyArchivesDeleted(QList<ArchivePtr> archives, bool done)
904 {
905     if(archives.count() > 1)
906     {
907         QString detail(archives[0]->name());
908         for(int i = 1; i < archives.count(); ++i)
909         {
910             ArchivePtr archive = archives.at(i);
911             detail.append(QString::fromLatin1(", ") + archive->name());
912         }
913         emit message(tr("Deleting archive <i>%1</i> and %2 more archives... %3")
914                          .arg(archives.first()->name())
915                          .arg(archives.count() - 1)
916                          .arg(done ? tr("done.") : ""),
917                      detail);
918     }
919     else if(archives.count() == 1)
920     {
921         emit message(tr("Deleting archive <i>%1</i>... %2")
922                          .arg(archives.first()->name())
923                          .arg(done ? tr("done.") : ""));
924     }
925 }
926 
getKeyIdFinished(QVariant data,int exitCode,QString stdOut,QString stdErr)927 void TaskManager::getKeyIdFinished(QVariant data, int exitCode, QString stdOut,
928                                    QString stdErr)
929 {
930     QString key = data.toString();
931     if(exitCode == SUCCESS)
932     {
933         bool ok = false;
934         int  id = stdOut.toInt(&ok);
935         if(ok)
936             emit keyId(key, id);
937         else
938             DEBUG << "Invalid output from tarsnap-keymgmt for key " << key;
939     }
940     else
941     {
942         DEBUG << "Failed to get the id for key " << key;
943         parseError(stdErr);
944     }
945 }
946 
queueTask(TarsnapTask * task,bool exclusive)947 void TaskManager::queueTask(TarsnapTask *task, bool exclusive)
948 {
949     if(task == nullptr)
950     {
951         DEBUG << "NULL argument";
952         return;
953     }
954     if(exclusive && !_runningTasks.isEmpty())
955         _taskQueue.enqueue(task);
956     else
957         startTask(task);
958 }
959 
startTask(TarsnapTask * task)960 void TaskManager::startTask(TarsnapTask *task)
961 {
962     if(task == nullptr)
963     {
964         if(!_taskQueue.isEmpty())
965             task = _taskQueue.dequeue();
966         else
967             return;
968     }
969     connect(task, &TarsnapTask::dequeue, this, &TaskManager::dequeueTask, QUEUED);
970     _runningTasks.append(task);
971     task->setAutoDelete(false);
972     _threadPool->start(task);
973     emit idle(false);
974 }
975 
dequeueTask()976 void TaskManager::dequeueTask()
977 {
978     TarsnapTask *task = qobject_cast<TarsnapTask *>(sender());
979     if(task == nullptr)
980         return;
981     _runningTasks.removeOne(task);
982     task->deleteLater();
983     if(_runningTasks.isEmpty())
984     {
985         if(_taskQueue.isEmpty())
986             emit idle(true);
987         else
988             startTask(nullptr); // start another queued task
989     }
990 }
991 
parseError(QString tarsnapOutput)992 void TaskManager::parseError(QString tarsnapOutput)
993 {
994     if(tarsnapOutput.contains("Error reading cache directory")
995        || tarsnapOutput.contains("Sequence number mismatch: Run --fsck")
996        || tarsnapOutput.contains(
997               "Directory is not consistent with archive: Run --fsck"))
998     {
999         emit error(TarsnapError::CacheError);
1000     }
1001     else if(tarsnapOutput.contains("Error fscking archives"))
1002     {
1003         emit error(TarsnapError::FsckError);
1004     }
1005     else if(tarsnapOutput.contains("Cannot obtain server address")
1006             || tarsnapOutput.contains("Error looking up")
1007             || tarsnapOutput.contains("Too many network failures"))
1008     {
1009         emit error(TarsnapError::NetworkError);
1010     }
1011 }
1012 
parseGlobalStats(QString tarsnapOutput)1013 void TaskManager::parseGlobalStats(QString tarsnapOutput)
1014 {
1015     QStringList lines = tarsnapOutput.split('\n', QString::SkipEmptyParts);
1016     if(lines.count() < 3)
1017     {
1018         DEBUG << "Malformed output from tarsnap CLI:\n" << tarsnapOutput;
1019         return;
1020     }
1021 
1022     quint64 sizeTotal            = 0;
1023     quint64 sizeCompressed       = 0;
1024     quint64 sizeUniqueTotal      = 0;
1025     quint64 sizeUniqueCompressed = 0;
1026 
1027     QRegExp sizeRX("^All archives\\s+(\\d+)\\s+(\\d+)$");
1028     if(-1 == sizeRX.indexIn(lines[1]))
1029     {
1030         DEBUG << "Malformed output from tarsnap CLI:\n" << tarsnapOutput;
1031         return;
1032     }
1033 
1034     QStringList captured = sizeRX.capturedTexts();
1035     captured.removeFirst();
1036     sizeTotal      = captured[0].toULongLong();
1037     sizeCompressed = captured[1].toULongLong();
1038 
1039     QRegExp uniqueSizeRX("^\\s+\\(unique data\\)\\s+(\\d+)\\s+(\\d+)$");
1040     if(-1 == uniqueSizeRX.indexIn(lines[2]))
1041     {
1042         DEBUG << "Malformed output from tarsnap CLI:\n" << tarsnapOutput;
1043         return;
1044     }
1045 
1046     captured = uniqueSizeRX.capturedTexts();
1047     captured.removeFirst();
1048     sizeUniqueTotal      = captured[0].toULongLong();
1049     sizeUniqueCompressed = captured[1].toULongLong();
1050 
1051     emit overallStats(sizeTotal, sizeCompressed, sizeUniqueTotal,
1052                       sizeUniqueCompressed,
1053                       static_cast<quint64>(_archiveMap.count()));
1054 }
1055 
parseArchiveStats(QString tarsnapOutput,bool newArchiveOutput,ArchivePtr archive)1056 void TaskManager::parseArchiveStats(QString tarsnapOutput,
1057                                     bool newArchiveOutput, ArchivePtr archive)
1058 {
1059     QStringList lines = tarsnapOutput.split('\n', QString::SkipEmptyParts);
1060     if(lines.count() < 5)
1061     {
1062         DEBUG << "Malformed output from tarsnap CLI:\n" << tarsnapOutput;
1063         return;
1064     }
1065     QRegExp sizeRX;
1066     QRegExp uniqueSizeRX;
1067     if(newArchiveOutput)
1068     {
1069         sizeRX.setPattern("^This archive\\s+(\\d+)\\s+(\\d+)$");
1070         uniqueSizeRX.setPattern("^New data\\s+(\\d+)\\s+(\\d+)$");
1071     }
1072     else
1073     {
1074         sizeRX.setPattern(
1075             QString("^%1\\s+(\\d+)\\s+(\\d+)$").arg(archive->name()));
1076         uniqueSizeRX.setPattern("^\\s+\\(unique data\\)\\s+(\\d+)\\s+(\\d+)$");
1077     }
1078     bool matched = false;
1079     foreach(QString line, lines)
1080     {
1081         if(-1 != sizeRX.indexIn(line))
1082         {
1083             QStringList captured = sizeRX.capturedTexts();
1084             captured.removeFirst();
1085             archive->setSizeTotal(captured[0].toULongLong());
1086             archive->setSizeCompressed(captured[1].toULongLong());
1087             matched = true;
1088         }
1089         if(-1 != uniqueSizeRX.indexIn(line))
1090         {
1091             QStringList captured = uniqueSizeRX.capturedTexts();
1092             captured.removeFirst();
1093             archive->setSizeUniqueTotal(captured[0].toULongLong());
1094             archive->setSizeUniqueCompressed(captured[1].toULongLong());
1095             matched = true;
1096         }
1097     }
1098     if(!matched)
1099     {
1100         DEBUG << "Malformed output from tarsnap CLI:\n" << tarsnapOutput;
1101         return;
1102     }
1103     archive->save();
1104 }
1105 
makeTarsnapCommand(QString cmd)1106 QString TaskManager::makeTarsnapCommand(QString cmd)
1107 {
1108     QSettings settings;
1109     QString   _tarsnapDir = settings.value("tarsnap/path").toString();
1110     if(_tarsnapDir.isEmpty())
1111         return cmd;
1112     else
1113         return _tarsnapDir + QDir::separator() + cmd;
1114 }
1115 
initTarsnapArgs(QStringList & args)1116 void TaskManager::initTarsnapArgs(QStringList &args)
1117 {
1118     QSettings settings;
1119     QString   tarsnapKeyFile = settings.value("tarsnap/key").toString();
1120     if(!tarsnapKeyFile.isEmpty())
1121         args << "--keyfile" << tarsnapKeyFile;
1122     QString tarsnapCacheDir = settings.value("tarsnap/cache").toString();
1123     if(!tarsnapCacheDir.isEmpty())
1124         args << "--cachedir" << tarsnapCacheDir;
1125     int download_rate_kbps = settings.value("app/limit_download", 0).toInt();
1126     if(download_rate_kbps)
1127     {
1128         args.prepend("--maxbw-rate-down");
1129         args.insert(1, QString::number(1024 * quint64(download_rate_kbps)));
1130     }
1131     int upload_rate_kbps = settings.value("app/limit_upload", 0).toInt();
1132     if(upload_rate_kbps)
1133     {
1134         args.prepend("--maxbw-rate-up");
1135         args.insert(1, QString::number(1024 * quint64(upload_rate_kbps)));
1136     }
1137     if(settings.value("tarsnap/no_default_config", DEFAULT_NO_DEFAULT_CONFIG)
1138            .toBool())
1139         args.prepend("--no-default-config");
1140 }
1141 
loadJobs()1142 void TaskManager::loadJobs()
1143 {
1144     _jobMap.clear();
1145     PersistentStore &store = PersistentStore::instance();
1146     if(!store.initialized())
1147     {
1148         DEBUG << "PersistentStore was not initialized properly.";
1149         return;
1150     }
1151     QSqlQuery query = store.createQuery();
1152     if(!query.prepare(QLatin1String("select name from jobs")))
1153     {
1154         DEBUG << query.lastError().text();
1155         return;
1156     }
1157     if(store.runQuery(query) && query.next())
1158     {
1159         do
1160         {
1161             JobPtr job(new Job);
1162             job->setName(query.value(query.record().indexOf("name")).toString());
1163             connect(job.data(), &Job::loadArchives, this,
1164                     &TaskManager::loadJobArchives, QUEUED);
1165             job->load();
1166             _jobMap[job->name()] = job;
1167         } while(query.next());
1168     }
1169     emit jobsList(_jobMap);
1170 }
1171 
deleteJob(JobPtr job,bool purgeArchives)1172 void TaskManager::deleteJob(JobPtr job, bool purgeArchives)
1173 {
1174     if(job)
1175     {
1176         // Clear JobRef for assigned Archives.
1177         foreach(ArchivePtr archive, job->archives())
1178         {
1179             archive->setJobRef("");
1180             archive->save();
1181         }
1182 
1183         job->purge();
1184         _jobMap.remove(job->name());
1185 
1186         if(purgeArchives)
1187         {
1188             emit message(tr("Job <i>%1</i> deleted. Deleting %2 associated "
1189                             "archives next...")
1190                          .arg(job->name())
1191                          .arg(job->archives().count()));
1192             deleteArchives(job->archives());
1193         }
1194         else
1195         {
1196             emit message(tr("Job <i>%1</i> deleted.").arg(job->name()));
1197         }
1198     }
1199 }
1200 
loadJobArchives()1201 void TaskManager::loadJobArchives()
1202 {
1203     Job *job = qobject_cast<Job *>(sender());
1204     QList<ArchivePtr> archives;
1205     foreach(ArchivePtr archive, _archiveMap)
1206     {
1207         if(archive->jobRef() == job->objectKey())
1208             archives << archive;
1209     }
1210     job->setArchives(archives);
1211 }
1212 
getTaskInfo()1213 void TaskManager::getTaskInfo()
1214 {
1215     bool backupTaskRunning = false;
1216     if(!_runningTasks.isEmpty() && !_backupTaskMap.isEmpty())
1217     {
1218         foreach(TarsnapTask *task, _runningTasks)
1219         {
1220             if(task && _backupTaskMap.contains(task->data().toUuid()))
1221             {
1222                 backupTaskRunning = true;
1223                 break;
1224             }
1225         }
1226     }
1227     emit taskInfo(backupTaskRunning, _runningTasks.count(), _taskQueue.count());
1228 }
1229 
addJob(JobPtr job)1230 void TaskManager::addJob(JobPtr job)
1231 {
1232     _jobMap[job->name()] = job;
1233     connect(job.data(), &Job::loadArchives, this, &TaskManager::loadJobArchives,
1234             QUEUED);
1235     emit message(tr("Job <i>%1</i> added.").arg(job->name()));
1236 }
1237 
getTarsnapVersionFinished(QVariant data,int exitCode,QString stdOut,QString stdErr)1238 void TaskManager::getTarsnapVersionFinished(QVariant data, int exitCode,
1239                                             QString stdOut, QString stdErr)
1240 {
1241     Q_UNUSED(data)
1242 
1243     if(exitCode != SUCCESS)
1244     {
1245         emit message(tr("Error: Failed to get Tarsnap version."),
1246                      tr("Tarsnap exited with code %1 and output:\n%2")
1247                          .arg(exitCode)
1248                          .arg(stdErr));
1249         return;
1250     }
1251 
1252     QRegExp versionRx("^tarsnap (\\S+)\\s?$");
1253     if(-1 != versionRx.indexIn(stdOut))
1254         emit tarsnapVersion(versionRx.cap(1));
1255 }
1256