mirror of
https://github.com/ep1cman/unifi-protect-backup.git
synced 2025-12-05 23:53:30 +00:00
91 lines
3.6 KiB
Python
91 lines
3.6 KiB
Python
# noqa: D100
|
|
|
|
import logging
|
|
import time
|
|
from datetime import datetime
|
|
|
|
import aiosqlite
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from unifi_protect_backup.utils import run_command, wait_until
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def delete_file(file_path, rclone_purge_args):
|
|
"""Delete `file_path` via rclone."""
|
|
returncode, stdout, stderr = await run_command(f'rclone delete -vv "{file_path}" {rclone_purge_args}')
|
|
if returncode != 0:
|
|
logger.error(f" Failed to delete file: '{file_path}'")
|
|
|
|
|
|
async def tidy_empty_dirs(base_dir_path):
|
|
"""Delete any empty directories in `base_dir_path` via rclone."""
|
|
returncode, stdout, stderr = await run_command(f'rclone rmdirs -vv --ignore-errors --leave-root "{base_dir_path}"')
|
|
if returncode != 0:
|
|
logger.error(" Failed to tidy empty dirs")
|
|
|
|
|
|
class Purge:
|
|
"""Deletes old files from rclone remotes."""
|
|
|
|
def __init__(
|
|
self,
|
|
db: aiosqlite.Connection,
|
|
retention: relativedelta,
|
|
rclone_destination: str,
|
|
interval: relativedelta | None,
|
|
rclone_purge_args: str = "",
|
|
):
|
|
"""Init.
|
|
|
|
Args:
|
|
db (aiosqlite.Connection): Async SQlite database connection to purge clips from
|
|
retention (relativedelta): How long clips should be kept
|
|
rclone_destination (str): What rclone destination the clips are stored in
|
|
interval (relativedelta): How often to purge old clips
|
|
rclone_purge_args (str): Optional extra arguments to pass to `rclone delete` directly.
|
|
|
|
"""
|
|
self._db: aiosqlite.Connection = db
|
|
self.retention: relativedelta = retention
|
|
self.rclone_destination: str = rclone_destination
|
|
self.interval: relativedelta = interval if interval is not None else relativedelta(days=1)
|
|
self.rclone_purge_args: str = rclone_purge_args
|
|
|
|
async def start(self):
|
|
"""Run main loop."""
|
|
while True:
|
|
try:
|
|
deleted_a_file = False
|
|
|
|
# For every event older than the retention time
|
|
retention_oldest_time = time.mktime((datetime.now() - self.retention).timetuple())
|
|
async with self._db.execute(
|
|
f"SELECT * FROM events WHERE end < {retention_oldest_time}"
|
|
) as event_cursor:
|
|
async for event_id, event_type, camera_id, event_start, event_end in event_cursor: # noqa: B007
|
|
logger.info(f"Purging event: {event_id}.")
|
|
|
|
# For every backup for this event
|
|
async with self._db.execute(f"SELECT * FROM backups WHERE id = '{event_id}'") as backup_cursor:
|
|
async for _, remote, file_path in backup_cursor:
|
|
logger.debug(f" Deleted: {remote}:{file_path}")
|
|
await delete_file(f"{remote}:{file_path}", self.rclone_purge_args)
|
|
deleted_a_file = True
|
|
|
|
# delete event from database
|
|
# entries in the `backups` table are automatically deleted by sqlite triggers
|
|
await self._db.execute(f"DELETE FROM events WHERE id = '{event_id}'")
|
|
await self._db.commit()
|
|
|
|
if deleted_a_file:
|
|
await tidy_empty_dirs(self.rclone_destination)
|
|
|
|
except Exception as e:
|
|
logger.error("Unexpected exception occurred during purge:", exc_info=e)
|
|
|
|
next_purge_time = datetime.now() + self.interval
|
|
logger.extra_debug(f"sleeping until {next_purge_time}")
|
|
await wait_until(next_purge_time)
|