from datetime import timedelta from threading import Lock from asyncio import Event from injector import inject, singleton from watchdog.events import FileSystemEvent, FileSystemEventHandler from watchdog.observers import Observer from backup.config import Config, Setting, Startable from backup.time import Time from backup.worker import Trigger from backup.logger import getLogger from backup.ha import HaSource from asyncio import get_event_loop from os.path import join, abspath logger = getLogger(__name__) REPORT_DELAY_SECONDS = 5 LOG_INTERVAL = timedelta(seconds=30) CHANGES_CHECK_DELAY = timedelta(seconds=10) @singleton class Watcher(Trigger, FileSystemEventHandler, Startable): @inject def __init__(self, time: Time, config: Config, source: HaSource): super().__init__() self.time = time self.config: Config = config self.observer: Observer = Observer() self._source = source self.lock: Lock = Lock() self.noticed_change_signal = Event() self._changes_have_happened = False self._last_change_time = None self._last_log_time = None self._last_notified_time = None self._loop = get_event_loop() async def start(self): if not self.config.get(Setting.WATCH_BACKUP_DIRECTORY): return self.observer.schedule(self, self.config.get( Setting.BACKUP_DIRECTORY_PATH), recursive=False) self.observer.start() def name(self): return "Backup Directory Watcher" def on_any_event(self, event: FileSystemEvent): if event.is_directory: # ignore any directory level events that bubble up return if event.event_type in ['modified', 'created']: return logger.trace("Backup directory modified: %s %s", event.event_type, event.src_path) with self.lock: self._last_change_time = self.time.now() self._changes_have_happened = True # Provide periodic log messages to indicate we'll backup soon. if not self._last_log_time: logger.info("A backup directory file was modified, we'll check for new backups soon.") self._last_log_time = self.time.now() elif (self.time.now() - self._last_log_time) > LOG_INTERVAL: logger.info("The backup directory is still being written to, waiting...") self._last_log_time = self.time.now() if not self.noticed_change_signal.is_set(): self._loop.call_soon_threadsafe(self.noticed_change_signal.set) async def check(self): if not self._changes_have_happened: return check_backup_source = False with self.lock: if not self._last_change_time: return if self.time.now() - self._last_change_time > CHANGES_CHECK_DELAY: check_backup_source = True self._last_change_time = None self._changes_have_happened = False self._last_log_time = None if check_backup_source: logger.debug("Checking backup source for changes...") await self._source.get() if self._source.query_had_changes: self.trigger() return await super().check() async def stop(self): if not self.config.get(Setting.WATCH_BACKUP_DIRECTORY): return self.observer.stop() self.observer.join()