Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use Ramon\Backup\Console\ExportCommand;
use Ramon\Backup\Console\ImportCommand;
use Ramon\Backup\Console\PruneStaleJobsCommand;
use Ramon\Backup\Settings\SettingsPreserver;

return [
(new Extend\Frontend('admin'))
Expand All @@ -46,7 +47,15 @@
// Same trust model as ramon/verified — the matching PRIVATE key is
// pasted into config.php under `backup-private-key`, never stored
// in the database.
->default('ramon-backup.encryption_public_key', ''),
->default('ramon-backup.encryption_public_key', '')
/*
* Padrões extra de chaves de `settings` que um restore não pode
* sobrescrever, um por linha (sufixo `*` casa por prefixo). O
* baseline — SMTP, fila, tokens desta instalação — está em
* SettingsPreserver::BASELINE; esta chave existe para o operador
* acrescentar o que for específico do ambiente dele.
*/
->default(SettingsPreserver::EXTRA_KEY, ''),

(new Extend\Routes('api'))
->get('/backup/backups', 'backup.list', ListBackupsController::class)
Expand Down
2 changes: 1 addition & 1 deletion js/dist/admin.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/dist/admin.js.map

Large diffs are not rendered by default.

50 changes: 49 additions & 1 deletion js/src/admin/components/ImportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ interface ArchiveManifest {
// New archives ship rich descriptors.
extensions?: string[] | ArchiveExtensionEntry[];
has_composer?: boolean;
// Archives from before the project-reconcile work don't carry the
// site's root extend.php, so the toggle for it stays hidden.
has_root_extend?: boolean;
}

interface InspectResult {
Expand All @@ -65,8 +68,10 @@ interface ImportProgress {
phase:
| "inspect"
| "extract"
| "reconcile"
| "restore"
| "rewrite"
| "verify"
| "finalize"
| "done"
| "error";
Expand Down Expand Up @@ -125,6 +130,13 @@ export default class ImportModal extends Modal<ImportModalAttrs> {
// Per-extension toggles, keyed by directory name from the manifest.
protected extensionsByName: Record<string, boolean> = {};

// Opt-in: overwriting the site's root extend.php replaces live config
// of THIS server, so it never happens unless asked for.
protected sectionRootExtend = false;
// Opt-out: losing this server's SMTP / queue / integration settings to
// the source forum's is the defect this whole layer exists to stop.
protected preserveSettings = true;

protected status: ImportProgress | null = null;
protected polling = false;

Expand Down Expand Up @@ -571,12 +583,46 @@ export default class ImportModal extends Modal<ImportModalAttrs> {
)}
</>
)}

{(hasDb || manifest.has_root_extend) && (
<div className="BackupImport-serverState">
<h4>{trans("server_state_title")}</h4>
<p className="helpText">{trans("server_state_help")}</p>

{hasDb &&
this.sectionRow(
"preserve_settings",
this.preserveSettings,
(v) => {
this.preserveSettings = v;
},
)}

{manifest.has_root_extend &&
this.sectionRow("root_extend", this.sectionRootExtend, (v) => {
this.sectionRootExtend = v;
})}

{this.sectionRootExtend && (
<div className="BackupImport-composerNote helpText">
<i className="icon fas fa-triangle-exclamation" />{" "}
{trans("root_extend_warning")}
</div>
)}
</div>
)}
</fieldset>
);
}

sectionRow(
key: "db" | "assets" | "storage" | "extensions",
key:
| "db"
| "assets"
| "storage"
| "extensions"
| "preserve_settings"
| "root_extend",
checked: boolean,
set: (v: boolean) => void,
count?: number,
Expand Down Expand Up @@ -619,6 +665,8 @@ export default class ImportModal extends Modal<ImportModalAttrs> {
assets: this.sectionAssets,
storage: this.sectionStorage,
extensions: extensionsField,
root_extend: this.sectionRootExtend,
preserve_settings: this.preserveSettings,
};
}

Expand Down
19 changes: 19 additions & 0 deletions less/admin.less
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,25 @@
font-size: 12px;
}

// Bloco das opções que decidem o que do SERVIDOR sobrevive ao restore.
// Separado por uma régua de propósito: são escolhas de natureza
// diferente das caixas "o que do backup aplicar" acima.
.BackupImport-serverState {
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--control-bg, rgba(0, 0, 0, 0.08));

h4 {
margin: 0 0 4px;
font-size: 13px;
font-weight: 600;
}

> .helpText {
margin-bottom: 8px;
}
}

