Files
2026-06-07 21:14:30 +03:00

295 lines
11 KiB
PHP

<?php
declare(strict_types=1);
use App\Core\Auth;
use App\Core\Database;
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
Auth::requireAdmin();
$sqlPath = base_path('database/import_old_site_generated.sql');
$summaryPath = base_path('storage/import_old_site_summary.json');
$progressPath = base_path('storage/import_old_site_progress.json');
$summary = is_file($summaryPath)
? json_decode((string) file_get_contents($summaryPath), true)
: [];
$message = null;
$error = null;
$progress = is_file($progressPath)
? json_decode((string) file_get_contents($progressPath), true)
: [];
$importSignature = is_file($sqlPath)
? ((string) filesize($sqlPath) . ':' . (string) filemtime($sqlPath))
: 'missing';
if (($progress['import_signature'] ?? null) !== $importSignature) {
$progress = [];
}
function old_site_import_default_progress(string $importSignature): array
{
return [
'import_signature' => $importSignature,
'offset' => 0,
'statement' => '',
'in_quote' => false,
'escaped' => false,
'executed' => 0,
'done' => false,
'started_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
];
}
function save_old_site_import_progress(string $path, array $progress): void
{
$progress['updated_at'] = date('Y-m-d H:i:s');
file_put_contents($path, json_encode($progress, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
}
function execute_old_site_import_batch(string $sqlPath, string $progressPath, array $progress): array
{
if (!is_file($sqlPath)) {
throw new RuntimeException('Файл импорта не найден.');
}
@set_time_limit(20);
$pdo = Database::pdo();
$handle = fopen($sqlPath, 'rb');
if ($handle === false) {
throw new RuntimeException('Не удалось открыть файл импорта.');
}
$fileSize = max(1, filesize($sqlPath) ?: 1);
$offset = max(0, (int) ($progress['offset'] ?? 0));
$statement = (string) ($progress['statement'] ?? '');
$executedTotal = (int) ($progress['executed'] ?? 0);
$executedBatch = 0;
$inQuote = (bool) ($progress['in_quote'] ?? false);
$escaped = (bool) ($progress['escaped'] ?? false);
$started = microtime(true);
$maxSeconds = 7.0;
$maxStatements = 500;
$pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
fseek($handle, $offset);
while (($chunk = fread($handle, 256 * 1024)) !== false && $chunk !== '') {
$length = strlen($chunk);
$chunkStartOffset = $offset;
for ($i = 0; $i < $length; $i++) {
$char = $chunk[$i];
$statement .= $char;
$offset = $chunkStartOffset + $i + 1;
if ($inQuote) {
if ($escaped) {
$escaped = false;
} elseif ($char === '\\') {
$escaped = true;
} elseif ($char === "'") {
$inQuote = false;
}
continue;
}
if ($char === "'") {
$inQuote = true;
continue;
}
if ($char !== ';') {
continue;
}
$sql = trim($statement);
$statement = '';
if ($sql === '') {
continue;
}
$pdo->exec($sql);
$executedBatch++;
$executedTotal++;
if ($executedBatch >= $maxStatements || (microtime(true) - $started) >= $maxSeconds) {
$progress = [
'offset' => $offset,
'statement' => '',
'in_quote' => false,
'escaped' => false,
'executed' => $executedTotal,
'done' => false,
'import_signature' => $progress['import_signature'] ?? '',
'started_at' => $progress['started_at'] ?? date('Y-m-d H:i:s'),
];
save_old_site_import_progress($progressPath, $progress);
$pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
fclose($handle);
return $progress + [
'batch_executed' => $executedBatch,
'percent' => min(99.9, round(($offset / $fileSize) * 100, 1)),
];
}
}
}
fclose($handle);
$tail = trim($statement);
if ($tail !== '') {
$pdo->exec($tail);
$executedBatch++;
$executedTotal++;
}
$pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
$progress = [
'offset' => $fileSize,
'statement' => '',
'in_quote' => false,
'escaped' => false,
'executed' => $executedTotal,
'done' => true,
'import_signature' => $progress['import_signature'] ?? '',
'started_at' => $progress['started_at'] ?? date('Y-m-d H:i:s'),
];
save_old_site_import_progress($progressPath, $progress);
return $progress + [
'batch_executed' => $executedBatch,
'percent' => 100,
];
}
function normalize_old_site_product_categories(PDO $pdo): void
{
$pdo->exec(
"UPDATE products p
SET p.category_id = (
SELECT pcl.category_id
FROM product_category_links pcl
INNER JOIN categories c ON c.id = pcl.category_id
WHERE pcl.product_id = p.id
AND c.is_active = 1
ORDER BY c.parent_id IS NULL, pcl.sort_order, pcl.category_id
LIMIT 1
)
WHERE p.legacy_path LIKE '/katalog/product/view/1/%'
AND EXISTS (
SELECT 1
FROM product_category_links pcl_exists
WHERE pcl_exists.product_id = p.id
)"
);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$action = (string) ($_POST['action'] ?? 'start');
if ($action === 'start') {
$progress = old_site_import_default_progress($importSignature);
save_old_site_import_progress($progressPath, $progress);
}
if (($progress['import_signature'] ?? null) !== $importSignature) {
throw new RuntimeException('Файл импорта обновился. Нажмите «Запустить импорт заново».');
}
$progress = execute_old_site_import_batch($sqlPath, $progressPath, $progress);
if (!empty($progress['done'])) {
normalize_old_site_product_categories(Database::pdo());
$message = 'Импорт выполнен. SQL-команд выполнено: ' . (int) $progress['executed'] . '.';
} else {
$message = 'Импорт продолжается: ' . (float) $progress['percent'] . '%, команд за этот шаг: ' . (int) $progress['batch_executed'] . '.';
}
} catch (Throwable $exception) {
$error = $exception->getMessage();
}
}
$fileSize = is_file($sqlPath) ? max(1, filesize($sqlPath) ?: 1) : 1;
$progressPercent = !empty($progress['done'])
? 100
: min(99.9, round((((int) ($progress['offset'] ?? 0)) / $fileSize) * 100, 1));
?>
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Импорт старого сайта - Рыбсток</title>
<link rel="stylesheet" href="<?= e(versioned_asset('/assets/css/app.css')) ?>">
</head>
<body>
<main class="page">
<section class="panel">
<p class="eyebrow">Админка</p>
<h1>Импорт старого сайта</h1>
<p class="muted">Страница переносит подготовленные данные Joomla/JoomShopping в новую структуру Рыбсток.</p>
<p class="muted">Импорт выполняется маленькими шагами, чтобы сервер не обрывал процесс по 504 Gateway Timeout.</p>
<?php if ($message !== null): ?>
<div class="status-box"><?= e($message) ?></div>
<?php endif; ?>
<?php if ($error !== null): ?>
<div class="status-box danger"><?= e($error) ?></div>
<?php endif; ?>
<?php if ($summary !== []): ?>
<dl class="settings-list">
<div><dt>Категорий</dt><dd><?= e((string) ($summary['categories'] ?? 0)) ?></dd></div>
<div><dt>Товаров</dt><dd><?= e((string) ($summary['products'] ?? 0)) ?></dd></div>
<div><dt>Фасовок</dt><dd><?= e((string) ($summary['product_variants'] ?? 0)) ?></dd></div>
<div><dt>Связей товар-категория</dt><dd><?= e((string) ($summary['product_category_links'] ?? 0)) ?></dd></div>
<div><dt>Картинок товаров</dt><dd><?= e((string) ($summary['copied_product_images'] ?? 0)) ?></dd></div>
<div><dt>Картинок категорий</dt><dd><?= e((string) ($summary['copied_category_images'] ?? 0)) ?></dd></div>
<div><dt>Картинок из описаний</dt><dd><?= e((string) ($summary['copied_content_images'] ?? 0)) ?></dd></div>
<div><dt>Редиректов категорий из sitemap</dt><dd><?= e((string) ($summary['sitemap_category_redirects'] ?? 0)) ?></dd></div>
<div><dt>SQL-файл</dt><dd><?= e((string) ($summary['sql_file'] ?? '')) ?></dd></div>
</dl>
<?php endif; ?>
<?php if ($progress !== []): ?>
<div class="status-box">
<strong><?= e((string) $progressPercent) ?>%</strong>
<span>Выполнено SQL-команд: <?= e((string) ((int) ($progress['executed'] ?? 0))) ?></span>
</div>
<?php endif; ?>
<?php if ($error === null && $progress !== [] && empty($progress['done'])): ?>
<form id="continue-import-form" method="post">
<input type="hidden" name="action" value="continue">
<button class="primary-button" type="submit">Продолжить импорт</button>
</form>
<script>
window.setTimeout(function () {
document.getElementById('continue-import-form').submit();
}, 700);
</script>
<?php else: ?>
<form method="post" onsubmit="return confirm('Запустить импорт заново? Текущие товары и категории будут заменены данными старого сайта.');">
<input type="hidden" name="action" value="start">
<button class="primary-button" type="submit">Запустить импорт заново</button>
</form>
<?php endif; ?>
<p>
<a class="secondary-button" href="/admin/">Вернуться в админку</a>
</p>
</section>
</main>
</body>
</html>