Publier Globinours 1.0.0-rc.3
This commit is contained in:
parent
ea8c24d622
commit
9a2b4068da
325 changed files with 38230 additions and 20 deletions
90
scripts/asm3-analyze.php
Normal file
90
scripts/asm3-analyze.php
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require dirname(__DIR__) . '/app/Services/Asm3Analyzer.php';
|
||||
|
||||
$args = array_values(array_slice($argv, 1));
|
||||
$json = false;
|
||||
$output = null;
|
||||
$source = null;
|
||||
for ($i = 0; $i < count($args); $i++) {
|
||||
if ($args[$i] === '--json') {
|
||||
$json = true;
|
||||
continue;
|
||||
}
|
||||
if ($args[$i] === '--output') {
|
||||
$output = $args[++$i] ?? null;
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($args[$i], '--')) {
|
||||
fwrite(STDERR, "Option inconnue : {$args[$i]}\n");
|
||||
exit(2);
|
||||
}
|
||||
$source = $args[$i];
|
||||
}
|
||||
if (!$source) {
|
||||
fwrite(STDERR, "Usage : php scripts/asm3-analyze.php <dump.sql> [--json] [--output rapport.md]\n");
|
||||
exit(2);
|
||||
}
|
||||
try {
|
||||
$report = Asm3Analyzer::analyze($source);
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, "Erreur : {$e->getMessage()}\n");
|
||||
exit(1);
|
||||
}
|
||||
if ($json) {
|
||||
$content = json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
} else {
|
||||
$a = $report['animals'];
|
||||
$content = "# Rapport de migration ASM3 → Globinours\n\n";
|
||||
$content .=
|
||||
"Analyse en lecture seule de `{$report['source']}` (" .
|
||||
number_format($report['size_bytes'] / 1048576, 2, ',', ' ') .
|
||||
" Mio). Aucune donnée Globinours n’a été modifiée.\n\n";
|
||||
$content .= "## Synthèse animale\n\n| Indicateur | Nombre |\n|---|---:|\n";
|
||||
$content .= '| Total des animaux | ' . ($a['active'] + $a['archived']) . " |\n";
|
||||
foreach (
|
||||
[
|
||||
'Dossiers actifs dans ASM3' => 'active',
|
||||
'Dossiers archivés dans ASM3' => 'archived',
|
||||
'Déclarés adoptables' => 'adoptable',
|
||||
'Non adoptables' => 'not_adoptable',
|
||||
'Décédés' => 'deceased',
|
||||
'Pucés' => 'microchip',
|
||||
'Tatoués' => 'tattoo',
|
||||
'Stérilisés' => 'neutered',
|
||||
'Sans nom' => 'missing_name',
|
||||
'Sans naissance' => 'missing_birth_date',
|
||||
]
|
||||
as $label => $key
|
||||
) {
|
||||
$content .= "| {$label} | {$a[$key]} |\n";
|
||||
}
|
||||
$content .=
|
||||
"\n## Périmètre de migration\n\n| Domaine ASM3 | Table | Lignes | Cible Globinours | État |\n|---|---|---:|---|---|\n";
|
||||
foreach ($report['domains'] as $d) {
|
||||
$state =
|
||||
['ready' => 'Prêt à mapper', 'planned' => 'À implémenter', 'review' => 'À étudier'][$d['status']] ??
|
||||
$d['status'];
|
||||
$content .= "| {$d['label']} | `{$d['table']}` | {$d['rows']} | `{$d['target']}` | {$state} |\n";
|
||||
}
|
||||
$content .=
|
||||
"\n## Alertes\n\n" .
|
||||
($report['warnings']
|
||||
? implode("\n", array_map(static fn($w) => '- ' . $w, $report['warnings']))
|
||||
: '- Aucune anomalie bloquante détectée.') .
|
||||
"\n";
|
||||
$content .= "\n## Tables non encore mappées\n\n";
|
||||
foreach (array_slice($report['unmapped_tables'], 0, 30, true) as $table => $count) {
|
||||
$content .= "- `{$table}` : {$count} ligne(s)\n";
|
||||
}
|
||||
}
|
||||
if ($output) {
|
||||
if (file_put_contents($output, $content) === false) {
|
||||
fwrite(STDERR, "Impossible d’écrire {$output}\n");
|
||||
exit(1);
|
||||
}
|
||||
fwrite(STDOUT, "Rapport écrit dans {$output}\n");
|
||||
} else {
|
||||
fwrite(STDOUT, $content);
|
||||
}
|
||||
21
scripts/asm3-clinical.php
Normal file
21
scripts/asm3-clinical.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
$root = dirname(__DIR__);
|
||||
foreach (['DB', 'Migrations', 'BackupService', 'AppSettings', 'Asm3Analyzer', 'Asm3ClinicalImporter'] as $s) {
|
||||
require $root . '/app/Services/' . $s . '.php';
|
||||
}
|
||||
$path = $argv[1] ?? null;
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
if (!$path) {
|
||||
fwrite(STDERR, "Usage : php scripts/asm3-clinical.php dump.sql [--apply]\n");
|
||||
exit(2);
|
||||
}
|
||||
if ($apply) {
|
||||
fwrite(STDERR, 'Tapez IMPORTER MEDICAL : ');
|
||||
if (trim((string) fgets(STDIN)) !== 'IMPORTER MEDICAL') {
|
||||
exit(3);
|
||||
}
|
||||
}
|
||||
Migrations::run($root . '/migrations');
|
||||
echo json_encode(Asm3ClinicalImporter::run($path, DB::pdo(), $apply), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), "\n";
|
||||
41
scripts/asm3-contacts.php
Normal file
41
scripts/asm3-contacts.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
$root = dirname(__DIR__);
|
||||
foreach (
|
||||
['DB', 'Migrations', 'BackupService', 'AppSettings', 'Asm3Analyzer', 'Asm3ContactPlanner', 'Asm3ContactImporter']
|
||||
as $service
|
||||
) {
|
||||
require $root . '/app/Services/' . $service . '.php';
|
||||
}
|
||||
$source = null;
|
||||
$apply = false;
|
||||
foreach (array_slice($argv, 1) as $arg) {
|
||||
if ($arg === '--apply') {
|
||||
$apply = true;
|
||||
} elseif (str_starts_with($arg, '--')) {
|
||||
fwrite(STDERR, "Option inconnue : {$arg}\n");
|
||||
exit(2);
|
||||
} else {
|
||||
$source = $arg;
|
||||
}
|
||||
}
|
||||
if (!$source) {
|
||||
fwrite(STDERR, "Usage : php scripts/asm3-contacts.php <dump.sql> [--apply]\n");
|
||||
exit(2);
|
||||
}
|
||||
if ($apply) {
|
||||
fwrite(STDERR, 'IMPORT CONTACTS demandé. Tapez IMPORTER CONTACTS : ');
|
||||
if (trim((string) fgets(STDIN)) !== 'IMPORTER CONTACTS') {
|
||||
fwrite(STDERR, "Import annulé.\n");
|
||||
exit(3);
|
||||
}
|
||||
}
|
||||
try {
|
||||
Migrations::run($root . '/migrations');
|
||||
echo json_encode(Asm3ContactImporter::run($source, DB::pdo(), $apply), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
|
||||
"\n";
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, "Échec : {$e->getMessage()}\n");
|
||||
exit(1);
|
||||
}
|
||||
46
scripts/asm3-import.php
Normal file
46
scripts/asm3-import.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
$root = dirname(__DIR__);
|
||||
require $root . '/app/Services/DB.php';
|
||||
require $root . '/app/Services/Migrations.php';
|
||||
require $root . '/app/Services/BackupService.php';
|
||||
require $root . '/app/Services/AppSettings.php';
|
||||
require $root . '/app/Services/Asm3Analyzer.php';
|
||||
require $root . '/app/Services/Asm3MigrationPlanner.php';
|
||||
require $root . '/app/Services/Asm3AnimalImporter.php';
|
||||
$source = null;
|
||||
$apply = false;
|
||||
foreach (array_slice($argv, 1) as $arg) {
|
||||
if ($arg === '--apply') {
|
||||
$apply = true;
|
||||
continue;
|
||||
}
|
||||
if (str_starts_with($arg, '--')) {
|
||||
fwrite(STDERR, "Option inconnue : {$arg}\n");
|
||||
exit(2);
|
||||
}
|
||||
$source = $arg;
|
||||
}
|
||||
if (!$source) {
|
||||
fwrite(
|
||||
STDERR,
|
||||
"Usage : php scripts/asm3-import.php <dump.sql> [--apply]\nSans --apply, aucune donnée métier n’est écrite.\n",
|
||||
);
|
||||
exit(2);
|
||||
}
|
||||
if ($apply) {
|
||||
fwrite(STDERR, 'IMPORT RÉEL demandé. Tapez exactement IMPORTER ASM3 pour continuer : ');
|
||||
if (trim((string) fgets(STDIN)) !== 'IMPORTER ASM3') {
|
||||
fwrite(STDERR, "Import annulé.\n");
|
||||
exit(3);
|
||||
}
|
||||
}
|
||||
try {
|
||||
Migrations::run($root . '/migrations');
|
||||
$result = Asm3AnimalImporter::run($source, DB::pdo(), $apply);
|
||||
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, "Échec : {$e->getMessage()}\n");
|
||||
exit(1);
|
||||
}
|
||||
20
scripts/asm3-litters.php
Normal file
20
scripts/asm3-litters.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
$root = dirname(__DIR__);
|
||||
foreach (['DB', 'Migrations', 'BackupService', 'AppSettings', 'Asm3Analyzer', 'Asm3LitterImporter'] as $s) {
|
||||
require $root . '/app/Services/' . $s . '.php';
|
||||
}
|
||||
$p = $argv[1] ?? null;
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
if (!$p) {
|
||||
exit(2);
|
||||
}
|
||||
if ($apply) {
|
||||
fwrite(STDERR, 'Tapez IMPORTER PORTEES : ');
|
||||
if (trim((string) fgets(STDIN)) !== 'IMPORTER PORTEES') {
|
||||
exit(3);
|
||||
}
|
||||
}
|
||||
Migrations::run($root . '/migrations');
|
||||
echo json_encode(Asm3LitterImporter::run($p, DB::pdo(), $apply), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), "\n";
|
||||
38
scripts/asm3-movements.php
Normal file
38
scripts/asm3-movements.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
$root = dirname(__DIR__);
|
||||
foreach (['DB', 'Migrations', 'BackupService', 'AppSettings', 'Asm3Analyzer', 'Asm3MovementImporter'] as $service) {
|
||||
require $root . '/app/Services/' . $service . '.php';
|
||||
}
|
||||
$source = null;
|
||||
$apply = false;
|
||||
foreach (array_slice($argv, 1) as $arg) {
|
||||
if ($arg === '--apply') {
|
||||
$apply = true;
|
||||
} elseif (str_starts_with($arg, '--')) {
|
||||
fwrite(STDERR, "Option inconnue : {$arg}\n");
|
||||
exit(2);
|
||||
} else {
|
||||
$source = $arg;
|
||||
}
|
||||
}
|
||||
if (!$source) {
|
||||
fwrite(STDERR, "Usage : php scripts/asm3-movements.php <dump.sql> [--apply]\n");
|
||||
exit(2);
|
||||
}
|
||||
if ($apply) {
|
||||
fwrite(STDERR, 'IMPORT MOUVEMENTS demandé. Tapez IMPORTER MOUVEMENTS : ');
|
||||
if (trim((string) fgets(STDIN)) !== 'IMPORTER MOUVEMENTS') {
|
||||
fwrite(STDERR, "Import annulé.\n");
|
||||
exit(3);
|
||||
}
|
||||
}
|
||||
try {
|
||||
Migrations::run($root . '/migrations');
|
||||
echo json_encode(Asm3MovementImporter::run($source, DB::pdo(), $apply), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
|
||||
"\n";
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, "Échec : {$e->getMessage()}\n");
|
||||
exit(1);
|
||||
}
|
||||
28
scripts/asm3-plan.php
Normal file
28
scripts/asm3-plan.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require dirname(__DIR__) . '/app/Services/DB.php';
|
||||
require dirname(__DIR__) . '/app/Services/Asm3Analyzer.php';
|
||||
require dirname(__DIR__) . '/app/Services/Asm3MigrationPlanner.php';
|
||||
$source = $argv[1] ?? null;
|
||||
$output = $argv[2] ?? null;
|
||||
if (!$source) {
|
||||
fwrite(STDERR, "Usage : php scripts/asm3-plan.php <dump.sql> [rapport.json]\n");
|
||||
exit(2);
|
||||
}
|
||||
try {
|
||||
$plan = Asm3MigrationPlanner::plan($source, DB::pdo());
|
||||
$json = json_encode($plan, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, "Erreur : {$e->getMessage()}\n");
|
||||
exit(1);
|
||||
}
|
||||
if ($output) {
|
||||
if (file_put_contents($output, $json) === false) {
|
||||
fwrite(STDERR, "Impossible d’écrire le rapport.\n");
|
||||
exit(1);
|
||||
}
|
||||
echo "Plan écrit dans {$output}\n";
|
||||
} else {
|
||||
echo $json;
|
||||
}
|
||||
18
scripts/backup.php
Normal file
18
scripts/backup.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit(1);
|
||||
}
|
||||
$root = dirname(__DIR__);
|
||||
require_once $root . '/app/Services/DB.php';
|
||||
require_once $root . '/app/Services/Migrations.php';
|
||||
require_once $root . '/app/Services/I18n.php';
|
||||
require_once $root . '/app/Services/AppSettings.php';
|
||||
require_once $root . '/app/Services/BackupService.php';
|
||||
Migrations::run($root . '/migrations');
|
||||
I18n::setLocale(AppSettings::get('app_language'));
|
||||
$backup = BackupService::create($argv[1] ?? 'cli');
|
||||
fwrite(STDOUT, $backup['name'] . PHP_EOL);
|
||||
108
scripts/check_translations.php
Normal file
108
scripts/check_translations.php
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
$root = dirname(__DIR__);
|
||||
$fr = require $root . '/resources/lang/fr.php';
|
||||
$en = require $root . '/resources/lang/en.php';
|
||||
$used = [];
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($root . '/app', FilesystemIterator::SKIP_DOTS),
|
||||
);
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
$source = file_get_contents($file->getPathname());
|
||||
if ($source === false) {
|
||||
continue;
|
||||
}
|
||||
preg_match_all("/\\bt(?:n)?\\(\\s*['\"]([^'\"]+)['\"]/", $source, $matches);
|
||||
foreach ($matches[1] as $key) {
|
||||
if (!str_ends_with($key, '.')) {
|
||||
$used[$key] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$missingFr = array_diff_key($used, $fr);
|
||||
$missingEn = array_diff_key($used, $en);
|
||||
$orphanFr = array_diff_key($fr, $en);
|
||||
$orphanEn = array_diff_key($en, $fr);
|
||||
foreach (
|
||||
[
|
||||
'fr manquantes' => $missingFr,
|
||||
'en manquantes' => $missingEn,
|
||||
'uniquement fr' => $orphanFr,
|
||||
'uniquement en' => $orphanEn,
|
||||
]
|
||||
as $label => $keys
|
||||
) {
|
||||
if ($keys) {
|
||||
fwrite(STDERR, $label . ' : ' . implode(', ', array_keys($keys)) . PHP_EOL);
|
||||
}
|
||||
}
|
||||
$placeholderErrors = [];
|
||||
foreach (array_intersect_key($fr, $en) as $key => $frText) {
|
||||
preg_match_all('/:([a-zA-Z][a-zA-Z0-9_]*)/', (string) $frText, $frMatches);
|
||||
preg_match_all('/:([a-zA-Z][a-zA-Z0-9_]*)/', (string) $en[$key], $enMatches);
|
||||
$frVars = array_values(array_unique($frMatches[1]));
|
||||
sort($frVars);
|
||||
$enVars = array_values(array_unique($enMatches[1]));
|
||||
sort($enVars);
|
||||
if ($frVars !== $enVars) {
|
||||
$placeholderErrors[] = $key . ' (FR: ' . implode(',', $frVars) . ' · EN: ' . implode(',', $enVars) . ')';
|
||||
}
|
||||
}
|
||||
if ($placeholderErrors) {
|
||||
fwrite(STDERR, 'variables incohérentes : ' . implode('; ', $placeholderErrors) . PHP_EOL);
|
||||
}
|
||||
$confirmationKeys = [
|
||||
'data.confirm_reset_phrase',
|
||||
'data.confirm_demo_phrase',
|
||||
'asm3.confirm_import_phrase',
|
||||
'asm3.confirm_replace_phrase',
|
||||
];
|
||||
$confirmationErrors = [];
|
||||
foreach ($confirmationKeys as $key) {
|
||||
if (trim((string) ($fr[$key] ?? '')) === '' || trim((string) ($en[$key] ?? '')) === '') {
|
||||
$confirmationErrors[] = $key . ' vide';
|
||||
} elseif ($fr[$key] === $en[$key]) {
|
||||
$confirmationErrors[] = $key . ' identique en FR et EN';
|
||||
}
|
||||
}
|
||||
if ($confirmationErrors) {
|
||||
fwrite(STDERR, 'confirmations incohérentes : ' . implode('; ', $confirmationErrors) . PHP_EOL);
|
||||
}
|
||||
$rawOutput = [];
|
||||
foreach (['/app/Controllers', '/app/Services'] as $relative) {
|
||||
$files = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($root . $relative, FilesystemIterator::SKIP_DOTS),
|
||||
);
|
||||
foreach ($files as $file) {
|
||||
if (
|
||||
$file->getExtension() !== 'php' ||
|
||||
in_array($file->getFilename(), ['DevController.php', 'Migrations.php'], true)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
$source = file_get_contents($file->getPathname()) ?: '';
|
||||
if (
|
||||
preg_match(
|
||||
'/(?:\becho\s*|throw\s+new\s+(?:RuntimeException|InvalidArgumentException)\s*\()\s*[\'\"][A-Za-zÀ-ÿ]/u',
|
||||
$source,
|
||||
)
|
||||
) {
|
||||
$rawOutput[] = str_replace($root . '/', '', $file->getPathname());
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($rawOutput) {
|
||||
fwrite(STDERR, 'sorties utilisateur codées en dur : ' . implode(', ', array_unique($rawOutput)) . PHP_EOL);
|
||||
}
|
||||
if ($missingFr || $missingEn || $orphanFr || $orphanEn || $placeholderErrors || $confirmationErrors || $rawOutput) {
|
||||
exit(1);
|
||||
}
|
||||
echo count($used) .
|
||||
' clés utilisées · ' .
|
||||
count($fr) .
|
||||
' traductions FR/EN cohérentes · variables et confirmations vérifiées' .
|
||||
PHP_EOL;
|
||||
165
scripts/import-asm3-photos.php
Normal file
165
scripts/import-asm3-photos.php
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
require $root . '/app/Services/DB.php';
|
||||
require $root . '/app/Services/BackupService.php';
|
||||
require $root . '/app/Services/ImageService.php';
|
||||
require $root . '/app/Services/Asm3Analyzer.php';
|
||||
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
$positional = array_values(
|
||||
array_filter(array_slice($argv, 1), static fn($arg) => !str_starts_with((string) $arg, '--')),
|
||||
);
|
||||
$dump = $positional[0] ?? '';
|
||||
$sourceDir = $positional[1] ?? '';
|
||||
if ($dump === '' || $sourceDir === '') {
|
||||
fwrite(STDERR, "Usage : php scripts/import-asm3-photos.php <dump.sql> <dossier-photos> [--apply]\n");
|
||||
exit(2);
|
||||
}
|
||||
if (!is_file($dump) || !is_dir($sourceDir)) {
|
||||
throw new RuntimeException('Dump ASM3 ou dossier des photos introuvable.');
|
||||
}
|
||||
$hash = hash_file('sha256', $dump);
|
||||
$source = Asm3Analyzer::rows($dump, ['media', 'dbfs']);
|
||||
$db = DB::pdo();
|
||||
$links = [];
|
||||
$q = $db->prepare("SELECT source_id,target_id FROM asm3_import_links WHERE source_sha256=? AND entity_type='animal'");
|
||||
$q->execute([$hash]);
|
||||
foreach ($q as $row) {
|
||||
$links[(int) $row['source_id']] = (int) $row['target_id'];
|
||||
}
|
||||
$dbfs = [];
|
||||
foreach ($source['dbfs'] ?? [] as $row) {
|
||||
$dbfs[(int) $row['ID']] = $row;
|
||||
}
|
||||
$items = [];
|
||||
$errors = [];
|
||||
foreach ($source['media'] ?? [] as $media) {
|
||||
if ((int) $media['LINKTYPEID'] !== 0 || !str_starts_with((string) $media['MEDIAMIMETYPE'], 'image/')) {
|
||||
continue;
|
||||
}
|
||||
$sourceAnimal = (int) $media['LINKID'];
|
||||
$targetAnimal = $links[$sourceAnimal] ?? 0;
|
||||
$physical = $dbfs[(int) $media['DBFSID']] ?? null;
|
||||
$filename = $physical ? basename(str_replace('file:', '', (string) $physical['URL'])) : '';
|
||||
$path = $sourceDir . '/' . $filename;
|
||||
if (!$targetAnimal) {
|
||||
$errors[] = 'Animal ASM3 non relié : ' . $sourceAnimal;
|
||||
} elseif ($filename === '' || !is_file($path)) {
|
||||
$errors[] = 'Fichier absent pour le média ASM3 ' . $media['ID'];
|
||||
} else {
|
||||
$inspection = ImageService::inspect($path);
|
||||
$items[] = [
|
||||
'media' => $media,
|
||||
'animal' => $targetAnimal,
|
||||
'path' => $path,
|
||||
'physical' => $filename,
|
||||
'inspection' => $inspection,
|
||||
];
|
||||
}
|
||||
}
|
||||
if ($errors) {
|
||||
throw new RuntimeException(implode("\n", $errors));
|
||||
}
|
||||
$targets = array_values(array_unique(array_column($items, 'animal')));
|
||||
$primaryCount = count(array_filter($items, static fn(array $i): bool => (int) $i['media']['WEBSITEPHOTO'] === 1));
|
||||
$summary = [
|
||||
'mode' => $apply ? 'APPLIQUE' : 'SIMULATION',
|
||||
'photos_found' => count($items),
|
||||
'animals' => count($targets),
|
||||
'primary_photos' => $primaryCount,
|
||||
'public_photos' => count($items),
|
||||
'already_imported' => 0,
|
||||
'created' => 0,
|
||||
'optimized' => 0,
|
||||
'backup' => null,
|
||||
];
|
||||
foreach ($items as $item) {
|
||||
$original = 'ASM3-' . (int) $item['media']['ID'] . '-' . (string) $item['media']['MEDIANAME'];
|
||||
$s = $db->prepare('SELECT 1 FROM animal_photos WHERE animal_id=? AND original_name=?');
|
||||
$s->execute([$item['animal'], $original]);
|
||||
if ($s->fetchColumn()) {
|
||||
$summary['already_imported']++;
|
||||
}
|
||||
}
|
||||
if (!$apply) {
|
||||
echo json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
|
||||
exit();
|
||||
}
|
||||
$backup = BackupService::create('avant-import-photos-asm3');
|
||||
$summary['backup'] = $backup['name'] ?? null;
|
||||
$written = [];
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
$db->prepare(
|
||||
"INSERT INTO asm3_import_runs(source_name,source_sha256,status,summary_json,backup_name) VALUES(?,?,'completed','{}',?)",
|
||||
)->execute([basename($dump) . ' · photos', $hash, $summary['backup']]);
|
||||
$run = (int) $db->lastInsertId();
|
||||
$reset = $db->prepare('UPDATE animal_photos SET is_primary=0 WHERE animal_id=?');
|
||||
foreach ($targets as $animalId) {
|
||||
$reset->execute([$animalId]);
|
||||
}
|
||||
$insert = $db->prepare(
|
||||
'INSERT INTO animal_photos(animal_id,filename,original_name,mime,size_bytes,is_primary,is_public,caption,created_at) VALUES(?,?,?,?,?,?,1,?,COALESCE(?,datetime(\'now\')))',
|
||||
);
|
||||
$link = $db->prepare(
|
||||
"INSERT OR IGNORE INTO asm3_import_links(source_sha256,entity_type,source_id,target_id,import_run_id) VALUES(?,'media',?,?,?)",
|
||||
);
|
||||
foreach ($items as $item) {
|
||||
$media = $item['media'];
|
||||
$original = 'ASM3-' . (int) $media['ID'] . '-' . (string) $media['MEDIANAME'];
|
||||
$existing = $db->prepare('SELECT id FROM animal_photos WHERE animal_id=? AND original_name=?');
|
||||
$existing->execute([$item['animal'], $original]);
|
||||
$photoId = (int) $existing->fetchColumn();
|
||||
if (!$photoId) {
|
||||
$stored = ImageService::storeLocal(
|
||||
$item['path'],
|
||||
$root . '/public/media/animals/' . $item['animal'],
|
||||
'asm3_' . (int) $media['ID'],
|
||||
'photo',
|
||||
);
|
||||
$written[] = $stored['path'];
|
||||
$caption = trim((string) ($media['MEDIANOTES'] ?? ''));
|
||||
$caption = $caption !== '' ? 'Archive ASM3 · ' . $caption : 'Archive ASM3';
|
||||
$insert->execute([
|
||||
$item['animal'],
|
||||
$stored['filename'],
|
||||
$original,
|
||||
$stored['mime'],
|
||||
$stored['size'],
|
||||
(int) $media['WEBSITEPHOTO'] === 1 ? 1 : 0,
|
||||
$caption,
|
||||
substr((string) $media['CREATEDDATE'], 0, 19) ?: null,
|
||||
]);
|
||||
$photoId = (int) $db->lastInsertId();
|
||||
$summary['created']++;
|
||||
if ($stored['optimized']) {
|
||||
$summary['optimized']++;
|
||||
}
|
||||
} elseif ((int) $media['WEBSITEPHOTO'] === 1) {
|
||||
$db->prepare('UPDATE animal_photos SET is_primary=1,is_public=1 WHERE id=?')->execute([$photoId]);
|
||||
} else {
|
||||
$db->prepare('UPDATE animal_photos SET is_public=1 WHERE id=?')->execute([$photoId]);
|
||||
}
|
||||
$link->execute([$hash, (int) $media['ID'], $photoId, $run]);
|
||||
}
|
||||
$db->prepare('UPDATE asm3_import_runs SET summary_json=? WHERE id=?')->execute([
|
||||
json_encode($summary, JSON_UNESCAPED_UNICODE),
|
||||
$run,
|
||||
]);
|
||||
if ($db->query('PRAGMA foreign_key_check')->fetchAll()) {
|
||||
throw new RuntimeException('Violation de clé étrangère après import.');
|
||||
}
|
||||
$db->commit();
|
||||
} catch (Throwable $e) {
|
||||
if ($db->inTransaction()) {
|
||||
$db->rollBack();
|
||||
}
|
||||
foreach ($written as $path) {
|
||||
@unlink($path);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
echo json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
|
||||
47
scripts/maintenance.php
Normal file
47
scripts/maintenance.php
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
$root = dirname(__DIR__);
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit();
|
||||
}
|
||||
foreach (
|
||||
[
|
||||
'DB',
|
||||
'Migrations',
|
||||
'I18n',
|
||||
'AppSettings',
|
||||
'Auth',
|
||||
'AuditService',
|
||||
'BackupService',
|
||||
'NotificationService',
|
||||
'PrivacyService',
|
||||
]
|
||||
as $class
|
||||
) {
|
||||
require_once $root . '/app/Services/' . $class . '.php';
|
||||
}
|
||||
Migrations::run($root . '/migrations');
|
||||
I18n::setLocale(AppSettings::get('app_language'));
|
||||
$command = $argv[1] ?? 'run';
|
||||
try {
|
||||
$results = [];
|
||||
if (in_array($command, ['run', 'backup'], true)) {
|
||||
$results['backup'] = BackupService::runScheduled($command === 'backup' && in_array('--force', $argv, true));
|
||||
}
|
||||
if (in_array($command, ['run', 'privacy'], true)) {
|
||||
$results['privacy'] = PrivacyService::maintenance();
|
||||
}
|
||||
if (in_array($command, ['run', 'notifications'], true)) {
|
||||
$results['notifications'] = NotificationService::send(false);
|
||||
}
|
||||
echo json_encode(
|
||||
['ok' => true, 'at' => date(DATE_ATOM), 'results' => $results],
|
||||
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE,
|
||||
),
|
||||
PHP_EOL;
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, json_encode(['ok' => false, 'error' => $e->getMessage()], JSON_UNESCAPED_UNICODE) . PHP_EOL);
|
||||
exit(1);
|
||||
}
|
||||
173
scripts/merge-duplicate-contacts.php
Normal file
173
scripts/merge-duplicate-contacts.php
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
require $root . '/app/Services/DB.php';
|
||||
require $root . '/app/Services/BackupService.php';
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
$db = DB::pdo();
|
||||
|
||||
function contactKey(string $value): string
|
||||
{
|
||||
$value = mb_strtolower(trim($value));
|
||||
$ascii = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
|
||||
if ($ascii !== false) {
|
||||
$value = $ascii;
|
||||
}
|
||||
$parts = preg_split('/[^a-z0-9]+/', $value, -1, PREG_SPLIT_NO_EMPTY);
|
||||
sort($parts);
|
||||
return implode(' ', $parts);
|
||||
}
|
||||
function filledScore(array $contact): int
|
||||
{
|
||||
$score = 0;
|
||||
foreach (['phone', 'email', 'address', 'postal_code', 'city', 'notes'] as $field) {
|
||||
if (trim((string) ($contact[$field] ?? '')) !== '') {
|
||||
$score++;
|
||||
}
|
||||
}
|
||||
return $score;
|
||||
}
|
||||
|
||||
$allContacts = $db
|
||||
->query("SELECT * FROM directory_contacts WHERE deleted_at IS NULL AND kind='person'")
|
||||
->fetchAll(PDO::FETCH_ASSOC);
|
||||
$byKey = [];
|
||||
foreach ($allContacts as $contact) {
|
||||
$byKey[contactKey($contact['name'])][] = $contact;
|
||||
}
|
||||
$groups = array_filter($byKey, static fn(array $rows): bool => count($rows) > 1);
|
||||
$singletons = array_values(array_filter($byKey, static fn(array $rows): bool => count($rows) === 1));
|
||||
$used = [];
|
||||
for ($i = 0; $i < count($singletons); $i++) {
|
||||
for ($j = $i + 1; $j < count($singletons); $j++) {
|
||||
$a = $singletons[$i][0];
|
||||
$b = $singletons[$j][0];
|
||||
if (isset($used[$a['id']]) || isset($used[$b['id']])) {
|
||||
continue;
|
||||
}
|
||||
$ka = contactKey($a['name']);
|
||||
$kb = contactKey($b['name']);
|
||||
if (min(strlen($ka), strlen($kb)) >= 8 && levenshtein($ka, $kb) <= 1) {
|
||||
$groups['fuzzy:' . $a['id'] . ':' . $b['id']] = [$a, $b];
|
||||
$used[$a['id']] = $used[$b['id']] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
$foreignKeys = [];
|
||||
foreach (
|
||||
$db
|
||||
->query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
|
||||
->fetchAll(PDO::FETCH_COLUMN)
|
||||
as $table
|
||||
) {
|
||||
foreach (
|
||||
$db->query('PRAGMA foreign_key_list("' . str_replace('"', '""', $table) . '")')->fetchAll(PDO::FETCH_ASSOC)
|
||||
as $fk
|
||||
) {
|
||||
if ($fk['table'] === 'directory_contacts' && $table !== 'directory_contact_roles') {
|
||||
$foreignKeys[] = [$table, $fk['from']];
|
||||
}
|
||||
}
|
||||
}
|
||||
$summary = ['groups' => count($groups), 'contacts_merged' => 0, 'relations_moved' => 0];
|
||||
$details = [];
|
||||
$backup = null;
|
||||
if ($apply) {
|
||||
$backup = BackupService::create('avant-fusion-annuaire');
|
||||
}
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
foreach ($groups as $key => $contacts) {
|
||||
usort($contacts, static function (array $a, array $b): int {
|
||||
$byDetails = filledScore($b) <=> filledScore($a);
|
||||
return $byDetails !== 0 ? $byDetails : (int) $a['id'] <=> (int) $b['id'];
|
||||
});
|
||||
$canonical = array_shift($contacts);
|
||||
$canonicalId = (int) $canonical['id'];
|
||||
$mergedNames = [$canonical['name']];
|
||||
foreach ($contacts as $duplicate) {
|
||||
$duplicateId = (int) $duplicate['id'];
|
||||
$mergedNames[] = $duplicate['name'];
|
||||
foreach ($foreignKeys as [$table, $column]) {
|
||||
$countStmt = $db->prepare("SELECT COUNT(*) FROM \"$table\" WHERE \"$column\"=?");
|
||||
$countStmt->execute([$duplicateId]);
|
||||
$count = (int) $countStmt->fetchColumn();
|
||||
if ($count) {
|
||||
$db->prepare("UPDATE \"$table\" SET \"$column\"=? WHERE \"$column\"=?")->execute([
|
||||
$canonicalId,
|
||||
$duplicateId,
|
||||
]);
|
||||
$summary['relations_moved'] += $count;
|
||||
}
|
||||
}
|
||||
$roles = $db->prepare('SELECT role FROM directory_contact_roles WHERE contact_id=?');
|
||||
$roles->execute([$duplicateId]);
|
||||
foreach ($roles->fetchAll(PDO::FETCH_COLUMN) as $role) {
|
||||
$db->prepare('INSERT OR IGNORE INTO directory_contact_roles(contact_id,role) VALUES(?,?)')->execute([
|
||||
$canonicalId,
|
||||
$role,
|
||||
]);
|
||||
}
|
||||
$db->prepare('DELETE FROM directory_contact_roles WHERE contact_id=?')->execute([$duplicateId]);
|
||||
foreach (
|
||||
['phone', 'email', 'address', 'postal_code', 'city', 'country', 'organization_id', 'notes']
|
||||
as $field
|
||||
) {
|
||||
if (empty($canonical[$field]) && !empty($duplicate[$field])) {
|
||||
$canonical[$field] = $duplicate[$field];
|
||||
}
|
||||
}
|
||||
$db->prepare('DELETE FROM directory_contacts WHERE id=?')->execute([$duplicateId]);
|
||||
$summary['contacts_merged']++;
|
||||
}
|
||||
$db->prepare(
|
||||
'UPDATE directory_contacts SET phone=?,email=?,address=?,postal_code=?,city=?,country=?,organization_id=?,notes=?,updated_at=datetime(\'now\') WHERE id=?',
|
||||
)->execute([
|
||||
$canonical['phone'] ?: null,
|
||||
$canonical['email'] ?: null,
|
||||
$canonical['address'] ?: null,
|
||||
$canonical['postal_code'] ?: null,
|
||||
$canonical['city'] ?: null,
|
||||
$canonical['country'] ?: 'France',
|
||||
$canonical['organization_id'] ?: null,
|
||||
$canonical['notes'] ?: null,
|
||||
$canonicalId,
|
||||
]);
|
||||
foreach (array_unique($mergedNames) as $oldName) {
|
||||
$db->prepare(
|
||||
'UPDATE adoptions SET adopter_name=? WHERE adopter_contact_id=? AND lower(trim(adopter_name))=lower(trim(?))',
|
||||
)->execute([$canonical['name'], $canonicalId, $oldName]);
|
||||
$db->prepare(
|
||||
'UPDATE animal_movements SET contact_name=? WHERE lower(trim(contact_name))=lower(trim(?))',
|
||||
)->execute([$canonical['name'], $oldName]);
|
||||
}
|
||||
$details[] = ['kept_id' => $canonicalId, 'kept_name' => $canonical['name'], 'merged_names' => $mergedNames];
|
||||
}
|
||||
$fk = $db->query('PRAGMA foreign_key_check')->fetchAll();
|
||||
if ($fk) {
|
||||
throw new RuntimeException('Violation de clé étrangère après fusion.');
|
||||
}
|
||||
if ($apply) {
|
||||
$db->commit();
|
||||
} else {
|
||||
$db->rollBack();
|
||||
}
|
||||
echo json_encode(
|
||||
[
|
||||
'mode' => $apply ? 'APPLIQUE' : 'SIMULATION',
|
||||
'backup' => $backup['name'] ?? null,
|
||||
'summary' => $summary,
|
||||
'groups' => $details,
|
||||
],
|
||||
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
|
||||
),
|
||||
"\n";
|
||||
} catch (Throwable $error) {
|
||||
if ($db->inTransaction()) {
|
||||
$db->rollBack();
|
||||
}
|
||||
fwrite(STDERR, 'Fusion interrompue : ' . $error->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
18
scripts/migrate.php
Normal file
18
scripts/migrate.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit(1);
|
||||
}
|
||||
$root = dirname(__DIR__);
|
||||
require_once $root . '/app/Services/DB.php';
|
||||
require_once $root . '/app/Services/Migrations.php';
|
||||
Migrations::run($root . '/migrations');
|
||||
$integrity = DB::pdo()->query('PRAGMA integrity_check')->fetchColumn();
|
||||
if ($integrity !== 'ok') {
|
||||
fwrite(STDERR, "SQLite integrity check failed: $integrity\n");
|
||||
exit(2);
|
||||
}
|
||||
fwrite(STDOUT, "Migrations applied; SQLite integrity: ok\n");
|
||||
106
scripts/reconcile-animal-current-state.php
Normal file
106
scripts/reconcile-animal-current-state.php
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
require $root . '/app/Services/DB.php';
|
||||
require $root . '/app/Services/BackupService.php';
|
||||
$apply = in_array('--apply', $argv, true);
|
||||
$db = DB::pdo();
|
||||
$backup = null;
|
||||
$user =
|
||||
(int) ($db->query("SELECT id FROM users WHERE role='admin' AND active=1 ORDER BY id LIMIT 1")->fetchColumn() ?:
|
||||
0) ?:
|
||||
null;
|
||||
$latest = $db
|
||||
->query(
|
||||
"WITH ranked AS (SELECT ap.*,ROW_NUMBER() OVER(PARTITION BY animal_id ORDER BY date(event_date) DESC,id DESC) rn FROM animal_placements ap) SELECT r.*,a.status current_status,a.archived_at current_archived,dc.name contact_name,dc.address contact_address,dc.postal_code contact_postal,dc.city contact_city,ad.adopter_address,ad.adopter_postal_code,ad.adopter_city FROM ranked r JOIN animals a ON a.id=r.animal_id LEFT JOIN animal_deaths d ON d.animal_id=a.id LEFT JOIN directory_contacts dc ON dc.id=r.contact_id LEFT JOIN adoptions ad ON ad.id=r.adoption_id WHERE r.rn=1 AND r.event_type='adoption' AND d.animal_id IS NULL AND a.deleted_at IS NULL ORDER BY r.event_date,r.animal_id",
|
||||
)
|
||||
->fetchAll(PDO::FETCH_ASSOC);
|
||||
$summary = [
|
||||
'adopted_states_checked' => count($latest),
|
||||
'statuses_fixed' => 0,
|
||||
'locations_fixed' => 0,
|
||||
'movements_created' => 0,
|
||||
'movements_updated' => 0,
|
||||
'history_created' => 0,
|
||||
];
|
||||
if ($apply) {
|
||||
$backup = BackupService::create('avant-reconciliation-etat-animaux');
|
||||
}
|
||||
$db->beginTransaction();
|
||||
try {
|
||||
foreach ($latest as $row) {
|
||||
$animal = (int) $row['animal_id'];
|
||||
$date = (string) $row['event_date'];
|
||||
$contact = trim((string) ($row['contact_name'] ?? '')) ?: 'Adoptant non renseigné';
|
||||
$address = implode(
|
||||
' ',
|
||||
array_filter([
|
||||
trim((string) ($row['contact_address'] ?? '' ?: $row['adopter_address'] ?? '')),
|
||||
trim((string) ($row['contact_postal'] ?? '' ?: $row['adopter_postal_code'] ?? '')),
|
||||
trim((string) ($row['contact_city'] ?? '' ?: $row['adopter_city'] ?? '')),
|
||||
]),
|
||||
);
|
||||
if ($address === '') {
|
||||
$address = 'Adopté par ' . $contact;
|
||||
}
|
||||
if ($row['current_status'] !== 'adopte' || empty($row['current_archived'])) {
|
||||
$summary['statuses_fixed']++;
|
||||
}
|
||||
$current = $db->prepare('SELECT current_address FROM animals WHERE id=?');
|
||||
$current->execute([$animal]);
|
||||
if (trim((string) $current->fetchColumn()) !== $address) {
|
||||
$summary['locations_fixed']++;
|
||||
}
|
||||
$db->prepare(
|
||||
"UPDATE animals SET status='adopte',is_archived=1,archived_at=?,refuge_room=NULL,care_box_key=NULL,quarantine_until=NULL,current_address=?,current_lat=NULL,current_lng=NULL,updated_at=datetime('now') WHERE id=?",
|
||||
)->execute([$date . ' 12:00:00', $address, $animal]);
|
||||
$movement = $db->prepare(
|
||||
"SELECT id FROM animal_movements WHERE animal_id=? AND kind='exit' AND lower(COALESCE(lieu,'')) LIKE 'adopt%' ORDER BY created_at DESC,id DESC LIMIT 1",
|
||||
);
|
||||
$movement->execute([$animal]);
|
||||
$movementId = (int) $movement->fetchColumn();
|
||||
if ($movementId) {
|
||||
$db->prepare(
|
||||
"UPDATE animal_movements SET place=?,lieu='Adoption',contact_name=?,note=CASE WHEN note IS NULL OR note='' THEN 'Sortie synchronisée avec le parcours d’adoption.' ELSE note END,created_at=? WHERE id=?",
|
||||
)->execute([$address, $contact, $date . ' 12:00:00', $movementId]);
|
||||
$summary['movements_updated']++;
|
||||
} else {
|
||||
$db->prepare(
|
||||
"INSERT INTO animal_movements(animal_id,kind,place,lieu,contact_name,note,created_at) VALUES(?,'exit',?,'Adoption',?,'Sortie synchronisée avec le parcours d’adoption.',?)",
|
||||
)->execute([$animal, $address, $contact, $date . ' 12:00:00']);
|
||||
$summary['movements_created']++;
|
||||
}
|
||||
$history = $db->prepare(
|
||||
"SELECT 1 FROM animal_history WHERE animal_id=? AND date(created_at)=date(?) AND (lower(label) LIKE '%adopt%' OR lower(type)='adoption') LIMIT 1",
|
||||
);
|
||||
$history->execute([$animal, $date]);
|
||||
if (!$history->fetchColumn()) {
|
||||
$db->prepare(
|
||||
"INSERT INTO animal_history(animal_id,type,label,details,user_id,created_at) VALUES(?,'status','Adoption de l’animal',?,?,?)",
|
||||
)->execute([$animal, 'Adopté par ' . $contact . '.', $user, $date . ' 12:00:00']);
|
||||
$summary['history_created']++;
|
||||
}
|
||||
}
|
||||
$fk = $db->query('PRAGMA foreign_key_check')->fetchAll();
|
||||
if ($fk) {
|
||||
throw new RuntimeException('Violation de clé étrangère après synchronisation.');
|
||||
}
|
||||
if ($apply) {
|
||||
$db->commit();
|
||||
} else {
|
||||
$db->rollBack();
|
||||
}
|
||||
echo json_encode(
|
||||
['mode' => $apply ? 'APPLIQUE' : 'SIMULATION', 'backup' => $backup['name'] ?? null, 'summary' => $summary],
|
||||
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
|
||||
),
|
||||
"\n";
|
||||
} catch (Throwable $error) {
|
||||
if ($db->inTransaction()) {
|
||||
$db->rollBack();
|
||||
}
|
||||
fwrite(STDERR, 'Synchronisation interrompue : ' . $error->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
16
scripts/rollback.sh
Executable file
16
scripts/rollback.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
root_dir="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
snapshot="${1:-}"
|
||||
if [[ -z "$snapshot" || ! -f "$snapshot" || "$(basename "$snapshot")" != code-*.tar.gz ]]; then
|
||||
echo "Usage: $0 storage/releases/code-VERSION-DATE.tar.gz" >&2
|
||||
exit 2
|
||||
fi
|
||||
case "$(realpath "$snapshot")" in "$root_dir"/storage/releases/*) ;; *) echo "Snapshot hors de storage/releases refusé." >&2; exit 2;; esac
|
||||
touch "$root_dir/storage/maintenance.flag"
|
||||
trap 'rm -f "$root_dir/storage/maintenance.flag"' EXIT
|
||||
tar -xzf "$snapshot" -C "$root_dir"
|
||||
php "$root_dir/scripts/migrate.php"
|
||||
rm -f "$root_dir/storage/maintenance.flag"
|
||||
echo "Code restauré. La base n’a pas été rétrogradée automatiquement."
|
||||
20
scripts/send-notifications.php
Normal file
20
scripts/send-notifications.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit();
|
||||
}
|
||||
$root = dirname(__DIR__);
|
||||
require $root . '/app/Services/DB.php';
|
||||
require $root . '/app/Services/Migrations.php';
|
||||
require $root . '/app/Services/I18n.php';
|
||||
require $root . '/app/Services/AppSettings.php';
|
||||
require $root . '/app/Services/Auth.php';
|
||||
require $root . '/app/Services/BackupService.php';
|
||||
require $root . '/app/Services/NotificationService.php';
|
||||
Migrations::run($root . '/migrations');
|
||||
I18n::setLocale(AppSettings::get('app_language'));
|
||||
$result = NotificationService::send(false);
|
||||
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||
exit(($result['sent'] ?? false) || in_array($result['reason'] ?? '', ['disabled', 'not_due'], true) ? 0 : 1);
|
||||
43
scripts/update.sh
Executable file
43
scripts/update.sh
Executable file
|
|
@ -0,0 +1,43 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
root_dir="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
release_archive="${1:-}"
|
||||
if [[ -z "$release_archive" || ! -f "$release_archive" ]]; then
|
||||
echo "Usage: $0 /chemin/vers/globinours.tar.gz" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! -f "$root_dir/public/index.php" || ! -f "$root_dir/VERSION" ]]; then
|
||||
echo "Installation Globinours invalide: $root_dir" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
stage_dir="$(mktemp -d /tmp/globinours-update-XXXXXX)"
|
||||
trap 'rm -rf "$stage_dir"; rm -f "$root_dir/storage/maintenance.flag"' EXIT
|
||||
if tar -tzf "$release_archive" | grep -Eq '(^/|(^|/)\.\.(/|$))'; then
|
||||
echo "Archive refusée: chemin dangereux." >&2
|
||||
exit 3
|
||||
fi
|
||||
tar -xzf "$release_archive" -C "$stage_dir"
|
||||
source_dir="$stage_dir"
|
||||
if [[ ! -f "$source_dir/public/index.php" ]]; then
|
||||
first_dir="$(find "$stage_dir" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
|
||||
source_dir="$first_dir"
|
||||
fi
|
||||
if [[ ! -f "$source_dir/public/index.php" || ! -f "$source_dir/VERSION" ]]; then
|
||||
echo "Archive Globinours invalide." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
mkdir -p "$root_dir/storage/releases"
|
||||
current_version="$(tr -d '[:space:]' < "$root_dir/VERSION")"
|
||||
new_version="$(tr -d '[:space:]' < "$source_dir/VERSION")"
|
||||
rollback_archive="$root_dir/storage/releases/code-${current_version}-$(date +%Y%m%d-%H%M%S).tar.gz"
|
||||
php "$root_dir/scripts/backup.php" pre-upgrade
|
||||
tar --exclude='./data' --exclude='./storage' --exclude='./public/media' --exclude='./.env' -czf "$rollback_archive" -C "$root_dir" .
|
||||
touch "$root_dir/storage/maintenance.flag"
|
||||
rsync -a --delete --exclude=data --exclude=storage --exclude=public/media --exclude=.env "$source_dir/" "$root_dir/"
|
||||
php "$root_dir/scripts/migrate.php"
|
||||
rm -f "$root_dir/storage/maintenance.flag"
|
||||
echo "Globinours $current_version -> $new_version"
|
||||
echo "Retour arrière du code: $rollback_archive"
|
||||
Loading…
Add table
Add a link
Reference in a new issue