Files
Juan DenisandClaude Opus 5 e8e94a126f feat(plugins): let extensions contribute backup kinds
An extension with something worth backing up had to hand-roll it — no policy,
no cron mirror, no run history, no retention, no offsite copy, no verification,
no restore, and no place in the Protection panel. A registered kind inherits
every one of those, because everything downstream of the target_type switches
is already generic: the run lifecycle, retention (count, age, incremental-chain
ancestry), the API blueprint, and the panel itself, which threads target_type
through without ever switching on its value.

No migration: target_type is an unconstrained String, and the allowed set lived
purely in a Python tuple. A kind supplies resolve() and execute(), plus an
optional restore().

Four switches had to learn about the registry, and the order matters in three
of them:

- validate_target_type is the keystone — every policy route funnels through it,
  so accepting a registered kind opens the whole blueprint at once.
- _resolve_target and _execute_backup check the registry BEFORE their core
  branches, because both end in an application fallthrough that swallows any
  unrecognised type.
- run_restore_job likewise. This is the sharp one: without it a plugin's
  archive would be unpacked over an app directory by core's else-branch.

A kind that didn't implement restore is refused at request time with a reason,
rather than queueing a job that takes the policy lock and runs a safety backup
before failing.

The registry normalises what plugins hand back — target_type is forced rather
than defaulted, so a kind cannot describe itself as an 'application' and be
restored as one; execute() must return (path, size, meta) with a real path; and
the metadata retention and the panel read is filled in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 13:59:51 -04:00

66 lines
2.5 KiB
Python

"""Plugin-facing SDK for backups.
from app.plugins_sdk import backups
def resolve(policy):
world = World.query.get(policy.target_id)
if not world:
raise ValueError('World not found')
return {'name': world.name, 'root_path': world.directory, 'world': world}
def execute(policy, target, kind):
path = tar_up(target['root_path'])
return path, os.path.getsize(path), {'world': target['name']}
def restore(policy, target, run, options):
untar(run.storage_path, target['root_path'])
backups.register('minecraft.world', resolve=resolve, execute=execute,
restore=restore)
Registering a kind means never writing backup plumbing again: the policy row
and its cron schedule, run history, retention (count and age, aware of
incremental chains), offsite copy, verification, restore, and the Protection
panel — which is already generic over the target type — all come from the
panel. Create a policy the way core does::
from app.services.backup_policy_service import BackupPolicyService
BackupPolicyService.get_or_create_policy('minecraft.world', world.id)
Then the operator schedules, runs, prunes and restores it from the UI.
``restore`` is optional but strongly encouraged. Without it the panel refuses
the restore up front and says so, rather than queueing a job that fails — and
critically, rather than falling through to core's application restore, which
would unpack your archive over an app directory.
"""
from app.services import backup_kind_registry
class BackupsSdk:
"""Stable backup surface for plugins."""
def register(self, target_type, resolve, execute, restore=None, replace=False):
"""Register a backup target type.
``resolve(policy) -> dict`` describes the live target (include
``name``, and ``root_path`` if it is a directory); raise if it is gone.
``execute(policy, target, kind) -> (storage_path, size_bytes, meta)``
produces the artifact. ``restore(policy, target, run, options)`` puts
one back.
Namespace the type after your plugin (``minecraft.world``); core's bare
words are reserved.
"""
return backup_kind_registry.register(
target_type, resolve=resolve, execute=execute, restore=restore,
replace=replace)
def kinds(self):
"""Backup target types registered by plugins."""
return backup_kind_registry.kinds()
backups = BackupsSdk()