317 lines
10 KiB
PHP
317 lines
10 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 $options = []): array
|
|
{
|
|
$result = ['success' => true, 'message' => '', 'filename' => ''];
|
|
|
|
try {
|
|
$this->cleanTemp();
|
|
File::ensureDirectoryExists($this->tempDir, 0755, true);
|
|
File::ensureDirectoryExists($this->backupDir, 0755, true);
|
|
|
|
$timestamp = now()->format('Y-m-d_H-i-s');
|
|
$appName = str_replace(' ', '_', AppSetting::getAppName() ?? 'app');
|
|
$filename = $appName . '_' . $timestamp . '.zip';
|
|
$filepath = $this->backupDir . '/' . $filename;
|
|
|
|
$includeFiles = $options['include_files'] ?? (bool) AppSetting::getSetting('backup_include_files', true);
|
|
$includeEnv = $options['include_env'] ?? (bool) AppSetting::getSetting('backup_include_env', true);
|
|
|
|
$steps = [];
|
|
$includedComponents = [];
|
|
|
|
$steps[] = $this->backupDatabase();
|
|
$includedComponents[] = 'database';
|
|
|
|
$stepEnv = $this->backupEnv($includeEnv);
|
|
$steps[] = $stepEnv;
|
|
if ($includeEnv) {
|
|
$includedComponents[] = 'env';
|
|
}
|
|
|
|
$stepFiles = $this->backupFiles($includeFiles);
|
|
$steps[] = $stepFiles;
|
|
if ($includeFiles) {
|
|
$includedComponents[] = 'files';
|
|
}
|
|
|
|
$this->writeManifest($timestamp, $includedComponents);
|
|
|
|
$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),
|
|
'steps' => $steps,
|
|
];
|
|
} catch (\Exception $e) {
|
|
$this->cleanTemp();
|
|
Log::error('Backup fallito: ' . $e->getMessage());
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Backup fallito: ' . $e->getMessage(),
|
|
'filename' => '',
|
|
'steps' => [],
|
|
];
|
|
}
|
|
}
|
|
|
|
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(bool $includeEnv = true): string
|
|
{
|
|
if (!$includeEnv) {
|
|
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(bool $includeFiles = true): string
|
|
{
|
|
if (!$includeFiles) {
|
|
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, array $includedComponents = ['database']): 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(),
|
|
'included_components' => $includedComponents,
|
|
];
|
|
|
|
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];
|
|
}
|
|
}
|