430 слов | 3 минуты
Очистка осиротевших файлов в b_file и /upload/iblock
Инструкция по поиску и удалению неиспользуемых записей в b_file и соответствующих физических файлов.
Контекст задачи: в b_file ~965к записей и столько же файлов в /upload/iblock, но реально инфоблоки используют только ~28к. Остальное — мусор, накопившийся за годы.
Общая логика
Подход — whitelist, а не blacklist:
- Создаём временную таблицу
b_file_usedсо всемиFILE_ID, которые реально используются (инфоблоки + UF + пользователи). - Кандидатами на удаление считаем записи
b_file, которых нет вb_file_used. - Дополнительно ограничиваем удаление только
MODULE_ID='iblock'— чтобы не задеть файлы других модулей (landing, sender, fileman, sale и т.д.), даже если они тоже могут быть осиротевшими. - Удаляем через
CFile::Delete()пакетами, чтобы корректно почистить и физические файлы, и кеши ресайзов.
1. Подготовка: бэкапы
Обязательно перед началом:
# Бэкап БД
mysqldump -u USER -p DBNAME > /backup/db_$(date +%F).sql
# Бэкап /upload/iblock (по желанию, но рекомендуется)
du -sh /home/bitrix/www/upload/iblock/ # прикинуть объём
tar czf /backup/upload_iblock_$(date +%F).tar.gz /home/bitrix/www/upload/iblock/
Если есть стейджинг с копией БД — прогнать сначала на нём.
2. Сбор используемых FILE_ID
2.1. Создание таблицы
DROP TABLE IF EXISTS b_file_used;
CREATE TABLE b_file_used (
FILE_ID INT UNSIGNED NOT NULL,
SOURCE VARCHAR(64) NOT NULL,
PRIMARY KEY (FILE_ID, SOURCE),
KEY idx_file (FILE_ID)
) ENGINE=InnoDB;
2.2. Превью и детальные картинки элементов
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT PREVIEW_PICTURE, 'iblock_element_preview'
FROM b_iblock_element
WHERE PREVIEW_PICTURE IS NOT NULL AND PREVIEW_PICTURE > 0;
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT DETAIL_PICTURE, 'iblock_element_detail'
FROM b_iblock_element
WHERE DETAIL_PICTURE IS NOT NULL AND DETAIL_PICTURE > 0;
2.3. Картинки разделов
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT PICTURE, 'iblock_section_picture'
FROM b_iblock_section
WHERE PICTURE IS NOT NULL AND PICTURE > 0;
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT DETAIL_PICTURE, 'iblock_section_detail'
FROM b_iblock_section
WHERE DETAIL_PICTURE IS NOT NULL AND DETAIL_PICTURE > 0;
2.4. Файловые свойства инфоблоков версии 1
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT DISTINCT ep.VALUE, 'iblock_prop_v1'
FROM b_iblock_element_property ep
INNER JOIN b_iblock_property p ON p.ID = ep.IBLOCK_PROPERTY_ID
WHERE p.PROPERTY_TYPE = 'F'
AND ep.VALUE REGEXP '^[0-9]+$'
AND ep.VALUE > 0;
2.5. Файловые свойства инфоблоков версии 2
Сначала смотрим, какие инфоблоки в v2 и какие у них файловые свойства:
SELECT
ib.ID AS iblock_id,
ib.NAME AS iblock_name,
p.ID AS prop_id,
p.CODE AS prop_code,
p.MULTIPLE,
CASE
WHEN p.MULTIPLE = 'Y' THEN CONCAT('b_iblock_element_prop_m', ib.ID)
ELSE CONCAT('b_iblock_element_prop_s', ib.ID)
END AS storage_table,
CONCAT('PROPERTY_', p.ID) AS column_name
FROM b_iblock ib
INNER JOIN b_iblock_property p ON p.IBLOCK_ID = ib.ID
WHERE ib.VERSION = 2
AND p.PROPERTY_TYPE = 'F'
ORDER BY ib.ID, p.ID;
На основе результата генерируем INSERT'ы. Пример для каталога IBLOCK_ID=7 (4 множественных свойства):
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT VALUE, 'iblock_7_prop_193_pictures'
FROM b_iblock_element_prop_m7
WHERE IBLOCK_PROPERTY_ID = 193
AND VALUE REGEXP '^[0-9]+$' AND VALUE > 0;
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT VALUE, 'iblock_7_prop_472_more_photo'
FROM b_iblock_element_prop_m7
WHERE IBLOCK_PROPERTY_ID = 472
AND VALUE REGEXP '^[0-9]+$' AND VALUE > 0;
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT VALUE, 'iblock_7_prop_473_files'
FROM b_iblock_element_prop_m7
WHERE IBLOCK_PROPERTY_ID = 473
AND VALUE REGEXP '^[0-9]+$' AND VALUE > 0;
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT VALUE, 'iblock_7_prop_804_photos_vk'
FROM b_iblock_element_prop_m7
WHERE IBLOCK_PROPERTY_ID = 804
AND VALUE REGEXP '^[0-9]+$' AND VALUE > 0;
Шаблон для немножественных (если попадутся): брать данные из b_iblock_element_prop_s{IBLOCK_ID}, колонка PROPERTY_{PROP_ID}.
2.6. Пользователи
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT PERSONAL_PHOTO, 'user_photo'
FROM b_user
WHERE PERSONAL_PHOTO IS NOT NULL AND PERSONAL_PHOTO > 0;
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT WORK_LOGO, 'user_logo'
FROM b_user
WHERE WORK_LOGO IS NOT NULL AND WORK_LOGO > 0;
2.7. UserField'ы типа "файл"
Сначала смотрим, какие есть:
SELECT
uf.ID,
uf.ENTITY_ID,
uf.FIELD_NAME,
uf.MULTIPLE,
LOWER(CONCAT('b_uts_', REPLACE(uf.ENTITY_ID, '\\', '_'))) AS single_table,
LOWER(CONCAT('b_utm_', REPLACE(uf.ENTITY_ID, '\\', '_'))) AS multi_table
FROM b_user_field uf
WHERE uf.USER_TYPE_ID = 'file'
ORDER BY uf.ENTITY_ID, uf.FIELD_NAME;
Пример для секций IBLOCK_7_SECTION (одно множественное поле + четыре одиночных):
-- Множественное UF_IMAGES (ID=2)
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT VALUE, 'uf_iblock_7_section_images'
FROM b_utm_iblock_7_section
WHERE FIELD_ID = 2
AND VALUE REGEXP '^[0-9]+$' AND VALUE > 0;
-- Одиночные из b_uts_iblock_7_section
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT UF_PRICE, 'uf_iblock_7_section_price'
FROM b_uts_iblock_7_section
WHERE UF_PRICE IS NOT NULL AND UF_PRICE > 0;
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT UF_SECTION_MENU_ICON, 'uf_iblock_7_section_menu_icon'
FROM b_uts_iblock_7_section
WHERE UF_SECTION_MENU_ICON IS NOT NULL AND UF_SECTION_MENU_ICON > 0;
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT UF_SVG_ICO, 'uf_iblock_7_section_svg_ico'
FROM b_uts_iblock_7_section
WHERE UF_SVG_ICO IS NOT NULL AND UF_SVG_ICO > 0;
INSERT IGNORE INTO b_file_used (FILE_ID, SOURCE)
SELECT UF_SVG_ICO_HOVER, 'uf_iblock_7_section_svg_ico_hover'
FROM b_uts_iblock_7_section
WHERE UF_SVG_ICO_HOVER IS NOT NULL AND UF_SVG_ICO_HOVER > 0;
3. Финальные счётчики
-- Сколько уникальных файлов используется
SELECT COUNT(DISTINCT FILE_ID) AS used_files FROM b_file_used;
-- Всего записей в b_file
SELECT COUNT(*) AS total_files FROM b_file;
-- Сколько кандидатов на удаление по модулю iblock
SELECT COUNT(*) AS iblock_orphans
FROM b_file f
LEFT JOIN b_file_used u ON u.FILE_ID = f.ID
WHERE u.FILE_ID IS NULL
AND f.MODULE_ID = 'iblock';
-- Распределение использования по источникам (для контроля)
SELECT SOURCE, COUNT(*) AS cnt
FROM b_file_used
GROUP BY SOURCE
ORDER BY cnt DESC;
-- Распределение "осиротевших" по MODULE_ID — подскажет, не упустили ли источник
SELECT f.MODULE_ID, COUNT(*) AS cnt
FROM b_file f
LEFT JOIN b_file_used u ON u.FILE_ID = f.ID
WHERE u.FILE_ID IS NULL
GROUP BY f.MODULE_ID
ORDER BY cnt DESC;
Ожидаемая картина:
used_files≈ 28 000total_files≈ 965 000iblock_orphans≈ 934 000
Если в распределении по модулям видны крупные группы для модулей, которыми реально пользуются (например, landing, sender, fileman) — нужно дописать сбор для этих модулей или просто не трогать их при удалении (фильтр MODULE_ID='iblock' это и обеспечивает).
4. Удаление осиротевших файлов
Стратегия
Удаление через CFile::Delete($id), пакетами по 500. Этот метод:
- удаляет физический файл с диска,
- удаляет запись из
b_file, - чистит ресайзы (
b_file_preview+/upload/resize_cache/).
Альтернатива (DELETE FROM b_file ... + rm) быстрее, но оставляет хвосты в b_file_preview и в resize_cache — придётся чистить вручную.
Скрипт cleanup_files.php
Создать файл /home/bitrix/www/local/php_interface/cleanup_files.php:
<?php
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
define('STOP_STATISTICS', true);
define('DisableEventsCheck', true);
require($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');
set_time_limit(0);
ini_set('memory_limit', '1024M');
$batchSize = 500;
$logFile = __DIR__ . '/cleanup_log_' . date('Y-m-d_H-i-s') . '.log';
$totalDeleted = 0;
$totalErrors = 0;
function logMsg($msg, $logFile) {
$line = '[' . date('Y-m-d H:i:s') . '] ' . $msg . PHP_EOL;
echo $line;
file_put_contents($logFile, $line, FILE_APPEND);
}
logMsg("=== Старт очистки ===", $logFile);
global $DB;
while (true) {
// Берём пакет ID кандидатов
$sql = "
SELECT f.ID
FROM b_file f
LEFT JOIN b_file_used u ON u.FILE_ID = f.ID
WHERE u.FILE_ID IS NULL
AND f.MODULE_ID = 'iblock'
LIMIT {$batchSize}
";
$res = $DB->Query($sql);
$ids = [];
while ($row = $res->Fetch()) {
$ids[] = (int)$row['ID'];
}
if (empty($ids)) {
logMsg("Кандидатов больше нет. Готово.", $logFile);
break;
}
foreach ($ids as $fileId) {
try {
CFile::Delete($fileId);
$totalDeleted++;
} catch (Exception $e) {
$totalErrors++;
logMsg("ERROR FILE_ID={$fileId}: " . $e->getMessage(), $logFile);
}
}
logMsg("Пакет: " . count($ids) . " | Всего: {$totalDeleted} | Ошибок: {$totalErrors}", $logFile);
usleep(100000); // 0.1 сек, чтобы не убить диск/БД
}
logMsg("=== Финал. Удалено: {$totalDeleted}, ошибок: {$totalErrors} ===", $logFile);
Запуск
cd /home/bitrix/www
nohup php -f local/php_interface/cleanup_files.php > /tmp/cleanup.out 2>&1 &
tail -f local/php_interface/cleanup_log_*.log
nohup ... & позволит спокойно отключиться от ssh, скрипт продолжит работать. Прогресс в реальном времени — через tail -f.
Скорость
Грубо 100–300 файлов/сек. На ~934к записей — от 1 до 3 часов.
Если железо позволяет — можно увеличить $batchSize до 1000–2000 и убрать usleep.
Прерывание и продолжение
Если скрипт упал/прервался — просто запусти снова. Каждая итерация делает свежий SELECT, поэтому продолжит с того, что осталось.
5. Финальная зачистка
После завершения скрипта:
# Очистить кеш ресайзов целиком (Bitrix пересоздаст по запросу)
rm -rf /home/bitrix/www/upload/resize_cache/*
# Удалить пустые директории в /upload/iblock
find /home/bitrix/www/upload/iblock/ -type d -empty -delete
# Ужать таблицу b_file
mysql -e "USE DBNAME; OPTIMIZE TABLE b_file;"
После этого таблица b_file_used больше не нужна:
DROP TABLE b_file_used;
Что НЕ покрывает инструкция
Сознательно оставлено за рамками — эти файлы при удалении сохранятся, потому что фильтр MODULE_ID='iblock' их не трогает. Но при необходимости их тоже можно почистить отдельно.
- Inline-картинки в HTML описаний инфоблоков (
PREVIEW_TEXT,DETAIL_TEXT). FILE_ID хранится в HTML как<img src="/upload/iblock/...">. Если в этих текстах есть картинки — нужен парсер HTML, который сматчит пути сb_file.SUBDIR + '/' + FILE_NAME. - Модуль landing (
b_landing_fileхранит явные привязки FILE_ID → блоки лендингов). - Модуль sender (HTML тел рассылок в
b_sender_message_param). - Модуль fileman (медиабиблиотека и картинки из визуального редактора).
- Файлы без записи в
b_file(физический мусор в/upload/iblock, до которого не дошли руки вb_file). Лечится отдельным скриптом, который сравнивает листинг директории сb_file. - Сериализованные настройки в
b_option(логотипы и т.п. — редко, но бывает).
Полезные ссылки и команды
# Размер /upload/iblock до и после
du -sh /home/bitrix/www/upload/iblock/
# Размер b_file до и после
mysql -e "USE DBNAME; SELECT
table_name,
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mb,
table_rows
FROM information_schema.tables
WHERE table_schema='DBNAME' AND table_name='b_file';"