.BackupImport-extList {
margin: 4px 0 8px 28px;
padding: 8px 12px;
Expand Down
9 changes: 8 additions & 1 deletion locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ ramon-backup:
extensions_tag_workbench: "workbench"
extensions_tag_vendor: "vendor"
extensions_tag_unknown: "unknown"
extensions_composer_note: "composer.json and composer.lock are bundled and will overwrite the destination's project root. Run `composer install` after restoring to materialise vendor extensions."
extensions_composer_note: "composer.json is merged into this server's, never overwritten — nothing installed here is removed. composer.lock is NOT applied, because a lock from another server prunes packages that only exist here. Run `composer install` after restoring to materialise vendor extensions."
inspect_title: "Archive details"
meta_when: "Created"
meta_flarum: "Flarum version"
Expand All @@ -164,6 +164,11 @@ ramon-backup:
section_storage: "Storage"
section_extensions: "Extensions"
section_count: "{count} files"
server_state_title: "This server's own configuration"
server_state_help: "Some state describes THIS server rather than the forum in the backup — SMTP credentials, queue driver, integration tokens, and the root extend.php. These options decide what survives the restore."
section_preserve_settings: "Keep this server's mail, queue and integration settings"
section_root_extend: "Replace the root extend.php with the backup's"
root_extend_warning: "The root extend.php is loaded before Flarum's error handler exists — a class it references that isn't installed here takes the forum down with a blank HTTP 500. The current file is copied to a .bak-* first, and the restore checks for unresolvable classes when it finishes."
key_title: "Private key"
key_help: "This archive is encrypted. Paste the matching base64 private key — it stays in this browser session and is sent ONCE to the server for the restore."
key_hint_local: "Leave blank to use the private key configured in this server's config.php."
Expand All @@ -189,8 +194,10 @@ ramon-backup:
logout_button: "Reload and sign in"
phase_inspect: "Inspecting archive…"
phase_extract: "Extracting…"
phase_reconcile: "Reconciling project files…"
phase_restore: "Restoring database…"
phase_rewrite: "Rewriting URLs…"
phase_verify: "Checking the forum still boots…"
phase_finalize: "Finalising…"
phase_done: "Done"
phase_error: "Error"
Expand Down
9 changes: 8 additions & 1 deletion locale/pt-BR.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ ramon-backup:
extensions_tag_workbench: "workbench"
extensions_tag_vendor: "vendor"
extensions_tag_unknown: "unknown"
extensions_composer_note: "composer.json e composer.lock estão no arquivo e vão sobrescrever os do servidor de destino. Rode `composer install` depois de restaurar para materializar as extensões vendor."
extensions_composer_note: "O composer.json é UNIDO ao deste servidor, nunca sobrescrito — nada que está instalado aqui é removido. O composer.lock NÃO é aplicado: um lock de outro servidor poda pacotes que só existem aqui. Rode `composer install` depois de restaurar para materializar as extensões vendor."
inspect_title: "Detalhes do arquivo"
meta_when: "Criado em"
meta_flarum: "Versão do Flarum"
Expand All @@ -164,6 +164,11 @@ ramon-backup:
section_storage: "Storage"
section_extensions: "Extensões"
section_count: "{count} arquivos"
server_state_title: "Configuração deste servidor"
server_state_help: "Parte do estado descreve ESTE servidor, não o fórum do backup — credenciais de SMTP, driver de fila, tokens de integração e o extend.php da raiz. Estas opções decidem o que sobrevive à restauração."
section_preserve_settings: "Manter e-mail, fila e integrações deste servidor"
section_root_extend: "Substituir o extend.php da raiz pelo do backup"
root_extend_warning: "O extend.php da raiz é carregado antes de existir handler de erro no Flarum — uma classe referenciada ali que não esteja instalada aqui derruba o fórum com HTTP 500 em branco. O arquivo atual é copiado para um .bak-* antes, e a restauração verifica classes não resolvidas ao terminar."
key_title: "Chave privada"
key_help: "Este arquivo está criptografado. Cole a chave privada base64 correspondente — ela fica somente nesta sessão do navegador e é enviada UMA VEZ ao servidor para a restauração."
key_hint_local: "Deixe em branco para usar a chave privada configurada no config.php deste servidor."
Expand All @@ -189,8 +194,10 @@ ramon-backup:
logout_button: "Recarregar e fazer login"
phase_inspect: "Inspecionando arquivo…"
phase_extract: "Extraindo…"
phase_reconcile: "Reconciliando arquivos do projeto…"
phase_restore: "Restaurando banco…"
phase_rewrite: "Reescrevendo URLs…"
phase_verify: "Conferindo se o fórum ainda sobe…"
phase_finalize: "Finalizando…"
phase_done: "Pronto"
phase_error: "Erro"
Expand Down
48 changes: 46 additions & 2 deletions src/Console/ImportCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@ protected function configure(): void
InputOption::VALUE_OPTIONAL,
'Restore extensions. Omit the value for ALL, or pass a comma-separated list of ids.',
false
)
->addOption(
'root-extend',
null,
InputOption::VALUE_NONE,
'Replace this install\'s root extend.php with the backup\'s (off by default — it is loaded before Flarum\'s error handler).'
)
->addOption(
'preserve-settings',
null,
InputOption::VALUE_NEGATABLE,
'Keep this server\'s mail / queue / integration settings across the restore.',
true
);
}

