Files
fitme3070.pt/shared/platform-tools/backups/restore_runner.php
T
andreandClaude Opus 5 4d40adbf64 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>
2026-09-13 23:06:23 +01:00

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);