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