Expand Down Expand Up @@ -113,7 +126,30 @@ protected function fire(): int
.', posts parsed: '.((int) ($rewrite['posts_parsed'] ?? 0)));
}

return 0;
return $this->reportOutcome($state);
}

/**
* Despeja os avisos e escolhe o exit code.
*
* Um restore que terminou `incomplete` sai com 1 de propósito: são os
* casos em que o operador precisa agir antes de recarregar o fórum —
* entrada não gravada, ou classe que o extend.php da raiz referencia
* e o autoloader não resolve. Sair 0 aqui é o que faz um script de
* deploy seguir adiante rumo a um HTTP 500 sem mensagem.
*/
private function reportOutcome(JobState $state): int
{
foreach ((array) $state->get('warnings', []) as $warning) {
$this->output->writeln('<comment>! '.(string) $warning.'</comment>');
}

if (! $state->get('incomplete', false)) {
return 0;
}

$this->error('Restore finished INCOMPLETE — review the warnings above before reloading the forum.');
return 1;
}

/**
Expand All @@ -130,7 +166,9 @@ private function resolveSelection(): ?array
|| $this->input->hasParameterOption('--no-assets')
|| $this->input->hasParameterOption('--storage')
|| $this->input->hasParameterOption('--no-storage')
|| $this->input->hasParameterOption('--extensions');
|| $this->input->hasParameterOption('--extensions')
|| $this->input->hasParameterOption('--root-extend')
|| $this->input->hasParameterOption('--no-preserve-settings');

if (! $touched) {
return null;
Expand All @@ -141,6 +179,12 @@ private function resolveSelection(): ?array
'assets' => (bool) $this->input->getOption('assets'),
'storage' => (bool) $this->input->getOption('storage'),
'extensions' => $this->resolveExtensionSelection(),
/*
* Assimétricos de propósito: sobrescrever o extend.php da raiz
* exige pedir, preservar a config deste servidor exige recusar.
*/
'root_extend' => (bool) $this->input->getOption('root-extend'),
'preserve_settings' => (bool) $this->input->getOption('preserve-settings'),
];
}

Expand Down
20 changes: 17 additions & 3 deletions src/Job/ExportJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ private function runScan(JobState $state): void
* in; the inventory drives WHERE they live (workbench/ for local
* dev, vendor/ for composer-managed).
*
* @return array{built: bool, targets: list<array<string, mixed>>, idx: int, files: int, bytes: int, counts: array{asset: int, storage: int, extension: int}, ext_descriptors: list<array<string, mixed>>, has_composer: bool}
* @return array{built: bool, targets: list<array<string, mixed>>, idx: int, files: int, bytes: int, counts: array{asset: int, storage: int, extension: int}, ext_descriptors: list<array<string, mixed>>, has_composer: bool, has_root_extend: bool}
*/
private function buildScanPlan(JobState $state): array
{
Expand Down Expand Up @@ -350,6 +350,7 @@ private function buildScanPlan(JobState $state): array
'counts' => ['asset' => 0, 'storage' => 0, 'extension' => 0],
'ext_descriptors' => [],
'has_composer' => false,
'has_root_extend' => false,
];
}

Expand Down Expand Up @@ -393,13 +394,25 @@ private function scanOneTarget(JobState $state, array &$scan, array $target): vo
break;

case 'composer':
foreach (['composer.json', 'composer.lock'] as $rel) {
/*
* O extend.php da raiz viaja junto com o composer.json de
* propósito: é ele que CONSUME as dependências que o
* manifesto declara. Mandar só o manifesto foi o que
* produziu, num restore real, um extend.php apontando para
* uma classe que o composer install seguinte tinha podado —
* fatal no boot, antes de existir handler de erro.
*/
foreach (['composer.json', 'composer.lock', 'extend.php'] as $rel) {
$abs = rtrim($this->appPaths->base, '/\\').DIRECTORY_SEPARATOR.$rel;
if (is_file($abs)) {
$size = filesize($abs) ?: 0;
$files[] = ['name' => 'project/'.$rel, 'absolute' => $abs, 'size' => $size];
$bytes += $size;
$scan['has_composer'] = true;
if ($rel === 'extend.php') {
$scan['has_root_extend'] = true;
} else {
$scan['has_composer'] = true;
}
}
}
break;
Expand Down Expand Up @@ -465,6 +478,7 @@ private function finalizeScan(JobState $state, array $scan): void
'extension_count' => (int) $scan['counts']['extension'],
'extensions' => $bundledExts,
'has_composer' => (bool) $scan['has_composer'],
'has_root_extend' => (bool) ($scan['has_root_extend'] ?? false),
];
$state->set('manifest_summary', $summary);

Expand Down
Loading
Loading