Skip to content

Releases

v1.3.0

Choose a tag to compare

@ONyklicek ONyklicek released this 17 Oct 08:20

Fixed

  • Retention judged every backup in the path, not the profile's own.
    RetentionManager::apply() listed the whole destination and deleted whatever
    fell outside the keep-set, without ever asking which profile a backup belonged
    to. Since path defaults to backups for every profile, two profiles sharing
    a disk is the default arrangement — so a nightly backup:clean run under the
    default profile's policy pruned the fortnightly full backups sitting beside it.
    Nothing reported it; keep_latest = 5 on the other profile simply never held.
    Every stored backup now carries the profile from its manifest, and both
    retention engines count and delete only their own. Backups whose manifest
    predates the tag (or cannot be read) still count as the asking profile's, so an
    upgrade keeps cleaning up exactly what it cleaned up before.

Added

  • backup:clean can go to the queue, symmetrically with backup:run: with
    BACKUP_QUEUE=true it dispatches the new CleanBackupsJob and --sync forces
    it inline. Scheduled without one, a cleanup that reference-counts an entire
    blob pool ran inside the scheduler's process on the web machine — no worker
    memory limit, nothing in failed_jobs, invisible to any queue dashboard.
    --dry-run never queues: its whole product is the preview it prints.
  • queue.restore_timeout (BACKUP_QUEUE_RESTORE_TIMEOUT) for jobs that
    restore. A restore downloads, decrypts, unpacks and takes its own pre-restore
    snapshot first, so it routinely outlasts the backup that produced it. Null —
    the default — uses queue.timeout.
  • CleanupManager, the one place a profile's retention policy is applied across
    its destinations. The CLI, the queue, the dashboard and the MCP tool had each
    grown their own copy of the same two-engine sequence; they now share this one,
    so none of them can drift on which backups a policy governs.
  • StoredBackup::$profile and StoredBackup::belongsTo().
  • CleanupReport, describing what retention did to one destination.

Changed

  • Listings are scoped to their profile. backup:list, backup:verify and
    backup:restore no longer offer up a neighbouring profile's backups from a
    shared destination — the set you list is now the set that profile's retention
    governs.
  • An empty or null queue.queue falls back to backups instead of dispatching
    onto a queue named '', which no worker listens on.
  • RetentionManager::apply() and the SnapshotDriver::applyRetention() contract
    take an optional trailing ?string $profile. Existing calls keep working
    unchanged (null means "every backup in the path", the old behaviour); a custom
    SnapshotDriver implementation needs the new parameter in its signature.

Changed

  • Test suite moved to Pest 4, with Pest 5 supported alongside it
    (^4.0|^5.0). Which one is installed follows the environment: Pest 5 needs
    PHP 8.4 and Symfony 8, so a Laravel 12 install stays on Pest 4 and gets the
    newer major only with Laravel 13. Both were run against the full suite.

v1.2.1

Choose a tag to compare

@ONyklicek ONyklicek released this 04 Sep 18:24

Fixed

  • Retention judged every backup in the path, not the profile's own.
    RetentionManager::apply() listed the whole destination and deleted whatever
    fell outside the keep-set, without ever asking which profile a backup belonged
    to. Since path defaults to backups for every profile, two profiles sharing
    a disk is the default arrangement — so a nightly backup:clean run under the
    default profile's policy pruned the fortnightly full backups sitting beside it.
    Nothing reported it; keep_latest = 5 on the other profile simply never held.
    Every stored backup now carries the profile from its manifest, and both
    retention engines count and delete only their own. Backups whose manifest
    predates the tag (or cannot be read) still count as the asking profile's, so an
    upgrade keeps cleaning up exactly what it cleaned up before.

