209 lines
8.6 KiB
PHP
209 lines
8.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
final class Asm3ImportController
|
|
{
|
|
public static function index(): void
|
|
{
|
|
self::admin();
|
|
$jobs = DB::pdo()
|
|
->query(
|
|
'SELECT j.*,COALESCE(u.display_name,u.username) actor FROM asm3_import_jobs j LEFT JOIN users u ON u.id=j.created_by ORDER BY j.id DESC LIMIT 20',
|
|
)
|
|
->fetchAll(PDO::FETCH_ASSOC);
|
|
render('settings_asm3.php', [
|
|
'title' => t('asm3.title'),
|
|
'jobs' => $jobs,
|
|
'error' => (string) ($_GET['error'] ?? ''),
|
|
'success' => (string) ($_GET['success'] ?? ''),
|
|
]);
|
|
}
|
|
public static function upload(): void
|
|
{
|
|
self::admin();
|
|
$f = $_FILES['dump'] ?? null;
|
|
if (!is_array($f)) {
|
|
self::fail(
|
|
self::postTooLarge()
|
|
? t('asm3.post_too_large', ['limit' => ini_get('post_max_size')])
|
|
: t('asm3.select_dump'),
|
|
);
|
|
}
|
|
$uploadError = (int) ($f['error'] ?? UPLOAD_ERR_NO_FILE);
|
|
if ($uploadError !== UPLOAD_ERR_OK) {
|
|
self::fail(self::uploadErrorMessage($uploadError));
|
|
}
|
|
if (!is_uploaded_file((string) $f['tmp_name'])) {
|
|
self::fail(t('asm3.upload_unverified'));
|
|
}
|
|
if ((int) $f['size'] > 250_000_000) {
|
|
self::fail(t('asm3.over_250'));
|
|
}
|
|
$name = (string) $f['name'];
|
|
$extension = strtolower(pathinfo($name, PATHINFO_EXTENSION));
|
|
if (!in_array($extension, ['sql', 'pgsql'], true)) {
|
|
self::fail(t('asm3.extension'));
|
|
}
|
|
$dir = dirname(__DIR__, 2) . '/storage/asm3-imports';
|
|
if (!is_dir($dir) && !mkdir($dir, 0700, true) && !is_dir($dir)) {
|
|
self::fail(t('asm3.storage_unavailable'));
|
|
}
|
|
$stored = bin2hex(random_bytes(16)) . '.' . $extension;
|
|
$path = $dir . '/' . $stored;
|
|
if (!move_uploaded_file((string) $f['tmp_name'], $path)) {
|
|
self::fail(t('asm3.storage_failed'));
|
|
}
|
|
chmod($path, 0600);
|
|
try {
|
|
$analysis = Asm3Analyzer::analyze($path);
|
|
if (($analysis['tables']['animal'] ?? 0) < 1) {
|
|
throw new RuntimeException(t('asm3.no_animal'));
|
|
}
|
|
$animals = Asm3MigrationPlanner::plan($path, DB::pdo());
|
|
$contacts = Asm3ContactPlanner::plan($path, DB::pdo());
|
|
$report = [
|
|
'analysis' => $analysis,
|
|
'animals' => $animals['summary'],
|
|
'contacts' => $contacts['summary'],
|
|
'movements' => Asm3MovementImporter::run($path, DB::pdo(), false)['summary'],
|
|
'clinical' => Asm3ClinicalImporter::run($path, DB::pdo(), false),
|
|
'litters' => Asm3LitterImporter::run($path, DB::pdo(), false),
|
|
'media' => [
|
|
'metadata' => $analysis['tables']['media'] ?? 0,
|
|
'importable' => 0,
|
|
'reason' => 'Le dump SQL ne contient pas les fichiers binaires ASM3.',
|
|
],
|
|
];
|
|
$db = DB::pdo();
|
|
$db->prepare(
|
|
'INSERT INTO asm3_import_jobs(original_name,stored_name,source_sha256,analysis_json,created_by) VALUES(:original,:stored,:hash,:analysis,:user)',
|
|
)->execute([
|
|
':original' => basename($name),
|
|
':stored' => $stored,
|
|
':hash' => hash_file('sha256', $path),
|
|
':analysis' => json_encode($report, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
|
|
':user' => Auth::id(),
|
|
]);
|
|
AuditService::log('asm3_analyzed', '/settings/asm3', 'Dump ASM3 analysé : ' . basename($name));
|
|
header('Location: /settings/asm3?success=analyzed');
|
|
exit();
|
|
} catch (Throwable $e) {
|
|
@unlink($path);
|
|
self::fail($e->getMessage());
|
|
}
|
|
}
|
|
public static function apply(): void
|
|
{
|
|
self::admin();
|
|
$id = (int) ($_POST['id'] ?? 0);
|
|
$mode = ($_POST['mode'] ?? 'merge') === 'replace' ? 'replace' : 'merge';
|
|
$expected = t($mode === 'replace' ? 'asm3.confirm_replace_phrase' : 'asm3.confirm_import_phrase');
|
|
if (trim((string) ($_POST['confirmation'] ?? '')) !== $expected) {
|
|
self::fail(t('asm3.confirm_required', ['phrase' => $expected]));
|
|
}
|
|
$db = DB::pdo();
|
|
$q = $db->prepare("SELECT * FROM asm3_import_jobs WHERE id=:id AND status='analyzed'");
|
|
$q->execute([':id' => $id]);
|
|
$job = $q->fetch(PDO::FETCH_ASSOC);
|
|
$q->closeCursor();
|
|
unset($q);
|
|
if (!$job) {
|
|
self::fail(t('asm3.analysis_unavailable'));
|
|
}
|
|
$path = dirname(__DIR__, 2) . '/storage/asm3-imports/' . basename($job['stored_name']);
|
|
if (!is_file($path) || !hash_equals($job['source_sha256'], hash_file('sha256', $path))) {
|
|
self::fail(t('asm3.dump_altered'));
|
|
}
|
|
try {
|
|
$backup = BackupService::create('pre-asm3-import');
|
|
$db->beginTransaction();
|
|
$removed = $mode === 'replace' ? Asm3ResetService::clear($db) : null;
|
|
$results = [
|
|
'mode' => $mode,
|
|
'backup' => $backup['name'],
|
|
'removed' => $removed,
|
|
'animals' => Asm3AnimalImporter::run($path, $db, true, false),
|
|
'contacts' => Asm3ContactImporter::run($path, $db, true, false),
|
|
'movements' => Asm3MovementImporter::run($path, $db, true, false),
|
|
'clinical' => Asm3ClinicalImporter::run($path, $db, true, false),
|
|
'litters' => Asm3LitterImporter::run($path, $db, true, false),
|
|
];
|
|
$db->prepare(
|
|
"UPDATE asm3_import_jobs SET status='completed',result_json=:result,completed_at=datetime('now') WHERE id=:id",
|
|
)->execute([
|
|
':result' => json_encode($results, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
|
|
':id' => $id,
|
|
]);
|
|
$db->commit();
|
|
if ($mode === 'replace') {
|
|
Asm3ResetService::clearMedia();
|
|
}
|
|
AuditService::log(
|
|
'asm3_imported',
|
|
'/settings/asm3/apply',
|
|
'Migration ASM3 terminée en mode ' . $mode . ' depuis ' . $job['original_name'],
|
|
);
|
|
header('Location: /settings/asm3?success=imported');
|
|
exit();
|
|
} catch (Throwable $e) {
|
|
if ($db->inTransaction()) {
|
|
$db->rollBack();
|
|
}
|
|
$db->prepare(
|
|
"UPDATE asm3_import_jobs SET status='failed',result_json=:result,completed_at=datetime('now') WHERE id=:id",
|
|
)->execute([
|
|
':result' => json_encode(['error' => $e->getMessage(), 'mode' => $mode], JSON_UNESCAPED_UNICODE),
|
|
':id' => $id,
|
|
]);
|
|
self::fail(t('asm3.import_failed', ['error' => $e->getMessage()]));
|
|
}
|
|
}
|
|
private static function uploadErrorMessage(int $error): string
|
|
{
|
|
return match ($error) {
|
|
UPLOAD_ERR_INI_SIZE => t('asm3.upload_ini', ['limit' => ini_get('upload_max_filesize')]),
|
|
UPLOAD_ERR_FORM_SIZE => t('asm3.upload_form'),
|
|
UPLOAD_ERR_PARTIAL => t('asm3.upload_partial'),
|
|
UPLOAD_ERR_NO_FILE => t('asm3.select_dump'),
|
|
UPLOAD_ERR_NO_TMP_DIR => t('asm3.tmp_unavailable'),
|
|
UPLOAD_ERR_CANT_WRITE => t('asm3.write_failed'),
|
|
UPLOAD_ERR_EXTENSION => t('asm3.extension_stopped'),
|
|
default => t('asm3.upload_failed', ['code' => $error]),
|
|
};
|
|
}
|
|
private static function postTooLarge(): bool
|
|
{
|
|
$length = (int) ($_SERVER['CONTENT_LENGTH'] ?? 0);
|
|
$limit = self::iniBytes((string) ini_get('post_max_size'));
|
|
return $length > 0 && $limit > 0 && $length > $limit;
|
|
}
|
|
private static function iniBytes(string $value): int
|
|
{
|
|
$value = trim($value);
|
|
if ($value === '') {
|
|
return 0;
|
|
}
|
|
$number = (float) $value;
|
|
return (int) round(
|
|
$number *
|
|
match (strtolower(substr($value, -1))) {
|
|
'g' => 1024 ** 3,
|
|
'm' => 1024 ** 2,
|
|
'k' => 1024,
|
|
default => 1,
|
|
},
|
|
);
|
|
}
|
|
private static function fail(string $message): never
|
|
{
|
|
header('Location: /settings/asm3?error=' . rawurlencode($message));
|
|
exit();
|
|
}
|
|
private static function admin(): void
|
|
{
|
|
if (!Auth::is('admin')) {
|
|
http_response_code(403);
|
|
exit();
|
|
}
|
|
}
|
|
}
|