Tell administrators about a location without a schedule
A notification when a schedule is turned off, then a reminder: once a week while another location is backed up, every day when none is. The round trip now also checks the text of these notifications and of the one about names, and that one location never gets two runs.
This commit is contained in:
@@ -59,8 +59,11 @@ final class BackupJob extends TimedJob {
|
||||
}
|
||||
$this->failStaleRestores();
|
||||
$this->weeklyPrune();
|
||||
foreach ($this->targets->list() as $t) {
|
||||
$targets = $this->targets->list();
|
||||
foreach ($targets as $t) {
|
||||
$this->alerts->checkStale($t);
|
||||
$othersActive = array_filter($targets, static fn ($o) => $o->getId() !== $t->getId() && (string)$o->getSchedule() !== '') !== [];
|
||||
$this->alerts->checkOff($t, $othersActive);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,13 +61,20 @@ final class Notifier implements INotifier {
|
||||
: $l->t('No successful backup to %1$s since %2$s', [$p['target'], $p['since']]));
|
||||
$notification->setParsedMessage($l->t('Check the backup settings and the server log.'));
|
||||
break;
|
||||
case 'schedule_off':
|
||||
$notification->setParsedSubject($l->t('Backup location %s has no schedule', [$p['target']]));
|
||||
$notification->setParsedMessage(($p['others'] ?? '') === '1'
|
||||
? $l->t('No backups run to this location until it gets a schedule or someone starts one. If that is what you want, you can ignore this reminder.')
|
||||
: $l->t('No location has a schedule, so no backups run at all unless someone starts one. Set a schedule under Backups.'));
|
||||
break;
|
||||
case 'security_event':
|
||||
$who = (string)($p['actor'] ?? '');
|
||||
$notification->setParsedSubject(match ($p['event'] ?? '') {
|
||||
'kit_downloaded' => $l->t('%s downloaded the backup recovery kit', [$who]),
|
||||
'target_removed' => $l->t('%1$s removed backup location %2$s', [$who, $p['target'] ?? '']),
|
||||
'target_changed' => $l->t('%1$s changed the connection settings of backup location %2$s', [$who, $p['target'] ?? '']),
|
||||
'append_only_off' => $l->t('%1$s turned off append-only for backup location %2$s', [$who, $p['target'] ?? '']),
|
||||
'schedule_off' => $l->t('%1$s turned off the schedule of backup location %2$s', [$who, $p['target'] ?? '']),
|
||||
'append_only_off' =>$l->t('%1$s turned off append-only for backup location %2$s', [$who, $p['target'] ?? '']),
|
||||
'retention_changed' => $l->t('%1$s changed the backup retention to %2$s', [$who, $p['policy'] ?? '']),
|
||||
'snapshots_forgotten' => $l->t('%1$s moved %2$s snapshots of %3$s to the trash; they are deleted permanently after %4$s unless restored', [$who, $p['count'] ?? '', $p['target'] ?? '', $p['until'] ?? '']),
|
||||
'snapshot_untrashed' => $l->t('%1$s restored snapshot %2$s of %3$s from the trash', [$who, $p['snapshot'] ?? '', $p['target'] ?? '']),
|
||||
|
||||
@@ -101,6 +101,30 @@ final class AlertService {
|
||||
return $v === '' ? null : (int)$v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether it is time to remind administrators of a location without a schedule: once a week
|
||||
* while another location is still backed up, every day when none is. An hour of margin, so
|
||||
* that the moment of the cron run does not push a reminder to the next day.
|
||||
*/
|
||||
public static function offReminderDue(int $last, int $now, bool $othersActive): bool {
|
||||
return $now - $last >= ($othersActive ? 7 * 86400 : 86400) - 3600;
|
||||
}
|
||||
|
||||
public function checkOff(Target $target, bool $othersActive): void {
|
||||
if ((string)$target->getSchedule() !== '') {
|
||||
return;
|
||||
}
|
||||
$key = 'off_notified_' . $target->getId();
|
||||
$now = time();
|
||||
$last = max((int)$target->getScheduleSince(), $this->appConfig->getValueInt(Application::APP_ID, $key, 0));
|
||||
if (!self::offReminderDue($last, $now, $othersActive)) {
|
||||
return;
|
||||
}
|
||||
$this->appConfig->setValueInt(Application::APP_ID, $key, $now);
|
||||
$this->notifyAdmins('schedule_off', 'off-' . $target->getId() . '-' . date('Y-m-d', $now),
|
||||
['target' => $target->getName(), 'others' => $othersActive ? '1' : '0']);
|
||||
}
|
||||
|
||||
/** At most one notification per location per day. */
|
||||
public function checkStale(Target $target): void {
|
||||
if ((string)$target->getSchedule() === '') {
|
||||
|
||||
@@ -35,6 +35,7 @@ final class TargetService {
|
||||
private \OCA\NgBackup\Db\DbIndexCache $indexCache,
|
||||
private \OCA\NgBackup\Db\AppConfigCatalogAnchor $anchor,
|
||||
private SnapshotMapper $snapshots,
|
||||
private AlertService $alerts,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -281,10 +282,16 @@ final class TargetService {
|
||||
if (!Schedule::validZone($zone)) {
|
||||
throw new \InvalidArgumentException('Unknown time zone: ' . $zone);
|
||||
}
|
||||
$target->setSchedule(LocationSchedule::normalize($schedule));
|
||||
$new = LocationSchedule::normalize($schedule);
|
||||
$turnedOff = $new === '' && (string)$target->getSchedule() !== '';
|
||||
$target->setSchedule($new);
|
||||
$target->setScheduleZone($zone);
|
||||
$target->setScheduleSince(time());
|
||||
return $this->mapper->update($target);
|
||||
$target = $this->mapper->update($target);
|
||||
if ($turnedOff) {
|
||||
$this->alerts->securityEvent('schedule_off', ['target' => $target->getName()]);
|
||||
}
|
||||
return $target;
|
||||
}
|
||||
|
||||
public function setMaxBytes(Target $target, ?int $maxBytes): Target {
|
||||
|
||||
Reference in New Issue
Block a user