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>
127 lines
5.1 KiB
PHP
127 lines
5.1 KiB
PHP
<?php
|
|
/**
|
|
* Restaura um backup produzido por db_backup.php ou backup_maker.php.
|
|
*
|
|
* Assumed location: <host>/shared/platform-tools/backups/restore_runner.php
|
|
*
|
|
* Via CLI :
|
|
* php shared/platform-tools/backups/restore_runner.php <ficheiro.zip> [opções]
|
|
* Opções: --dry-run apenas valida e mostra o que faria
|
|
* --no-safety não tira backup de segurança (NÃO recomendado)
|
|
* --purge (só ficheiros) apaga o que não estiver no artefacto
|
|
* --list lista os backups disponíveis e sai
|
|
*
|
|
* Via HTTP: POST restore_runner.php com token=<BACKUP_TOKEN>&file=<ficheiro.zip>
|
|
* GET é recusado de propósito: restaurar não é uma operação idempotente de
|
|
* leitura e não deve ser possível dispará-la com um link.
|
|
*
|
|
* TODO RESTAURO É DESTRUTIVO. Por omissão tira sempre um backup do estado
|
|
* actual primeiro e devolve o nome desse ficheiro no relatório.
|
|
*/
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
require_once __DIR__ . '/../functions_restore.php';
|
|
|
|
$IS_CLI = (php_sapi_name() === 'cli');
|
|
|
|
// --- Autenticação -----------------------------------------------------------
|
|
if (!$IS_CLI) {
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
http_response_code(405);
|
|
header('Content-Type: application/json');
|
|
die(json_encode(['ok' => false, 'error' => 'Use POST.']));
|
|
}
|
|
$token = $_POST['token'] ?? '';
|
|
if (BACKUP_TOKEN === '' || BACKUP_TOKEN === 'replace_me' || !hash_equals(BACKUP_TOKEN, (string) $token)) {
|
|
http_response_code(403);
|
|
header('Content-Type: application/json');
|
|
die(json_encode(['ok' => false, 'error' => 'Acesso negado.']));
|
|
}
|
|
ignore_user_abort(true);
|
|
set_time_limit(0);
|
|
}
|
|
|
|
// --- Argumentos -------------------------------------------------------------
|
|
if ($IS_CLI) {
|
|
$args = array_slice($argv, 1);
|
|
$flags = array_values(array_filter($args, fn($a) => str_starts_with($a, '--')));
|
|
$files = array_values(array_filter($args, fn($a) => !str_starts_with($a, '--')));
|
|
$file = $files[0] ?? '';
|
|
$dryRun = in_array('--dry-run', $flags, true);
|
|
$safety = !in_array('--no-safety', $flags, true);
|
|
$purge = in_array('--purge', $flags, true);
|
|
$list = in_array('--list', $flags, true);
|
|
} else {
|
|
$file = (string) ($_POST['file'] ?? '');
|
|
$dryRun = !empty($_POST['dry_run']);
|
|
$safety = ($_POST['safety'] ?? '1') !== '0';
|
|
$purge = !empty($_POST['purge']);
|
|
$list = !empty($_POST['list']);
|
|
}
|
|
|
|
function out(array $data, bool $cli): void {
|
|
if (!$cli) { header('Content-Type: application/json'); echo json_encode($data, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE); return; }
|
|
foreach ($data as $k => $v) {
|
|
if (is_array($v)) $v = $v ? implode(', ', array_map('strval', $v)) : '(nenhum)';
|
|
elseif (is_bool($v)) $v = $v ? 'sim' : 'não';
|
|
elseif ($v === null) $v = '-';
|
|
printf(" %-16s %s\n", $k, $v);
|
|
}
|
|
}
|
|
|
|
if ($list) {
|
|
$rows = backup_list();
|
|
if ($IS_CLI) {
|
|
echo "Backups disponíveis em " . backup_dir() . ":\n";
|
|
foreach ($rows as $r) printf(" %-46s %-6s %s\n", $r['filename'], $r['type'], $r['size_human']);
|
|
if (!$rows) echo " (nenhum)\n";
|
|
} else { header('Content-Type: application/json'); echo json_encode(['ok'=>true,'backups'=>$rows]); }
|
|
exit;
|
|
}
|
|
|
|
if ($file === '') {
|
|
out(['ok' => false, 'error' => 'Indique o ficheiro a restaurar (ou --list).'], $IS_CLI);
|
|
exit($IS_CLI ? 1 : 0);
|
|
}
|
|
|
|
// --- Validação (sempre, mesmo fora do dry-run) ------------------------------
|
|
$info = restore_inspect($file);
|
|
if ($IS_CLI) {
|
|
echo "Artefacto : {$file}\n";
|
|
echo "Tipo : " . ($info['type'] ?? '?') . "\n";
|
|
echo "Tamanho : " . ($info['size_human'] ?? '?') . "\n";
|
|
echo "Entradas : " . ($info['entries'] ?? 0) . "\n\n";
|
|
}
|
|
if (!$info['ok']) { out(['ok' => false, 'error' => $info['error']], $IS_CLI); exit($IS_CLI ? 1 : 0); }
|
|
|
|
if ($dryRun) {
|
|
out(['ok' => true, 'dry_run' => true, 'type' => $info['type'],
|
|
'would_restore' => $info['type'] === 'db'
|
|
? 'base de dados ' . DB_NAME
|
|
: (defined('BACKUP_FILES_DIR') ? BACKUP_FILES_DIR : 'uploads'),
|
|
'safety_backup' => $safety ? 'sim (antes de escrever)' : 'NÃO',
|
|
'purge' => $purge], $IS_CLI);
|
|
exit;
|
|
}
|
|
|
|
// --- Execução ---------------------------------------------------------------
|
|
if ($info['type'] === 'db') {
|
|
try {
|
|
$dsn = 'mysql:host=' . DB_HOST . ';port=' . DB_PORT . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET;
|
|
$pdo = new PDO($dsn, DB_USER, DB_PASS, [
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
|
|
]);
|
|
} catch (PDOException $e) {
|
|
out(['ok' => false, 'error' => 'Ligação à BD falhou: ' . $e->getMessage()], $IS_CLI);
|
|
exit($IS_CLI ? 1 : 0);
|
|
}
|
|
$report = restore_db($pdo, $file, ['safety' => $safety]);
|
|
} else {
|
|
$report = restore_files($file, ['safety' => $safety, 'purge' => $purge]);
|
|
}
|
|
|
|
out($report, $IS_CLI);
|
|
exit($IS_CLI ? ($report['ok'] ? 0 : 1) : 0);
|