Files
ghassanandClaude Opus 4.7 8ab59fc1bb chore(backup): ship takeone:backup command + nightly scheduler entry
Moves BackupCommand and config/backup.php from the pre-audit WIP that
got stashed on 2026-10-21 to master, and registers the command in the
Laravel scheduler so it runs at 03:30 Asia/Bahrain nightly.

The command handles sqlite (VACUUM INTO after a WAL checkpoint) and
mysql (mysqldump | gzip, verified with gzip -t) backends; verifies
every snapshot by re-opening it with PDO + PRAGMA integrity_check +
a sanity SELECT; ships an off-server copy via Storage::disk() when
BACKUP_DISK is set; and prunes artifacts older than BACKUP_RETAIN_DAYS
while always keeping at least BACKUP_KEEP_MINIMUM.

Scheduler entry mirrors the nas-auto-sync shape — withoutOverlapping
+ runInBackground so a slow upload doesn't block the next tick.

.env knobs:
  BACKUP_PATH          (default: data/backups)
  BACKUP_RETAIN_DAYS   (default: 14)
  BACKUP_KEEP_MINIMUM  (default: 3)
  BACKUP_DISK          (unset — warns on every run until configured)
  BACKUP_MEDIA_DEST    (unset — required when running with --media)
  BACKUP_MEDIA_DELETE  (default: false)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-10-21 13:14:21 +03:00

75 lines
2.7 KiB
PHP

<?php
/*
|--------------------------------------------------------------------------
| Backup
|--------------------------------------------------------------------------
|
| Settings for `php artisan takeone:backup`.
|
| The database and the media library are backed up by DIFFERENT mechanisms on
| purpose. The database is small and must be captured as a consistent
| point-in-time snapshot, so it is copied in full on every run. The media
| library is gigabytes of immutable uploads, so it is mirrored incrementally
| and only when asked — tarring it nightly would burn hours of CPU and fill
| the disk with near-identical archives.
|
*/
return [
// Where snapshots are written. Lives under /data, which is git-ignored, so
// a backup can never be committed by accident.
'path' => env('BACKUP_PATH', base_path('data/backups')),
/*
|--------------------------------------------------------------------------
| Retention
|--------------------------------------------------------------------------
|
| Artifacts older than retain_days are pruned, but the newest keep_minimum
| are ALWAYS kept regardless of age — a box that sat idle for a month must
| still have something to restore from.
|
*/
'retain_days' => (int) env('BACKUP_RETAIN_DAYS', 14),
'keep_minimum' => (int) env('BACKUP_KEEP_MINIMUM', 3),
/*
|--------------------------------------------------------------------------
| Off-server copy
|--------------------------------------------------------------------------
|
| A filesystems.php disk name. A snapshot sitting on the same disk as the
| data survives a bad migration but not a dead disk, so set this to a
| remote disk. The command warns on every run while it is unset.
|
*/
'disk' => env('BACKUP_DISK'),
/*
|--------------------------------------------------------------------------
| Media library
|--------------------------------------------------------------------------
|
| Uploaded video is the irreplaceable asset here — it cannot be regenerated
| from anything. It is mirrored with rsync (incremental, resumable) to
| media_destination, which should be a NAS mount or another machine.
|
| Only runs when --media is passed AND a destination is set.
|
| media_delete stays false by default: a mirror that deletes is one bad
| path away from erasing the copy it was meant to protect.
|
*/
'media_paths' => [
base_path('data/app/users'),
base_path('data/app/public'),
],
'media_destination' => env('BACKUP_MEDIA_DEST'),
'media_delete' => (bool) env('BACKUP_MEDIA_DELETE', false),
];