/shared/platform-tools/backups/backup_maker.php * → $sourceDir = / (default: uploads; ver config.php) * → $backupDir = /backups (where the ZIP lands) * * Via CLI : php shared/platform-tools/backups/backup_maker.php * Via HTTP: GET shared/platform-tools/backups/backup_maker.php?token=SEU_TOKEN */ require_once __DIR__ . '/config.php'; require_once __DIR__ . '/../functions_backup.php'; // --- Autenticação ----------------------------------------------------------- if (php_sapi_name() !== 'cli') { $token = $_GET['token'] ?? $_POST['token'] ?? ''; if (!hash_equals(BACKUP_TOKEN, $token)) { http_response_code(403); die(json_encode(['status' => 'error', 'message' => 'Acesso negado.'])); } ignore_user_abort(true); set_time_limit(0); } // --- Configuração ----------------------------------------------------------- $siteRoot = dirname(__DIR__, 3); // $backupDir = $siteRoot . '/backups'; // /backups if (!is_dir($backupDir)) @mkdir($backupDir, 0775, true); $siteRoot = realpath($siteRoot) ?: $siteRoot; $backupDir = realpath($backupDir) ?: $backupDir; backup_dir_protect($backupDir); // BACKUP_FILES_DIR='' is an explicit opt-out, not a misconfiguration: some // hosts keep no local user content at all (surf serves media from Nextcloud // over WebDAV), so there is genuinely nothing to archive. Report and exit 0 // rather than die() -- a scheduled run must not be marked failed for it. if (trim(BACKUP_FILES_DIR) === '') { echo "Backup de ficheiros desactivado (BACKUP_FILES_DIR vazio) — este host não guarda ficheiros localmente.\n"; return; } // BACKUP_FILES_DIR aceita uma LISTA separada por vírgulas, não só uma pasta. // // Era uma única pasta, e isso perdia dados em silêncio: o design guarda os // modelos 3D em public/threed/ e o resto em public/uploads/, por isso um // backup "de ficheiros" trazia metade do conteúdo do utilizador e ninguém // reparava até tentar restaurar. Uma pasta continua a ser válido -- a maioria // dos sites tem só uma. $requestedDirs = array_values(array_filter(array_map( fn($d) => trim($d, " \t/"), explode(',', BACKUP_FILES_DIR) ), fn($d) => $d !== '')); $sourceDirs = []; // rótulo relativo => caminho absoluto foreach ($requestedDirs as $rel) { $abs = realpath($siteRoot . '/' . $rel) ?: ''; if ($abs === '' || !is_dir($abs)) { // Falhar alto: uma pasta configurada que não existe é quase sempre um // erro de configuração, e continuar em silêncio produz exactamente o // backup incompleto que isto existe para evitar. die("Erro: pasta de uploads não encontrada: $siteRoot/$rel" . " (verifica BACKUP_FILES_DIR no .env).\n"); } $sourceDirs[$rel] = $abs; } if (!$sourceDirs) { die("Erro: BACKUP_FILES_DIR não resolveu nenhuma pasta válida.\n"); } // Seconds + random suffix: no same-minute overwrite, and the download URL // can't be guessed even if the folder deny rule is missing. $filename = 'backup_' . date('d_m_Y_H_i_s') . '_' . bin2hex(random_bytes(3)) . '.zip'; $outputPath = "$backupDir/$filename"; echo "Raiz do site : $siteRoot\n"; echo "Pastas origem: " . implode(', ', array_keys($sourceDirs)) . "\n"; echo "Pasta backup : $backupDir\n"; echo "Destino ZIP : $outputPath\n\n"; if (!class_exists('ZipArchive')) { die("Erro: extensão ZipArchive não disponível.\n"); } if (!is_writable($backupDir)) { die("Erro: sem permissão de escrita em: $backupDir\n"); } // --- Criar ZIP -------------------------------------------------------------- $zip = new ZipArchive(); $result = $zip->open($outputPath, ZipArchive::CREATE | ZipArchive::OVERWRITE); if ($result !== true) { die("Erro ao criar ZIP (código: $result)\n"); } // Só a pasta de uploads é percorrida — código, vendor/ e afins são // reconstituíveis a partir do git e ficam de fora por construção. Dentro dos // uploads NADA é excluído por nome/extensão: tudo ali é conteúdo externo de // utilizadores (um .zip carregado por um utilizador é dado legítimo). Única // exclusão: a própria pasta backups/, caso algum host a tenha aninhado aqui. $skipped = 0; $added = 0; $errors = 0; foreach ($sourceDirs as $relRoot => $sourceDir) { $filter = new RecursiveCallbackFilterIterator( new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS), function ($file) use ($backupDir, &$skipped): bool { $realPath = $file->getRealPath(); if ($realPath === false) { $skipped++; return false; } if ($realPath === $backupDir || str_starts_with($realPath, $backupDir . DIRECTORY_SEPARATOR)) { $skipped++; return false; } return true; } ); $iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST); // Entrada raiz explícita: o restore extrai na raiz do host, e um backup de // uma pasta de uploads vazia continua a ser um ZIP válido. $zip->addEmptyDir($relRoot); foreach ($iterator as $file) { $realPath = $file->getRealPath(); // Entradas prefixadas com uploads/... para o ZIP extraído na raiz do host // repor os ficheiros no sítio certo. Ancorado em $sourceDir (não em // $siteRoot) para funcionar mesmo quando uploads é um symlink para fora // da raiz do site. $relativePath = $relRoot . '/' . str_replace(DIRECTORY_SEPARATOR, '/', substr($realPath, strlen($sourceDir) + 1)); if ($file->isDir()) { $zip->addEmptyDir($relativePath); } else { if (!is_readable($realPath)) { echo "AVISO: sem permissão de leitura — $realPath\n"; $errors++; continue; } // addFile() streams no close() — não carrega o ficheiro em memória. if (!$zip->addFile($realPath, $relativePath)) { echo "AVISO: falha ao adicionar — $realPath\n"; $errors++; continue; } $added++; } } } // fim do foreach por pasta configurada echo "Âmbito: " . implode('/, ', array_keys($sourceDirs)) . "/ (o código reconstitui-se a partir do git)\n"; if (!$zip->close()) { die("Erro: falha ao finalizar o ficheiro ZIP.\n"); } clearstatcache(true, $outputPath); if (!file_exists($outputPath)) { die("Erro: o ficheiro ZIP não foi criado.\n"); } // --- Resultado -------------------------------------------------------------- $bytes = filesize($outputPath); $size = $bytes >= 1024 * 1024 ? round($bytes / 1024 / 1024, 2) . ' MB' : round($bytes / 1024, 1) . ' KB'; echo "Backup concluído!\n"; echo "Ficheiro : $outputPath\n"; echo "Tamanho : $size\n"; echo "Ficheiros: $added adicionados, $skipped ignorados" . ($errors ? ", $errors erros" : '') . "\n"; cleanOldBackups($backupDir, 'backup_*.zip');