Added

  • backup:clean can go to the queue, symmetrically with backup:run: with
    BACKUP_QUEUE=true it dispatches the new CleanBackupsJob and --sync forces
    it inline. Scheduled without one, a cleanup that reference-counts an entire
    blob pool ran inside the scheduler's process on the web machine — no worker
    memory limit, nothing in failed_jobs, invisible to any queue dashboard.
    --dry-run never queues: its whole product is the preview it prints.
  • queue.restore_timeout (BACKUP_QUEUE_RESTORE_TIMEOUT) for jobs that
    restore. A restore downloads, decrypts, unpacks and takes its own pre-restore
    snapshot first, so it routinely outlasts the backup that produced it. Null —
    the default — uses queue.timeout.
  • CleanupManager, the one place a profile's retention policy is applied across
    its destinations. The CLI, the queue, the dashboard and the MCP tool had each
    grown their own copy of the same two-engine sequence; they now share this one,
    so none of them can drift on which backups a policy governs.
  • StoredBackup::$profile and StoredBackup::belongsTo().
  • CleanupReport, describing what retention did to one destination.

Changed

  • Listings are scoped to their profile. backup:list, backup:verify and
    backup:restore no longer offer up a neighbouring profile's backups from a
    shared destination — the set you list is now the set that profile's retention
    governs.
  • An empty or null queue.queue falls back to backups instead of dispatching
    onto a queue named '', which no worker listens on.
  • RetentionManager::apply() and the SnapshotDriver::applyRetention() contract
    take an optional trailing ?string $profile. Existing calls keep working
    unchanged (null means "every backup in the path", the old behaviour); a custom
    SnapshotDriver implementation needs the new parameter in its signature.

Changed

  • Test suite moved to Pest 4, with Pest 5 supported alongside it
    (^4.0|^5.0). Which one is installed follows the environment: Pest 5 needs
    PHP 8.4 and Symfony 8, so a Laravel 12 install stays on Pest 4 and gets the
    newer major only with Laravel 13. Both were run against the full suite.

v1.2.0

Choose a tag to compare

@ONyklicek ONyklicek released this 22 Aug 13:54

Fixed

  • MariaDB hosts without the legacy mysql* symlinks could not be backed up.
    Both the dumper and the restorer asked for mysqldump / mysql by name,
    while MariaDB 11 renamed its clients to mariadb-dump / mariadb and left
    the old names as deprecated symlinks that several distributions and container
    images no longer ship — so a backup failed with "The [mysqldump] binary could
    not be found", despite MariaDB being a documented driver. Each connection now
    asks for the clients its own driver names, best first, so either spelling
    works; where both are installed the one matching the server wins, since
    MySQL's mysqldump can emit SQL a MariaDB server will not read back.
    mariadb-dump and mariadb are configurable like the other binaries
    (BACKUP_MARIADB_DUMP_PATH, BACKUP_MARIADB_PATH).

