AJAX-версия для перекодировки файлов Windows-1251 в UTF-8 в 1С-Битрикс. Она состоит из двух файлов:
convert_page.php — страница с интерфейсом и JS, которая по очереди дёргает worker, ждёт 5 секунд и продолжает.
convert_worker.php — обрабатывает файлы порцией по 10 секунд и завершается (никаких sleep() внутри — пауза делается на клиенте).
Такая схема работает на любом хостинге: каждый HTTP-запрос короткий, буферизация не важна, пауза реально видна в логе.
convert_page.php<?require($_SERVER['DOCUMENT_ROOT']."/bitrix/header.php"); ?>
<div style="font-family: monospace; padding: 20px;">
<h2>Перекодировка файлов в UTF-8</h2>
<div style="margin-bottom: 10px;">
<button id="btn-start" onclick="startConvert()">▶ Старт</button>
<button id="btn-stop" onclick="stopConvert()" disabled>■ Стоп</button>
<button id="btn-reset" onclick="resetState()">⟲ Сбросить прогресс</button>
</div>
<div style="margin-bottom: 10px;">
Статус: <span id="status">ожидание</span> |
Обработано: <span id="counter">0</span> |
Осталось примерно: <span id="remaining">?</span>
</div>
<div style="margin-bottom: 5px;">
<progress id="progress" value="0" max="100" style="width: 100%; height: 20px;"></progress>
</div>
<pre id="log" style="background:#111; color:#0f0; padding:10px; height:500px; overflow:auto; border:1px solid #333;"></pre>
</div>
<script>
let running = false;
let totalFiles = 0;
let processedTotal = 0;
const WORKER_URL = '/convert_worker.php';
function log(msg, color) {
const el = document.getElementById('log');
const line = document.createElement('div');
if (color) line.style.color = color;
line.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg;
el.appendChild(line);
el.scrollTop = el.scrollHeight;
}
function setStatus(s) {
document.getElementById('status').textContent = s;
}
function updateProgress(processed, total) {
processedTotal = processed;
totalFiles = total;
document.getElementById('counter').textContent = processed;
const pct = total > 0 ? Math.round(processed / total * 100) : 0;
document.getElementById('progress').value = pct;
document.getElementById('remaining').textContent = (total - processed);
}
function startConvert() {
if (running) return;
running = true;
document.getElementById('btn-start').disabled = true;
document.getElementById('btn-stop').disabled = false;
document.getElementById('btn-reset').disabled = true;
setStatus('работаем...');
log('=== Запуск ===', '#ff0');
step();
}
function stopConvert() {
running = false;
setStatus('остановлено пользователем');
log('=== Остановлено ===', '#f80');
document.getElementById('btn-start').disabled = false;
document.getElementById('btn-stop').disabled = true;
document.getElementById('btn-reset').disabled = false;
}
function resetState() {
if (!confirm('Сбросить прогресс? Все файлы будут проверяться заново.')) return;
fetch(WORKER_URL + '?action=reset')
.then(r => r.json())
.then(d => {
log('Прогресс сброшен', '#f80');
updateProgress(0, 0);
})
.catch(e => log('Ошибка сброса: ' + e, '#f00'));
}
function step() {
if (!running) return;
setStatus('обрабатываю порцию...');
fetch(WORKER_URL + '?action=step')
.then(r => r.json())
.then(data => {
if (data.error) {
log('ОШИБКА: ' + data.error, '#f00');
stopConvert();
return;
}
// Выводим новые строки лога
if (data.log && data.log.length) {
data.log.forEach(line => log(line));
}
updateProgress(data.processed, data.total);
if (data.done) {
setStatus('готово');
log('=== ВСЁ ГОТОВО. Обработано: ' + data.processed + ' ===', '#0f0');
running = false;
document.getElementById('btn-start').disabled = false;
document.getElementById('btn-stop').disabled = true;
document.getElementById('btn-reset').disabled = false;
return;
}
// Пауза 5 секунд и следующий шаг
setStatus('пауза 5 сек...');
log('--- пауза 5 сек ---', '#888');
setTimeout(step, 5000);
})
.catch(err => {
log('Сетевая ошибка: ' + err, '#f00');
setStatus('ошибка');
running = false;
document.getElementById('btn-start').disabled = false;
document.getElementById('btn-stop').disabled = true;
});
}
</script>
<?require($_SERVER['DOCUMENT_ROOT']."/bitrix/footer.php"); ?>
convert_worker.phpПоложите его в корень сайта (рядом с convert_page.php).
<?php
// Worker не подключает header/footer Битрикса — это чистый JSON-ответ.
// При необходимости можно подключить пролог, если нужны настройки, но обычно не нужно.
header('Content-Type: application/json; charset=utf-8');
header('X-Accel-Buffering: no');
set_time_limit(0);
$stateFile = $_SERVER['DOCUMENT_ROOT'] . '/convert_state.json';
$lockFile = $_SERVER['DOCUMENT_ROOT'] . '/convert_state.lock';
$directory = $_SERVER['DOCUMENT_ROOT'] . '/';
$skipPaths = [
$_SERVER['DOCUMENT_ROOT'] . '/upload/',
$_SERVER['DOCUMENT_ROOT'] . '/bitrix/',
];
$allowedExtensions = ['php', 'js', 'css'];
$workTime = 10; // сколько секунд работать в одной порции
$action = isset($_GET['action']) ? $_GET['action'] : 'step';
// ==== Сброс прогресса ====
if ($action === 'reset') {
if (file_exists($stateFile)) @unlink($stateFile);
echo json_encode(['ok' => true]);
exit;
}
// ==== Простая блокировка, чтобы два таба не работали одновременно ====
$fp = @fopen($lockFile, 'c');
if ($fp && !flock($fp, LOCK_EX | LOCK_NB)) {
echo json_encode([
'error' => 'Другой процесс уже выполняет перекодировку. Закройте другие вкладки.',
]);
exit;
}
// ==== Загрузка/сохранение состояния ====
function loadState($stateFile) {
if (file_exists($stateFile)) {
$data = json_decode(file_get_contents($stateFile), true);
if (is_array($data) && isset($data['processed']) && is_array($data['processed'])) {
return $data['processed'];
}
}
return [];
}
function saveState($stateFile, $processed) {
file_put_contents($stateFile, json_encode(['processed' => $processed], JSON_UNESCAPED_UNICODE));
}
function collectFiles($dir, $skipPaths, $allowedExtensions, &$result = []) {
$files = @scandir($dir);
if ($files === false) return $result;
foreach ($files as $file) {
if ($file === '.' || $file === '..') continue;
$path = $dir . '/' . $file;
if (in_array($path, $skipPaths)) continue;
// Не заходим в файл состояния и lock
if (basename($path) === 'convert_state.json' || basename($path) === 'convert_state.lock') {
continue;
}
if (is_dir($path)) {
collectFiles($path, $skipPaths, $allowedExtensions, $result);
} elseif (is_file($path)) {
$ext = pathinfo($path, PATHINFO_EXTENSION);
if (in_array($ext, $allowedExtensions)) {
$result[] = $path;
}
}
}
return $result;
}
// ==== Основная обработка порции ====
$processed = loadState($stateFile);
$processedMap = array_flip($processed);
$allFiles = collectFiles($directory, $skipPaths, $allowedExtensions);
$totalFiles = count($allFiles);
$log = [];
$startTime = microtime(true);
$countInBatch = 0;
$done = false;
foreach ($allFiles as $path) {
if (isset($processedMap[$path])) continue;
// Если время порции вышло — прекращаем, сохраняем состояние и отдаём JSON
if ((microtime(true) - $startTime) >= $workTime) {
$done = false;
break;
}
$content = @file_get_contents($path);
if ($content === false) {
$log[] = "Не удалось прочитать: $path";
$processedMap[$path] = true;
$countInBatch++;
continue;
}
$encoding = mb_detect_encoding($content, ['CP1251', 'Windows-1251', 'UTF-8'], true);
if ($encoding === 'CP1251' || $encoding === 'Windows-1251') {
$contentUtf8 = mb_convert_encoding($content, 'UTF-8', 'CP1251');
@file_put_contents($path, $contentUtf8);
$log[] = "Перекодирован: $path";
} elseif ($encoding === 'UTF-8') {
$log[] = "Уже UTF-8: $path";
} else {
$log[] = "Неизвестная кодировка ($encoding): $path";
}
$processedMap[$path] = true;
$countInBatch++;
}
// Если прошли весь список — значит всё готово
if (count($processedMap) >= $totalFiles) {
$done = true;
}
saveState($stateFile, array_keys($processedMap));
// Освобождаем блокировку
if ($fp) {
flock($fp, LOCK_UN);
fclose($fp);
}
echo json_encode([
'done' => $done,
'processed' => count($processedMap),
'total' => $totalFiles,
'batch' => $countInBatch,
'log' => $log,
], JSON_UNESCAPED_UNICODE);
Пользователь открывает convert_page.php в браузере и жмёт «Старт».
JS вызывает convert_worker.php?action=step.
Worker:
загружает список уже обработанных файлов из convert_state.json;
обходит дерево, пропуская upload/ и bitrix/;
обрабатывает файлы, пока не пройдёт 10 секунд;
сохраняет состояние;
возвращает JSON с новыми строками лога и прогрессом;
завершается — никакого sleep() внутри.
JS выводит новые строки в лог, обновляет прогресс-бар, ждёт 5 секунд через setTimeout и вызывает worker снова.
Когда все файлы обработаны — worker вернёт done: true, JS покажет «ВСЁ ГОТОВО» и остановится.
Паузы видны всегда — они делаются на клиенте, сервер в это время свободен, никакой буферизации ответа ждать не нужно.
Прогресс сохраняется — если браузер закрыть и открыть страницу заново, при нажатии «Старт» обработка продолжится с того же места.
Нет таймаутов — каждый запрос длится максимум ~10 секунд.
Защита от двух вкладок — через flock().
Кнопка «Сбросить прогресс» удаляет convert_state.json — обработка начнётся заново.
Файл convert_state.lock создаётся автоматически; можно удалить вручную, если что-то зависло.
Если хочется поменять время порции — измените $workTime в worker’е. Паузу — setTimeout(step, 5000) на странице.
Оба файла кладите в корень сайта, чтобы пути $_SERVER['DOCUMENT_ROOT'] и URL /convert_worker.php совпадали. Если положите в подпапку — поправьте WORKER_URL в JS.