2022-05-21 11:06:10 +00:00
|
|
|
import enum
|
2018-11-22 03:26:26 +00:00
|
|
|
import logging
|
2021-11-12 07:05:31 +00:00
|
|
|
import threading
|
2022-02-06 11:21:17 +00:00
|
|
|
from datetime import datetime as dt
|
|
|
|
from datetime import timedelta
|
2022-05-22 03:40:46 +00:00
|
|
|
from typing import Dict, NamedTuple, Optional, Tuple, Union
|
2019-01-13 01:51:35 +00:00
|
|
|
|
2021-02-11 06:09:22 +00:00
|
|
|
from PyQt5 import QtCore
|
2022-05-21 11:06:10 +00:00
|
|
|
from PyQt5.QtCore import QTimer
|
2021-11-12 07:05:31 +00:00
|
|
|
from PyQt5.QtWidgets import QApplication
|
2022-02-06 11:21:17 +00:00
|
|
|
|
2022-05-21 11:06:10 +00:00
|
|
|
from vorta import application
|
2021-10-04 11:31:41 +00:00
|
|
|
from vorta.borg.check import BorgCheckJob
|
|
|
|
from vorta.borg.create import BorgCreateJob
|
|
|
|
from vorta.borg.list_repo import BorgListRepoJob
|
|
|
|
from vorta.borg.prune import BorgPruneJob
|
2019-01-20 03:50:10 +00:00
|
|
|
from vorta.i18n import translate
|
2020-06-01 23:58:09 +00:00
|
|
|
from vorta.notifications import VortaNotifications
|
2022-02-06 11:21:17 +00:00
|
|
|
from vorta.store.models import BackupProfileModel, EventLogModel
|
2018-11-06 05:13:49 +00:00
|
|
|
|
2019-01-13 01:51:35 +00:00
|
|
|
logger = logging.getLogger(__name__)
|
2018-11-22 03:26:26 +00:00
|
|
|
|
2018-10-31 16:09:01 +00:00
|
|
|
|
2022-05-21 11:06:10 +00:00
|
|
|
class ScheduleStatusType(enum.Enum):
|
|
|
|
SCHEDULED = enum.auto() # date provided
|
|
|
|
UNSCHEDULED = enum.auto() # Unknown
|
|
|
|
TOO_FAR_AHEAD = enum.auto() # QTimer range exceeded, date provided
|
|
|
|
NO_PREVIOUS_BACKUP = enum.auto() # run a manual backup first
|
|
|
|
|
|
|
|
|
|
|
|
class ScheduleStatus(NamedTuple):
|
|
|
|
type: ScheduleStatusType
|
|
|
|
time: Optional[dt] = None
|
|
|
|
|
|
|
|
|
2021-10-27 04:37:28 +00:00
|
|
|
class VortaScheduler(QtCore.QObject):
|
2022-02-09 10:55:26 +00:00
|
|
|
|
|
|
|
#: The schedule for the profile with the given id changed.
|
|
|
|
schedule_changed = QtCore.pyqtSignal(int)
|
|
|
|
|
2021-10-27 04:37:28 +00:00
|
|
|
def __init__(self):
|
2018-10-31 11:14:12 +00:00
|
|
|
super().__init__()
|
2022-02-20 06:04:12 +00:00
|
|
|
|
|
|
|
#: mapping of profiles to timers
|
2022-05-21 11:06:10 +00:00
|
|
|
self.timers: Dict[int, Dict[str, Union[
|
|
|
|
Optional[QTimer], Optional[dt], ScheduleStatusType]]] = dict()
|
2022-02-20 06:04:12 +00:00
|
|
|
|
2022-05-21 11:06:10 +00:00
|
|
|
self.app: application.VortaApp = QApplication.instance()
|
2021-11-12 07:05:31 +00:00
|
|
|
self.lock = threading.Lock()
|
2018-10-29 05:12:45 +00:00
|
|
|
|
2022-05-22 03:40:46 +00:00
|
|
|
# pausing will prevent scheduling for a specified time
|
|
|
|
self.pauses: Dict[int, Tuple[dt, QtCore.QTimer]] = dict()
|
|
|
|
|
2021-10-27 04:37:28 +00:00
|
|
|
# Set additional timer to make sure background tasks stay scheduled.
|
|
|
|
# E.g. after hibernation
|
2022-05-21 11:06:10 +00:00
|
|
|
self.qt_timer = QTimer()
|
2021-10-27 04:37:28 +00:00
|
|
|
self.qt_timer.timeout.connect(self.reload_all_timers)
|
|
|
|
self.qt_timer.setInterval(15 * 60 * 1000)
|
2021-02-11 06:09:22 +00:00
|
|
|
self.qt_timer.start()
|
|
|
|
|
2022-05-21 11:06:10 +00:00
|
|
|
# connect signals
|
|
|
|
self.app.backup_finished_event.connect(
|
|
|
|
lambda res: self.set_timer_for_profile(res['params']['profile_id']))
|
|
|
|
|
2019-01-20 03:50:10 +00:00
|
|
|
def tr(self, *args, **kwargs):
|
|
|
|
scope = self.__class__.__name__
|
|
|
|
return translate(scope, *args, **kwargs)
|
|
|
|
|
2022-05-22 03:40:46 +00:00
|
|
|
def pause(self, profile_id: int, until: Optional[dt] = None):
|
|
|
|
"""
|
|
|
|
Call a timeout for scheduling of a given profile.
|
|
|
|
|
|
|
|
If `until` is omitted, a default time for the break is calculated.
|
|
|
|
|
|
|
|
.. warning::
|
|
|
|
This method won't work correctly when called from a non-`QThread`.
|
|
|
|
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
profile_id : int
|
|
|
|
The profile to pause the scheduling for.
|
|
|
|
until : Optional[dt], optional
|
|
|
|
The time to end the pause, by default None
|
|
|
|
"""
|
|
|
|
profile = BackupProfileModel.get_or_none(id=profile_id)
|
|
|
|
if profile is None: # profile doesn't exist any more.
|
|
|
|
return
|
|
|
|
|
|
|
|
if profile.schedule_mode == 'off':
|
|
|
|
return
|
|
|
|
|
|
|
|
if until is None:
|
|
|
|
# calculate default timeout
|
|
|
|
|
|
|
|
if profile.schedule_mode == 'interval':
|
|
|
|
interval = timedelta(
|
|
|
|
**{profile.schedule_interval_unit:
|
|
|
|
profile.schedule_interval_count})
|
|
|
|
else:
|
|
|
|
# fixed
|
|
|
|
interval = timedelta(days=1)
|
|
|
|
|
|
|
|
timeout = interval // 6 # 60 / 6 = 10 [min]
|
|
|
|
timeout = max(min(timeout, timedelta(hours=1)),
|
|
|
|
timedelta(minutes=1)) # 1 <= t <= 60
|
|
|
|
|
|
|
|
until = dt.now().replace(microsecond=0) + timeout
|
|
|
|
elif until < dt.now():
|
|
|
|
return
|
|
|
|
|
|
|
|
# remove existing schedule
|
|
|
|
self.remove_job(profile_id)
|
|
|
|
|
|
|
|
# setting timer for reschedule is not possible if called
|
|
|
|
# from a non-QThread - it won't fail but won't work
|
|
|
|
timer_value = max(1, (until - dt.now()).total_seconds())
|
|
|
|
timer = QtCore.QTimer()
|
|
|
|
timer.setInterval(int(timer_value * 1000) + 100)
|
|
|
|
timer.timeout.connect(lambda: self.set_timer_for_profile(profile_id))
|
|
|
|
timer.start()
|
|
|
|
|
|
|
|
# set timeout/pause
|
|
|
|
other_pause = self.pauses.get(profile_id)
|
|
|
|
if other_pause is not None:
|
|
|
|
logger.debug(f"Override existing timeout for profile {profile_id}")
|
|
|
|
|
|
|
|
self.pauses[profile_id] = (until, timer)
|
|
|
|
logger.debug(
|
|
|
|
f"Paused {profile_id} until {until.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
|
|
|
|
|
|
def unpause(self, profile_id: int):
|
|
|
|
"""
|
|
|
|
Return to scheduling for a profile.
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
profile_id : int
|
|
|
|
The profile to end the timeout for.
|
|
|
|
"""
|
|
|
|
profile = BackupProfileModel.get_or_none(id=profile_id)
|
|
|
|
if profile is None: # profile doesn't exist any more.
|
|
|
|
return
|
|
|
|
|
|
|
|
pause = self.pauses.get(profile_id)
|
|
|
|
if pause is None: # already unpaused
|
|
|
|
return
|
|
|
|
|
|
|
|
dummy, timer = pause
|
|
|
|
timer.stop()
|
|
|
|
del self.pauses[profile_id]
|
|
|
|
|
|
|
|
logger.debug(f"Unpaused {profile_id}")
|
|
|
|
|
|
|
|
self.set_timer_for_profile(profile_id)
|
|
|
|
|
|
|
|
def paused(self, profile_id: int) -> bool:
|
|
|
|
"""
|
|
|
|
Determine whether scheduling for a profile is paused
|
|
|
|
|
|
|
|
Parameters
|
|
|
|
----------
|
|
|
|
profile_id : int
|
|
|
|
|
|
|
|
Returns
|
|
|
|
-------
|
|
|
|
bool
|
|
|
|
"""
|
|
|
|
return self.pauses.get(profile_id) is not None
|
|
|
|
|
2022-02-20 06:04:12 +00:00
|
|
|
def set_timer_for_profile(self, profile_id: int):
|
2021-10-27 04:37:28 +00:00
|
|
|
"""
|
|
|
|
Set a timer for next scheduled backup run of this profile.
|
|
|
|
|
2021-11-15 11:02:27 +00:00
|
|
|
Removes existing jobs if set to manual only or no repo is assigned.
|
2021-10-27 04:37:28 +00:00
|
|
|
|
|
|
|
Else will look for previous scheduled backups and catch up if
|
|
|
|
schedule_make_up_missed is enabled.
|
|
|
|
|
|
|
|
Or, if catch-up is not enabled, will add interval to last run to find
|
|
|
|
next suitable backup time.
|
|
|
|
"""
|
|
|
|
profile = BackupProfileModel.get_or_none(id=profile_id)
|
2021-11-15 11:02:27 +00:00
|
|
|
if profile is None: # profile doesn't exist any more.
|
2021-10-27 04:37:28 +00:00
|
|
|
return
|
|
|
|
|
2022-02-20 06:04:12 +00:00
|
|
|
with self.lock: # Acquire lock
|
|
|
|
|
2022-05-21 11:06:10 +00:00
|
|
|
self.remove_job(profile_id) # reset schedule
|
2022-02-20 06:04:12 +00:00
|
|
|
|
2022-05-22 03:40:46 +00:00
|
|
|
pause = self.pauses.get(profile_id)
|
|
|
|
if pause is not None:
|
|
|
|
pause_end, timer = pause
|
|
|
|
if dt.now() < pause_end:
|
|
|
|
logger.debug(
|
|
|
|
'Nothing scheduled for profile %s ' +
|
|
|
|
'because of timeout until %s.',
|
|
|
|
profile_id, pause[0].strftime('%Y-%m-%d %H:%M:%S'))
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
timer.stop()
|
|
|
|
del self.pauses[profile_id]
|
|
|
|
|
2022-02-20 06:04:12 +00:00
|
|
|
if profile.repo is None: # No backups without repo set
|
|
|
|
logger.debug(
|
|
|
|
'Nothing scheduled for profile %s because of unset repo.',
|
|
|
|
profile_id)
|
2022-05-21 11:06:10 +00:00
|
|
|
# Emit signal so that e.g. the GUI can react to the new schedule
|
|
|
|
self.schedule_changed.emit(profile_id)
|
2022-05-08 13:30:48 +00:00
|
|
|
return
|
2022-02-20 06:04:12 +00:00
|
|
|
|
|
|
|
if profile.schedule_mode == 'off':
|
|
|
|
logger.debug('Scheduler for profile %s is disabled.', profile_id)
|
2022-05-21 11:06:10 +00:00
|
|
|
# Emit signal so that e.g. the GUI can react to the new schedule
|
|
|
|
self.schedule_changed.emit(profile_id)
|
2022-02-20 06:04:12 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
logger.info('Setting timer for profile %s', profile_id)
|
|
|
|
|
|
|
|
# determine last backup time
|
|
|
|
last_run_log = EventLogModel.select().where(
|
|
|
|
EventLogModel.subcommand == 'create',
|
|
|
|
EventLogModel.category == 'scheduled',
|
|
|
|
EventLogModel.profile == profile.id,
|
|
|
|
0 <= EventLogModel.returncode <= 1,
|
|
|
|
).order_by(EventLogModel.end_time.desc()).first()
|
|
|
|
|
|
|
|
if last_run_log is None:
|
|
|
|
# look for non scheduled (manual) backup runs
|
|
|
|
last_run_log = EventLogModel.select().where(
|
|
|
|
EventLogModel.subcommand == 'create',
|
|
|
|
EventLogModel.profile == profile.id,
|
|
|
|
0 <= EventLogModel.returncode <= 1,
|
|
|
|
).order_by(EventLogModel.end_time.desc()).first()
|
|
|
|
|
2022-05-21 11:06:10 +00:00
|
|
|
if last_run_log is None:
|
|
|
|
logger.info(f"Nothing scheduled for profile {profile_id} " +
|
|
|
|
"because it would be the first backup " +
|
|
|
|
"for this profile.")
|
|
|
|
self.timers[profile_id] = {
|
|
|
|
'type': ScheduleStatusType.NO_PREVIOUS_BACKUP
|
|
|
|
}
|
|
|
|
# Emit signal so that e.g. the GUI can react to the new schedule
|
|
|
|
self.schedule_changed.emit(profile_id)
|
|
|
|
return
|
|
|
|
|
2022-02-20 06:04:12 +00:00
|
|
|
# calculate next scheduled time
|
|
|
|
if profile.schedule_mode == 'interval':
|
2022-05-21 11:06:10 +00:00
|
|
|
last_time = last_run_log.end_time
|
2022-02-20 06:04:12 +00:00
|
|
|
|
|
|
|
interval = {profile.schedule_interval_unit: profile.schedule_interval_count}
|
|
|
|
next_time = last_time + timedelta(**interval)
|
|
|
|
|
|
|
|
elif profile.schedule_mode == 'fixed':
|
2022-05-21 11:06:10 +00:00
|
|
|
last_time = last_run_log.end_time + timedelta(days=1)
|
2022-02-20 06:04:12 +00:00
|
|
|
|
|
|
|
next_time = last_time.replace(
|
|
|
|
hour=profile.schedule_fixed_hour,
|
|
|
|
minute=profile.schedule_fixed_minute)
|
|
|
|
|
|
|
|
else:
|
|
|
|
# unknown schedule mode
|
|
|
|
raise ValueError(
|
|
|
|
"Unknown schedule mode '{}'".format(profile.schedule_mode))
|
|
|
|
|
|
|
|
# handle missing of a scheduled time
|
|
|
|
if next_time <= dt.now():
|
|
|
|
|
|
|
|
if profile.schedule_make_up_missed:
|
|
|
|
self.lock.release()
|
|
|
|
try:
|
|
|
|
logger.debug('Catching up by running job for %s (%s)',
|
|
|
|
profile.name, profile_id)
|
|
|
|
self.create_backup(profile_id)
|
|
|
|
finally:
|
|
|
|
self.lock.acquire() # with-statement will try to release
|
|
|
|
|
|
|
|
return # create_backup will lead to a call to this method
|
|
|
|
|
|
|
|
# calculate next time from now
|
|
|
|
if profile.schedule_mode == 'interval':
|
|
|
|
# next_time % interval should be 0
|
|
|
|
# while next_time > now
|
|
|
|
delta = dt.now() - last_time
|
|
|
|
next_time = dt.now() - delta % timedelta(**interval)
|
|
|
|
next_time += timedelta(**interval)
|
|
|
|
|
|
|
|
elif profile.schedule_mode == 'fixed':
|
|
|
|
if next_time.date() == dt.now().date():
|
|
|
|
# time for today has passed, schedule for tomorrow
|
|
|
|
next_time += timedelta(days=1)
|
|
|
|
else:
|
|
|
|
# schedule for today
|
|
|
|
next_time = dt.now().replace(
|
|
|
|
hour=profile.schedule_fixed_hour,
|
|
|
|
minute=profile.schedule_fixed_minute)
|
|
|
|
|
|
|
|
# start QTimer
|
|
|
|
timer_ms = (next_time - dt.now()).total_seconds() * 1000
|
|
|
|
|
2022-03-23 06:14:18 +00:00
|
|
|
if timer_ms < 2**31 - 1:
|
|
|
|
logger.debug('Scheduling next run for %s', next_time)
|
|
|
|
|
2022-05-21 11:06:10 +00:00
|
|
|
timer = QTimer()
|
2022-03-23 06:14:18 +00:00
|
|
|
timer.setSingleShot(True)
|
|
|
|
timer.setInterval(int(timer_ms))
|
|
|
|
timer.timeout.connect(lambda: self.create_backup(profile_id))
|
|
|
|
timer.start()
|
2022-02-20 06:04:12 +00:00
|
|
|
|
2022-05-21 11:06:10 +00:00
|
|
|
self.timers[profile_id] = {
|
|
|
|
'qtt': timer,
|
|
|
|
'dt': next_time,
|
|
|
|
'type': ScheduleStatusType.SCHEDULED
|
|
|
|
}
|
2022-03-23 06:14:18 +00:00
|
|
|
else:
|
|
|
|
# int to big to pass it to qt which expects a c++ int
|
|
|
|
# wait 15 min for regular reschedule
|
|
|
|
logger.debug(
|
2022-03-24 06:27:07 +00:00
|
|
|
f"Couldn't schedule for {next_time} because "
|
2022-03-23 06:14:18 +00:00
|
|
|
f"timer value {timer_ms} too large.")
|
2021-11-12 07:05:31 +00:00
|
|
|
|
2022-05-21 11:06:10 +00:00
|
|
|
self.timers[profile_id] = {
|
|
|
|
'dt': next_time,
|
|
|
|
'type': ScheduleStatusType.TOO_FAR_AHEAD
|
|
|
|
}
|
|
|
|
|
2022-02-09 10:55:26 +00:00
|
|
|
# Emit signal so that e.g. the GUI can react to the new schedule
|
|
|
|
self.schedule_changed.emit(profile_id)
|
|
|
|
|
2021-10-27 04:37:28 +00:00
|
|
|
def reload_all_timers(self):
|
|
|
|
logger.debug('Refreshing all scheduler timers')
|
2018-11-17 08:51:53 +00:00
|
|
|
for profile in BackupProfileModel.select():
|
2021-10-27 04:37:28 +00:00
|
|
|
self.set_timer_for_profile(profile.id)
|
2018-10-31 16:09:01 +00:00
|
|
|
|
|
|
|
def next_job(self):
|
2022-05-29 13:44:17 +00:00
|
|
|
now = dt.now()
|
|
|
|
|
|
|
|
def is_scheduled(timer):
|
|
|
|
return (
|
|
|
|
timer["type"] == ScheduleStatusType.SCHEDULED
|
|
|
|
and timer["qtt"].isActive()
|
|
|
|
and timer["dt"] >= now
|
|
|
|
)
|
|
|
|
|
|
|
|
scheduled = {profile_id: timer for profile_id, timer in self.timers.items() if is_scheduled(timer)}
|
|
|
|
if len(scheduled) == 0:
|
|
|
|
return self.tr("None scheduled")
|
|
|
|
|
|
|
|
closest_job = min(scheduled.items(), key=lambda item: item[1]["dt"])
|
|
|
|
profile_id, timer = closest_job
|
|
|
|
time = timer["dt"]
|
|
|
|
profile = BackupProfileModel.get_or_none(id=profile_id)
|
|
|
|
|
|
|
|
time_format = "%H:%M"
|
|
|
|
if time - now > timedelta(days=1):
|
|
|
|
time_format = "%b %d, %H:%M"
|
|
|
|
return f"{time.strftime(time_format)} ({profile.name})"
|
2018-11-17 08:51:53 +00:00
|
|
|
|
2022-05-21 11:06:10 +00:00
|
|
|
def next_job_for_profile(self, profile_id: int) -> ScheduleStatus:
|
2021-10-27 04:37:28 +00:00
|
|
|
job = self.timers.get(profile_id)
|
2018-11-01 01:10:11 +00:00
|
|
|
if job is None:
|
2022-05-21 11:06:10 +00:00
|
|
|
return ScheduleStatus(ScheduleStatusType.UNSCHEDULED)
|
|
|
|
return ScheduleStatus(job['type'], time=job.get('dt'))
|
2018-10-31 16:09:01 +00:00
|
|
|
|
2021-10-27 04:37:28 +00:00
|
|
|
def create_backup(self, profile_id):
|
2019-01-13 01:51:35 +00:00
|
|
|
notifier = VortaNotifications.pick()
|
2021-10-27 04:37:28 +00:00
|
|
|
profile = BackupProfileModel.get_or_none(id=profile_id)
|
|
|
|
|
|
|
|
if profile is None:
|
|
|
|
logger.info('Profile not found. Maybe deleted?')
|
|
|
|
return
|
2019-01-13 01:51:35 +00:00
|
|
|
|
2021-11-12 07:05:31 +00:00
|
|
|
# Skip if a job for this profile (repo) is already in progress
|
|
|
|
if self.app.jobs_manager.is_worker_running(site=profile.repo.id):
|
|
|
|
logger.debug('A job for repo %s is already active.', profile.repo.id)
|
2022-05-22 03:40:46 +00:00
|
|
|
self.pause(profile_id)
|
2021-11-12 07:05:31 +00:00
|
|
|
return
|
|
|
|
|
2022-05-22 03:40:46 +00:00
|
|
|
with self.lock:
|
|
|
|
logger.info('Starting background backup for %s', profile.name)
|
|
|
|
notifier.deliver(self.tr('Vorta Backup'),
|
|
|
|
self.tr('Starting background backup for %s.') % profile.name,
|
|
|
|
level='info')
|
|
|
|
msg = BorgCreateJob.prepare(profile)
|
|
|
|
if msg['ok']:
|
|
|
|
logger.info('Preparation for backup successful.')
|
|
|
|
msg['category'] = 'scheduled'
|
|
|
|
job = BorgCreateJob(msg['cmd'], msg, profile.repo.id)
|
|
|
|
job.result.connect(self.notify)
|
|
|
|
self.app.jobs_manager.add_job(job)
|
|
|
|
else:
|
|
|
|
logger.error('Conditions for backup not met. Aborting.')
|
|
|
|
logger.error(msg['message'])
|
|
|
|
notifier.deliver(self.tr('Vorta Backup'),
|
|
|
|
translate('messages', msg['message']),
|
|
|
|
level='error')
|
|
|
|
self.pause(profile_id)
|
2021-10-04 11:31:41 +00:00
|
|
|
|
|
|
|
def notify(self, result):
|
|
|
|
notifier = VortaNotifications.pick()
|
|
|
|
profile_name = result['params']['profile_name']
|
2021-11-15 11:18:11 +00:00
|
|
|
profile_id = result['params']['profile'].id
|
2021-10-04 11:31:41 +00:00
|
|
|
|
|
|
|
if result['returncode'] in [0, 1]:
|
|
|
|
notifier.deliver(self.tr('Vorta Backup'),
|
|
|
|
self.tr('Backup successful for %s.') % profile_name,
|
|
|
|
level='info')
|
|
|
|
logger.info('Backup creation successful.')
|
2022-05-22 03:40:46 +00:00
|
|
|
# unpause scheduler
|
|
|
|
self.unpause(result['params']['profile_id'])
|
|
|
|
|
2021-10-04 11:31:41 +00:00
|
|
|
self.post_backup_tasks(profile_id)
|
|
|
|
else:
|
|
|
|
notifier.deliver(self.tr('Vorta Backup'), self.tr('Error during backup creation.'), level='error')
|
|
|
|
logger.error('Error during backup creation.')
|
2022-05-22 03:40:46 +00:00
|
|
|
# pause scheduler
|
|
|
|
# if a scheduled backup fails the scheduler should pause
|
|
|
|
# temporarily.
|
|
|
|
self.pause(result['params']['profile_id'])
|
2018-11-04 15:37:46 +00:00
|
|
|
|
2021-10-27 04:37:28 +00:00
|
|
|
self.set_timer_for_profile(profile_id)
|
|
|
|
|
2018-11-17 08:51:53 +00:00
|
|
|
def post_backup_tasks(self, profile_id):
|
2018-11-04 15:37:46 +00:00
|
|
|
"""
|
|
|
|
Pruning and checking after successful backup.
|
|
|
|
"""
|
2018-11-17 08:51:53 +00:00
|
|
|
profile = BackupProfileModel.get(id=profile_id)
|
2018-11-22 03:26:26 +00:00
|
|
|
logger.info('Doing post-backup jobs for %s', profile.name)
|
2018-11-04 15:37:46 +00:00
|
|
|
if profile.prune_on:
|
2021-10-04 11:31:41 +00:00
|
|
|
msg = BorgPruneJob.prepare(profile)
|
2018-11-04 15:37:46 +00:00
|
|
|
if msg['ok']:
|
2021-10-04 11:31:41 +00:00
|
|
|
job = BorgPruneJob(msg['cmd'], msg, profile.repo.id)
|
2021-11-12 07:05:31 +00:00
|
|
|
self.app.jobs_manager.add_job(job)
|
2018-11-04 15:37:46 +00:00
|
|
|
|
2019-01-20 03:50:10 +00:00
|
|
|
# Refresh archives
|
2021-10-04 11:31:41 +00:00
|
|
|
msg = BorgListRepoJob.prepare(profile)
|
2018-11-04 15:37:46 +00:00
|
|
|
if msg['ok']:
|
2021-10-04 11:31:41 +00:00
|
|
|
job = BorgListRepoJob(msg['cmd'], msg, profile.repo.id)
|
2021-11-12 07:05:31 +00:00
|
|
|
self.app.jobs_manager.add_job(job)
|
2018-11-04 08:23:17 +00:00
|
|
|
|
2022-02-20 06:04:12 +00:00
|
|
|
validation_cutoff = dt.now() - timedelta(days=7 * profile.validation_weeks)
|
2018-11-04 15:37:46 +00:00
|
|
|
recent_validations = EventLogModel.select().where(
|
2018-11-22 20:21:52 +00:00
|
|
|
(
|
|
|
|
EventLogModel.subcommand == 'check'
|
|
|
|
) & (
|
|
|
|
EventLogModel.start_time > validation_cutoff
|
|
|
|
) & (
|
|
|
|
EventLogModel.repo_url == profile.repo.url
|
|
|
|
)
|
2018-11-04 15:37:46 +00:00
|
|
|
).count()
|
|
|
|
if profile.validation_on and recent_validations == 0:
|
2021-10-04 11:31:41 +00:00
|
|
|
msg = BorgCheckJob.prepare(profile)
|
2018-11-04 15:37:46 +00:00
|
|
|
if msg['ok']:
|
2021-10-04 11:31:41 +00:00
|
|
|
job = BorgCheckJob(msg['cmd'], msg, profile.repo.id)
|
2021-11-12 07:05:31 +00:00
|
|
|
self.app.jobs_manager.add_job(job)
|
2018-11-22 03:26:26 +00:00
|
|
|
|
|
|
|
logger.info('Finished background task for profile %s', profile.name)
|
2021-11-15 11:18:11 +00:00
|
|
|
|
|
|
|
def remove_job(self, profile_id):
|
2021-11-19 13:26:59 +00:00
|
|
|
if profile_id in self.timers:
|
2022-05-21 11:06:10 +00:00
|
|
|
qtimer = self.timers[profile_id].get('qtt')
|
|
|
|
if qtimer is not None:
|
|
|
|
qtimer.stop()
|
|
|
|
|
2021-11-19 13:26:59 +00:00
|
|
|
del self.timers[profile_id]
|