Files
glastree/app/Services/BackupService.php
T
2026-06-01 16:11:29 +02:00

299 lines
9.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\AppSetting;
use App\Models\Notifica;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use RuntimeException;
use ZipArchive;
class BackupService
{
private string $backupDir;
private string $tempDir;
public function __construct()
{
$basePath = AppSetting::getSetting('backup_path', 'backups');
$this->backupDir = storage_path('app/' . ltrim($basePath, '/'));
$this->tempDir = storage_path('app/backup-temp');
}
public function getBackupDir(): string
{
return $this->backupDir;
}
public function list(): array
{
if (!is_dir($this->backupDir)) {
return [];
}
$files = File::files($this->backupDir);
$backups = [];
foreach ($files as $file) {
if ($file->getExtension() !== 'zip') {
continue;
}
$manifest = $this->readManifest($file->getPathname());
$backups[] = [
'filename' => $file->getFilename(),
'pathname' => $file->getPathname(),
'size' => $file->getSize(),
'size_formatted' => $this->formatBytes($file->getSize()),
'last_modified' => $file->getMTime(),
'manifest' => $manifest,
];
}
usort($backups, fn(array $a, array $b) => $b['last_modified'] - $a['last_modified']);
return $backups;
}
public function run(): array
{
$result = ['success' => true, 'message' => '', 'filename' => ''];
try {
File::ensureDirectoryExists($this->tempDir, 0755, true);
File::ensureDirectoryExists($this->backupDir, 0755, true);
$this->cleanTemp();
$timestamp = now()->format('Y-m-d_H-i-s');
$appName = str_replace(' ', '_', AppSetting::getAppName() ?? 'app');
$filename = $appName . '_' . $timestamp . '.zip';
$filepath = $this->backupDir . '/' . $filename;
$steps = [];
$steps[] = $this->backupDatabase();
$steps[] = $this->backupEnv();
$steps[] = $this->backupFiles();
$this->writeManifest($timestamp);
$zipResult = $this->createZip($filepath);
if (!$zipResult) {
throw new RuntimeException('Errore durante la creazione del file ZIP.');
}
$this->cleanTemp();
$size = file_exists($filepath) ? filesize($filepath) : 0;
Log::info("Backup completato: {$filename} (" . $this->formatBytes($size) . ")");
$this->cleanupOldBackups();
return [
'success' => true,
'message' => 'Backup completato con successo.',
'filename' => $filename,
'size' => $size,
'size_formatted' => $this->formatBytes($size),
];
} catch (\Exception $e) {
$this->cleanTemp();
Log::error('Backup fallito: ' . $e->getMessage());
return [
'success' => false,
'message' => 'Backup fallito: ' . $e->getMessage(),
'filename' => '',
];
}
}
public function download(string $filename): ?string
{
$filepath = $this->backupDir . '/' . basename($filename);
return file_exists($filepath) ? $filepath : null;
}
public function delete(string $filename): bool
{
$filepath = $this->backupDir . '/' . basename($filename);
if (file_exists($filepath)) {
unlink($filepath);
Log::info("Backup eliminato: {$filename}");
return true;
}
return false;
}
public function getConfig(): array
{
return [
'backup_path' => AppSetting::getSetting('backup_path', 'backups'),
'backup_retention_days' => (int) AppSetting::getSetting('backup_retention_days', 30),
'backup_include_files' => (bool) AppSetting::getSetting('backup_include_files', true),
'backup_include_env' => (bool) AppSetting::getSetting('backup_include_env', true),
];
}
public function saveConfig(array $data): void
{
$settings = AppSetting::first() ?? new AppSetting();
$settings->backup_path = trim($data['backup_path'] ?? 'backups', '/');
$settings->backup_retention_days = max(1, (int) ($data['backup_retention_days'] ?? 30));
$settings->backup_include_files = !empty($data['backup_include_files']);
$settings->backup_include_env = !empty($data['backup_include_env']);
$settings->save();
}
private function backupDatabase(): string
{
$dbName = config('database.connections.mysql.database');
$dbUser = config('database.connections.mysql.username');
$dbPass = config('database.connections.mysql.password');
$dbHost = config('database.connections.mysql.host');
$dbPort = config('database.connections.mysql.port');
$sqlFile = $this->tempDir . '/database.sql';
$command = sprintf(
'mysqldump --host=%s --port=%s --user=%s --password=%s --routines --events --single-transaction --quick %s > %s 2>&1',
escapeshellarg($dbHost),
escapeshellarg($dbPort),
escapeshellarg($dbUser),
escapeshellarg($dbPass),
escapeshellarg($dbName),
escapeshellarg($sqlFile)
);
$output = null;
$exitCode = null;
exec($command, $output, $exitCode);
if ($exitCode !== 0 || !file_exists($sqlFile) || filesize($sqlFile) === 0) {
throw new RuntimeException('mysqldump fallito: ' . implode("\n", (array) $output));
}
return 'database.sql (' . $this->formatBytes(filesize($sqlFile)) . ')';
}
private function backupEnv(): string
{
if (!AppSetting::getSetting('backup_include_env', true)) {
return '.env (escluso)';
}
$envPath = base_path('.env');
$envKeyPath = base_path('.env.key');
if (file_exists($envPath)) {
copy($envPath, $this->tempDir . '/.env');
}
if (file_exists($envKeyPath)) {
copy($envKeyPath, $this->tempDir . '/.env.key');
}
return '.env copiato';
}
private function backupFiles(): string
{
if (!AppSetting::getSetting('backup_include_files', true)) {
return 'files (esclusi)';
}
$filesDir = AppSetting::getDocumentiStorageAbsolutePath();
$targetDir = $this->tempDir . '/files';
if (!is_dir($filesDir)) {
return 'files (cartella non trovata)';
}
File::ensureDirectoryExists($targetDir, 0755, true);
File::copyDirectory($filesDir, $targetDir);
return 'files copiati';
}
private function writeManifest(string $timestamp): void
{
$manifest = [
'created_at' => $timestamp,
'app_version' => AppSetting::getAppVersion() ?? 'unknown',
'app_name' => AppSetting::getAppName() ?? 'Glastree',
'db_driver' => config('database.default'),
'db_name' => config('database.connections.mysql.database'),
'php_version' => PHP_VERSION,
'laravel_version' => app()->version(),
];
File::put($this->tempDir . '/manifest.json', json_encode($manifest, JSON_PRETTY_PRINT));
}
private function readManifest(string $zipPath): ?array
{
$zip = new ZipArchive();
if ($zip->open($zipPath) === true) {
$manifestContent = $zip->getFromName('manifest.json');
$zip->close();
if ($manifestContent !== false) {
return json_decode($manifestContent, true);
}
}
return null;
}
private function createZip(string $filepath): bool
{
$zip = new ZipArchive();
if ($zip->open($filepath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
return false;
}
$files = File::allFiles($this->tempDir);
foreach ($files as $file) {
$relativePath = substr($file->getPathname(), strlen($this->tempDir) + 1);
$zip->addFile($file->getPathname(), $relativePath);
}
return $zip->close();
}
private function cleanTemp(): void
{
if (is_dir($this->tempDir)) {
File::deleteDirectory($this->tempDir);
}
}
private function cleanupOldBackups(): void
{
$retentionDays = (int) AppSetting::getSetting('backup_retention_days', 30);
$cutoff = now()->subDays($retentionDays)->timestamp;
foreach (File::files($this->backupDir) as $file) {
if ($file->getExtension() === 'zip' && $file->getMTime() < $cutoff) {
unlink($file->getPathname());
Log::info("Backup eliminato per retention: " . $file->getFilename());
}
}
}
private function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$i = 0;
while ($bytes >= 1024 && $i < count($units) - 1) {
$bytes /= 1024;
$i++;
}
return round($bytes, 2) . ' ' . $units[$i];
}
}