diff --git a/app/Models/SystemConfiguration.php b/app/Models/SystemConfiguration.php index e366cbb8b..14143b89c 100644 --- a/app/Models/SystemConfiguration.php +++ b/app/Models/SystemConfiguration.php @@ -9,6 +9,7 @@ declare(strict_types=1); * @property bool $api_enabled * @property string $archiving * @property 'form'|'http_auth'|'none' $auth_type + * @property array{enabled:bool,retention:int} $auto_sqlite_export * @property-read bool $reauth_required * @property-read int $reauth_time * @property-read string $auto_update_url diff --git a/cli/README.md b/cli/README.md index 57b0a03a1..cecf47278 100644 --- a/cli/README.md +++ b/cli/README.md @@ -127,6 +127,11 @@ cd /usr/share/FreshRSS # Back-up all users respective database to `data/users/*/backup.sqlite` # -q, --quiet suppress non-error messages +./cli/export-sqlite-auto.php +# Periodic SQLite export per user to `data/users/*/sqlite-backups/.sqlite`, pruned to retention. +# Gated by `auto_sqlite_export` in `data/config.php`. +# -q, --quiet suppress non-error messages + ./cli/db-restore.php --delete-backup --force-overwrite # Restore all users respective database from `data/users/*/backup.sqlite` # --delete-backup: delete `data/users/*/backup.sqlite` after successful import diff --git a/cli/export-sqlite-auto.php b/cli/export-sqlite-auto.php new file mode 100755 index 000000000..5f129cd3d --- /dev/null +++ b/cli/export-sqlite-auto.php @@ -0,0 +1,75 @@ +#!/usr/bin/env php +db['type'] ?? ''); + +$cliOptions = new class extends CliOptionsParser { + public bool $quiet; + + public function __construct() { + $this->addOption('quiet', (new CliOption('quiet', 'q'))->withValueNone()); + parent::__construct(); + } +}; + +if (!empty($cliOptions->errors)) { + fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage); +} + +$config = FreshRSS_Context::systemConf()->auto_sqlite_export; +$enabled = !empty($config['enabled']); +$retention = max(1, (int)($config['retention'] ?? 7)); +$verbose = !$cliOptions->quiet; + +if (!$enabled) { + if ($verbose) { + echo "FreshRSS automatic SQLite export is disabled (see `auto_sqlite_export.enabled` in `data/config.php`).\n"; + } + exit(0); +} + +$ok = true; +$timestamp = gmdate('Ymd\THis\Z'); + +foreach (FreshRSS_user_Controller::listUsers() as $username) { + $username = cliInitUser($username); + $exportDir = DATA_PATH . '/users/' . $username . '/sqlite-backups'; + if (!is_dir($exportDir) && !@mkdir($exportDir, 0755, true)) { + fwrite(STDERR, "FreshRSS error: unable to create export directory: {$exportDir}\n"); + $ok = false; + continue; + } + + $filename = $exportDir . '/' . $timestamp . '.sqlite'; + + if ($verbose) { + echo 'FreshRSS automatic SQLite export for user “', $username, '” -> ', $filename, "\n"; + } + + $databaseDAO = FreshRSS_Factory::createDatabaseDAO($username); + $exported = $databaseDAO->dbCopy($filename, FreshRSS_DatabaseDAO::SQLITE_EXPORT, false, $verbose); + $ok = $ok && $exported; + + if (!$exported) { + continue; + } + + $existing = glob($exportDir . '/*.sqlite') ?: []; + if (count($existing) > $retention) { + sort($existing); + $toDelete = array_slice($existing, 0, count($existing) - $retention); + foreach ($toDelete as $old) { + if (@unlink($old)) { + if ($verbose) { + echo "Pruned old export: {$old}\n"; + } + } else { + fwrite(STDERR, "FreshRSS warning: failed to prune old export: {$old}\n"); + } + } + } +} + +done($ok); diff --git a/config.default.php b/config.default.php index 99a039801..d54141a71 100644 --- a/config.default.php +++ b/config.default.php @@ -210,6 +210,16 @@ return [ 'from' => 'root@localhost', ], + # Automatic SQLite export of each user’s database, triggered by `./cli/export-sqlite-auto.php`. + # Intended to be scheduled by an admin (e.g. via cron) for periodic on-server backups + # distinct from the manual `./cli/db-backup.php` / `./cli/db-restore.php` migration workflow. + 'auto_sqlite_export' => [ + # Enable the automatic export. When false, `./cli/export-sqlite-auto.php` exits without writing. + 'enabled' => false, + # Number of past exports to retain per user. Older files are pruned after a successful export. + 'retention' => 7, + ], + # List of enabled FreshRSS extensions. 'extensions_enabled' => [ ], diff --git a/docs/en/admins/05_Backup.md b/docs/en/admins/05_Backup.md index dd1d2d04a..c9adb3855 100644 --- a/docs/en/admins/05_Backup.md +++ b/docs/en/admins/05_Backup.md @@ -57,6 +57,25 @@ cd /usr/share/FreshRSS/ ./cli/db-restore.php --delete-backup --force-overwrite ``` +## Automatic periodic SQLite export + +For ongoing on-server backups, separate from the one-shot `db-backup.php` / `db-restore.php` migration workflow, enable automatic SQLite export in `./data/config.php`: + +```php +'auto_sqlite_export' => [ + 'enabled' => true, + 'retention' => 7, +], +``` + +Then schedule it (for example via cron): + +```sh +./cli/export-sqlite-auto.php +``` + +Each run writes `./data/users//sqlite-backups/.sqlite` (UTC) for every user and prunes older files to the configured `retention` count. + ## Migrating the database First, back up all user databases to SQLite files: