feat: person excludes for auto-organize rules, backup & restore system

Add person exclude criteria to Immich auto-organize — assets containing
excluded persons are filtered out after candidate gathering. Also adds
full backup/restore system with export, import, scheduled backups, and
retention management.
This commit is contained in:
2026-04-02 14:13:42 +03:00
parent 6e51164f8e
commit 6b2211353d
13 changed files with 2191 additions and 2 deletions
@@ -38,6 +38,9 @@ async def start_scheduler() -> None:
from .command_sync import start_sync_scheduler
start_sync_scheduler()
# Load scheduled backup job if enabled
await _load_backup_job()
def _schedule_event_cleanup() -> None:
"""Schedule a daily job to delete EventLog entries older than 90 days."""
@@ -321,3 +324,103 @@ async def _run_action(action_id: int) -> None:
await run_action(action_id, trigger="scheduled")
except Exception as e:
_LOGGER.error("Error running action %d: %s", action_id, e)
# ---------------------------------------------------------------------------
# Scheduled backup
# ---------------------------------------------------------------------------
_BACKUP_JOB_ID = "scheduled_backup"
async def _load_backup_job() -> None:
"""Load scheduled backup job from settings if enabled."""
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession as _AS
from ..database.engine import get_engine
from ..database.models import AppSetting
engine = get_engine()
async with _AS(engine) as session:
enabled_row = await session.get(AppSetting, "backup_scheduled_enabled")
interval_row = await session.get(AppSetting, "backup_scheduled_interval_hours")
enabled = enabled_row and enabled_row.value == "true"
if not enabled:
return
interval_hours = int(interval_row.value) if interval_row and interval_row.value else 24
scheduler = get_scheduler()
scheduler.add_job(
_run_scheduled_backup,
"interval",
hours=interval_hours,
id=_BACKUP_JOB_ID,
replace_existing=True,
max_instances=1,
)
_LOGGER.info("Scheduled backup every %dh", interval_hours)
async def schedule_backup(interval_hours: int = 24) -> None:
"""Add or update the scheduled backup job."""
scheduler = get_scheduler()
if scheduler.get_job(_BACKUP_JOB_ID):
scheduler.remove_job(_BACKUP_JOB_ID)
scheduler.add_job(
_run_scheduled_backup,
"interval",
hours=interval_hours,
id=_BACKUP_JOB_ID,
replace_existing=True,
max_instances=1,
)
_LOGGER.info("Scheduled backup every %dh", interval_hours)
async def unschedule_backup() -> None:
"""Remove the scheduled backup job."""
scheduler = get_scheduler()
if scheduler.get_job(_BACKUP_JOB_ID):
scheduler.remove_job(_BACKUP_JOB_ID)
_LOGGER.info("Unscheduled backup job")
async def _run_scheduled_backup() -> None:
"""Run a scheduled backup (called by APScheduler)."""
from sqlmodel.ext.asyncio.session import AsyncSession as _AS
from ..database.engine import get_engine
from ..database.models import AppSetting, User
from ..config import settings as app_config
from .backup_schema import SecretsMode
from .backup_service import export_backup_to_file, cleanup_old_backups
try:
engine = get_engine()
async with _AS(engine) as session:
# Read settings
secrets_row = await session.get(AppSetting, "backup_secrets_mode")
retention_row = await session.get(AppSetting, "backup_retention_count")
secrets_mode = SecretsMode(secrets_row.value) if secrets_row and secrets_row.value else SecretsMode.EXCLUDE
retention = int(retention_row.value) if retention_row and retention_row.value else 5
# Find admin user (first admin) for ownership context
from sqlmodel import select
admin_result = await session.exec(
select(User).where(User.role == "admin")
)
admin = admin_result.first()
if not admin:
_LOGGER.warning("No admin user found, skipping scheduled backup")
return
backup_dir = app_config.data_dir / "backups"
await export_backup_to_file(session, admin.id, backup_dir, secrets_mode)
# Cleanup outside the session
cleanup_old_backups(backup_dir, keep=retention)
except Exception as e:
_LOGGER.error("Scheduled backup failed: %s", e)