Added

  • An MCP server, so an AI assistant can set backups up for you
    (php artisan backup:mcp). It speaks the Model Context Protocol over
    stdin/stdout with no extra dependency, and exposes twelve tools: a setup
    guide, an overview of the installation, the environment (disks, connections,
    binaries, free space), the resolved profiles, the full configuration
    reference, a validator, the stored backups, a retention preview, and the
    operations themselves — configure, run, verify, prune. The point is that an
    assistant reads this installation before it proposes anything, instead of
    inventing a disk name or a config key that sounds plausible.

    Writing is bounded on every side. backup_configure accepts only keys the
    reference documents, with values of the declared type; it defaults to a dry
    run that returns both the exact edits and the findings the result would
    have, and refuses to apply a change that would leave errors behind unless
    explicitly forced. A setting the config file reads through env() is written
    to .env rather than baked in as a literal, secrets are written but never
    echoed back, the config file is edited by token so its comments survive, the
    edited source is re-parsed before it is written, the previous file is copied
    aside, and the write itself is atomic.

    Permissions are per access level, not per tool name, so a tool an add-on
    contributes is governed by the same rules: reading and running backups is
    always allowed, configuration writes are on by default,
    deleting backups is off until BACKUP_MCP_ALLOW_DESTRUCTIVE=true, and the
    server refuses to start outside mcp.environments (local by default).
    Restore is not exposed over MCP at all — it overwrites a live application, so
    it stays a deliberate human act. See docs/14-mcp.md.

  • ConfigValidator — a standing check on the configuration, reachable from
    the MCP server and usable on its own. It reports what would fail (a
    destination disk that does not exist, a missing dump binary, encryption
    enabled without a key, a capability that needs Pro) and what would quietly
    protect nothing: no off-site destination, no schedule, nobody on the failure
    mail, a keep_latest below 1 that would delete every backup it finds. Each
    finding names the key and the fix.

  • Configuration writing primitives used by the MCP server and available to
    anything else that needs them: ConfigReference (the package's own
    configuration described as data), EnvFile (comment-preserving .env
    upserts), PhpConfigFile (token-level edits to a published config file),
    ConfigWriter (routes each key to the right file), BackupFinder and
    Edition.

  • The manifest records which migrations had run (migrations.ran, manifest
    schema 1.2). A database dump only fits a codebase whose schema expectations
    match it, and the app version does not establish that — two commits can share
    a version string and differ by a migration. The dump has always contained the
    migrations table, but only after decrypting and unpacking it; the same fact
    now sits in the sidecar, so a backup can be checked against a codebase, and two
    backups diffed against each other, without touching the archive. MigrationState
    exposes latest(), count(), ahead() and matches(). Reading is fail-soft:
    no database, an unreachable one or one that never migrated records null rather
    than failing the backup, and manifests written before 1.2 read back as null —
    "not recorded", not "none had run".

  • DeleteManager — targeted deletion of named backups. Retention prunes on
    a policy; this removes exactly what the caller names, for a corrupted archive
    or a run kept by mistake. Guarded rather than trusted: it refuses outright if
    the deletion would leave the destination with no backup at all, and every name
    is resolved before anything is removed, so an unknown one aborts the whole
    call instead of deleting half a selection. Deleting an incremental snapshot
    removes only its manifest — its blocks stay in the shared pool until a
    retention run garbage-collects them, because other snapshots may still
    reference them. Supports a dry run.

  • BackupDeleted event. Dispatched per deleted backup, carrying an optional
    actor so a listener can record who removed what. Retention stays silent; only
    a targeted deletion fires it.

  • The package now contributes a Backup Manager section to php artisan about, reporting the default profile, encryption, manifest signing, whether
    the incremental (Pro) engine is active, and scheduling.

v1.1.2

Choose a tag to compare

@ONyklicek ONyklicek released this 06 Aug 10:01

Release v1.1.2

v1.1.1

Choose a tag to compare

@ONyklicek ONyklicek released this 19 Jul 22:49

Security

  • Manifest signature is now enforced on restore. When signing is enabled
    (a Signer is bound), restore refuses any backup whose manifest signature is
    missing or invalid instead of trusting the checksum alone — closing a gap where
    a tampered manifest, or one with its signature stripped, could still be
    restored. Environments without signing keep the checksum-only behaviour, so
    unsigned backups still restore there; --no-verify remains the explicit
    opt-out.
  • Database dump path now goes through the traversal guard. RestoreManager
    resolves each manifest-declared database dump path with Support\SafePath, so
    a hostile archived_path (../../…) can no longer make restore read and pipe
    an arbitrary host file into the database client.
  • backup:upgrade keeps the licence token off the process argv. Credentials
    are written straight into auth.json (merging any existing entries, mode
    0600) rather than via composer config --auth, so the token is no longer
    visible to other local users through ps//proc.

Fixed

  • Corrected the Pro package name to nyoncode/laravel-backup-manager-pro
    throughout config, the backup:upgrade command, runtime messages and docs so
    activation targets the actually-published package.