Vendor platform-tools: everything this site runs is now in one repo
shared/platform-tools was a git submodule. On this branch it is a plain
directory of tracked files, frozen at ea3616abae30106df283e73a3e2f9e5bb0e6ae90
— the same commit fotografia.andmore.pt pins. Contents are byte-identical to
that tree, verified blob by blob.
The submodule caused every deployment failure this site has had:
- .gitmodules named no `branch`, so the update fell back to
refs/remotes/origin/HEAD — a ref a plain server-side clone never writes.
"Atualizar submódulo" failed with "Unable to find refs/remotes/origin/HEAD
revision in submodule path".
- Its URL was http://, which the origin answers with 523.
- This install is deployed by uploading files, and a submodule is invisible
to that. The tree moved forward while shared/platform-tools stayed on the
old pin, config/functions_secrets.php died on a missing compat_secrets.php,
and the whole site — public pages included — went down.
Each of those was separately fixable. Together they mean a second repository
nobody can see from the deployment path, gating whether the first one boots. A
plain directory deploys like every other file and cannot fall out of step.
The cost is that platform-tools updates no longer arrive by bumping a pin; they
are a copy-over-and-commit. shared/platform-tools/VENDORED.md records the
upstream SHA, the procedure, and which four files differ from upstream in line
endings only (social/*.js, .css, functions_publications.php — CRLF upstream,
normalised to LF here; content otherwise identical).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0e034232e9
commit
4d40adbf64
67 files changed
+17478
-5
No files matched your search
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
/**
|
||||
* Backup dos ficheiros do site — APENAS a pasta de uploads.
|
||||
*
|
||||
* Tudo o resto no host é reconstituível a partir do git (código) ou do backup
|
||||
* da base de dados; os uploads são a única pasta com informação externa. Os
|
||||
* caminhos dentro do ZIP ficam relativos à raiz do host (uploads/...), por
|
||||
* isso um restore é extrair o ZIP na raiz.
|
||||
*
|
||||
* Assumed location: <host>/shared/platform-tools/backups/backup_maker.php
|
||||
* → $sourceDir = <host>/<BACKUP_FILES_DIR> (default: uploads; ver config.php)
|
||||
* → $backupDir = <host>/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); // <host>
|
||||
$backupDir = $siteRoot . '/backups'; // <host>/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');
|
||||
Reference in new issue
Block a user