Keep old pages readable, detect stale settings writes, make backups honest
- Own CSS is parsed on write only. A page whose stored CSS no longer passes a stricter rule answers 200 and opens in the editor; that one block loses its CSS. A document read several times in one request is validated once. - Settings and theme refuse a write from an outdated form. - A backup fails when a media file does not arrive completely and leaves no half directory behind; rotation counts intact backups only. - The archive update takes its backup behind the maintenance page. - A package import puts all media in place before removing the old ones, reports honestly when that fails and keeps a maintenance state that was already on. - Restoring replaces the SQLite file in one move and points at pending migrations. - Six irreversible actions ask first, in a dialog of the admin's own. - Ctrl+S in the editor takes the field being typed in along; Delete no longer removes a block while a dialog is open. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d61aa0f22
commit
ead47cc924
25 files changed
+437
-72
No files matched your search
+53
-18
@@ -207,19 +207,34 @@ final class SitePackage
|
||||
return $fail();
|
||||
}
|
||||
|
||||
// Eine Wartung, die schon vorher an war, hat jemand mit Absicht eingeschaltet - die
|
||||
// hebt ein Import nicht auf.
|
||||
$inWartung = is_file($this->root.'/'.Updater::MAINTENANCE_FILE);
|
||||
$this->updater->maintenance(true);
|
||||
try {
|
||||
$media = $this->extractMedia($zip);
|
||||
$this->write($tables, $withUsers);
|
||||
$step('Inhalte ersetzen', true, $this->summary($tables, $withUsers));
|
||||
$moved = $this->replaceMedia($media);
|
||||
$step('Medien ersetzen', true, sprintf('%d Datei(en) übernommen.', $moved));
|
||||
} catch (\Throwable $e) {
|
||||
$step('Inhalte ersetzen', false, $e->getMessage().' Die Datenbank blieb unverändert; die Sicherung liegt bereit.');
|
||||
try {
|
||||
$media = $this->extractMedia($zip);
|
||||
$this->write($tables, $withUsers);
|
||||
} catch (\Throwable $e) {
|
||||
$step('Inhalte ersetzen', false, $e->getMessage().' Die Datenbank blieb unverändert; die Sicherung liegt bereit.');
|
||||
|
||||
return $fail();
|
||||
return $fail();
|
||||
}
|
||||
$step('Inhalte ersetzen', true, $this->summary($tables, $withUsers));
|
||||
// Ab hier ist die Datenbank die des Pakets. Scheitern die Medien, sagt die Meldung
|
||||
// genau das - und nennt die Sicherung, mit der sich beides zurücknehmen lässt.
|
||||
try {
|
||||
$moved = $this->replaceMedia($media);
|
||||
} catch (\Throwable $e) {
|
||||
$step('Medien ersetzen', false, sprintf('%s Die Inhalte sind bereits die des Pakets, die Medien nicht vollständig. Zurück auf den Stand vorher: cms:restore %s.', $e->getMessage(), $safety['name']));
|
||||
|
||||
return $fail();
|
||||
}
|
||||
$step('Medien ersetzen', true, sprintf('%d Datei(en) übernommen.', $moved));
|
||||
} finally {
|
||||
$this->updater->maintenance(false);
|
||||
if (!$inWartung) {
|
||||
$this->updater->maintenance(false);
|
||||
}
|
||||
if (isset($media)) {
|
||||
$this->remove($media);
|
||||
}
|
||||
@@ -435,25 +450,45 @@ final class SitePackage
|
||||
return $target;
|
||||
}
|
||||
|
||||
/** Leert den Medienordner (die Sicherung hat ihn) und legt die Dateien des Pakets hinein. */
|
||||
/**
|
||||
* Legt die Dateien des Pakets in den Medienordner und entfernt danach, was nicht dazugehört.
|
||||
*
|
||||
* In dieser Reihenfolge: Erst wenn jede Datei des Pakets angekommen ist, verschwinden die
|
||||
* alten. Vorher wurde zuerst geleert und dann kopiert, ohne nachzuzählen - bei knappem
|
||||
* Speicher meldete der Import Erfolg, und die Website verwies auf Bilder, die es nicht gab.
|
||||
*
|
||||
* @throws \RuntimeException wenn eine Datei nicht ankommt; der Medienordner hat dann noch alle alten Dateien
|
||||
*/
|
||||
private function replaceMedia(string $source): int
|
||||
{
|
||||
if (!is_dir($this->mediaDirectory) && !@mkdir($this->mediaDirectory, 0o775, true)) {
|
||||
throw new \RuntimeException('Der Medienordner ließ sich nicht anlegen.');
|
||||
}
|
||||
$neu = [];
|
||||
foreach (scandir($source) ?: [] as $name) {
|
||||
if (1 !== preg_match(self::MEDIA_NAME, $name)) {
|
||||
continue;
|
||||
}
|
||||
$size = (int) filesize($source.'/'.$name);
|
||||
$target = $this->mediaDirectory.'/'.$name;
|
||||
// Verschieben braucht keinen zweiten Platz auf der Platte; über Laufwerksgrenzen
|
||||
// hinweg bleibt das Kopieren.
|
||||
if (!@rename($source.'/'.$name, $target) && !@copy($source.'/'.$name, $target)) {
|
||||
throw new \RuntimeException(sprintf('Die Mediendatei %s ließ sich nicht ablegen. Freien Speicherplatz prüfen.', $name));
|
||||
}
|
||||
clearstatcache(true, $target);
|
||||
if ($size !== (int) @filesize($target)) {
|
||||
throw new \RuntimeException(sprintf('Die Mediendatei %s kam unvollständig an. Freien Speicherplatz prüfen.', $name));
|
||||
}
|
||||
$neu[$name] = true;
|
||||
}
|
||||
foreach (scandir($this->mediaDirectory) ?: [] as $name) {
|
||||
if (1 === preg_match(self::MEDIA_NAME, $name)) {
|
||||
if (!isset($neu[$name]) && 1 === preg_match(self::MEDIA_NAME, $name)) {
|
||||
@unlink($this->mediaDirectory.'/'.$name);
|
||||
}
|
||||
}
|
||||
$count = 0;
|
||||
foreach (scandir($source) ?: [] as $name) {
|
||||
if (1 === preg_match(self::MEDIA_NAME, $name) && @copy($source.'/'.$name, $this->mediaDirectory.'/'.$name)) {
|
||||
++$count;
|
||||
}
|
||||
}
|
||||
|
||||
return $count;
|
||||
return \count($neu);
|
||||
}
|
||||
|
||||
private function summary(array $tables, bool $withUsers): string
|
||||
|
||||
Reference in new issue
Block a user