cms/app/Services/BackupService.php

147 lines
4.3 KiB
PHP
Raw Permalink Normal View History

<?php
namespace App\Services;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Exception;
/**
* Service to handle site Backup and Restore operations.
*/
class BackupService
{
/**
* Get all available backup files from storage.
*
* @return array List of backups with metadata.
*/
public function getBackups(): array
{
$backupDir = storage_path('app/backups');
if (!File::exists($backupDir)) {
File::makeDirectory($backupDir, 0755, true);
}
// Ensure any cached file list is cleared
clearstatcache();
$files = File::files($backupDir);
$backups = [];
foreach ($files as $file) {
if ($file->getExtension() === 'gz') {
$backups[] = [
'name' => $file->getFilename(),
'size' => round($file->getSize() / 1024 / 1024, 2) . ' MB',
'date' => date('Y-m-d H:i:s', $file->getMTime()),
];
}
}
// Sort by newest first
usort($backups, fn($a, $b) => $b['date'] <=> $a['date']);
return $backups;
}
/**
* Trigger a new site backup process.
*
* @return bool True if backup was successful.
*/
public function create(): bool
{
return Artisan::call('sw:site:backup') === 0;
}
/**
* Create a pre-restore snapshot of the current state.
*
* @return string The filename of the created snapshot.
* @throws Exception If snapshot creation fails.
*/
public function createPreRestoreSnapshot(): string
{
$timestamp = date('Y-m-d_H-i-s');
$snapshotName = "pre-restore-snapshot_{$timestamp}.gz";
// We use the same artisan command logic but with a specific name if possible,
// or rename the latest backup. Since sw:site:backup doesn't take a name,
// we'll run it and then identify/rename if needed, but for KISS,
// let's just run it and it will have its own timestamped name.
if (Artisan::call('sw:site:backup') !== 0) {
throw new Exception('Failed to create pre-restore snapshot.');
}
// The sw:site:backup command creates a file like siteweaver_backup_Y-m-d_H-i-s.gz
// Let's find the most recent one.
$backups = $this->getBackups();
return $backups[0]['name'];
}
/**
* Restore the site from a specific backup file.
*
* @param string $filename The name of the backup file to restore.
* @param bool $createSnapshot Whether to create a pre-restore snapshot.
* @return bool True if restore was successful.
* @throws Exception If the backup file is missing or snapshot fails.
*/
public function restore(string $filename, bool $createSnapshot = true): bool
{
$backupPath = storage_path('app/backups/' . $filename);
if (!File::exists($backupPath)) {
throw new Exception('Backup file not found.');
}
if ($createSnapshot) {
$this->createPreRestoreSnapshot();
}
return Artisan::call('sw:site:restore', [
'filename' => $filename,
'--force' => true,
]) === 0;
}
/**
* Upload a backup file to the backup directory.
*
* @param UploadedFile $file The uploaded .gz backup file.
* @return bool True if upload was successful.
*/
public function upload(UploadedFile $file): bool
{
$filename = $file->getClientOriginalName();
// Basic sanitization of filename
$filename = preg_replace('/[^a-zA-Z0-9.\-_]/', '_', $filename);
if (!str_ends_with($filename, '.gz')) {
$filename .= '.gz';
}
$backupDir = storage_path('app/backups');
if (!File::exists($backupDir)) {
File::makeDirectory($backupDir, 0755, true);
}
$file->move($backupDir, $filename);
return true;
}
/**
* Get the current restoration progress.
*
* @return array|null The progress data or null if not found.
*/
public function getProgress(): ?array
{
return \Illuminate\Support\Facades\Cache::get(\App\Console\Commands\SiteRestore::PROGRESS_KEY);
}
}