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 { $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; $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]; } }