295 lines
11 KiB
PHP
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>
|