# Плагин «Очистка неиспользуемых файлов»

Плагин NGCMS для поиска файлов в `uploads`, которые не используются сайтом, и их безопасного ручного удаления.

Плагин работает в режиме предварительной проверки:

- сканирует только выбранные каталоги;
- собирает ссылки на файлы из контента и таблиц базы данных;
- защищает новые файлы и явно исключённые пути;
- показывает результат с фильтрами, сортировкой и пагинацией;
- удаляет файлы только после ручного подтверждения;
- повторно проверяет файл непосредственно перед удалением;
- ведёт отдельные журналы удаления и операций;
- может запускать фоновое сканирование через cron;
- не удаляет файлы автоматически.

## Требования

- NGCMS с поддержкой плагинов и `extra-config`;
- PHP 7.2 или новее, рекомендуется PHP 8.3;
- доступ к каталогу `uploads`;
- права веб-сервера на чтение файлов и удаление только тех файлов, которые администратор выбрал вручную;
- для источников базы данных должен быть доступен штатный объект `$mysql`.

Плагин не изменяет файлы ядра NGCMS. Его основные файлы находятся в:

```text
engine/plugins/filecleaner/
```

## Установка и запуск

1. Установите каталог плагина в `engine/plugins/filecleaner/`.
2. Включите плагин штатным менеджером плагинов NGCMS.
3. Откройте административную страницу:

```text
admin.php?mod=extra-config&plugin=filecleaner
```

4. На вкладке «Настройки» выберите каталоги для сканирования.
5. Сохраните настройки.
6. Нажмите «Сканировать».
7. После завершения проверьте статусы файлов.
8. Удаляйте только файлы со статусом «Не используется».

Перед первым удалением рекомендуется сделать резервную копию файлов и базы данных.

## Вкладка «Файлы»

### Статистика

В верхней части отображаются:

- общее количество найденных файлов;
- количество файлов со статусом «Используется»;
- количество файлов со статусом «Не используется»;
- общий размер потенциального мусора.

Статистика рассчитывается по последнему завершённому сканированию.

### Поиск и фильтры

Доступны следующие фильтры:

- поиск по пути файла;
- статус;
- тип файла;
- диапазон размера;
- сортировка по имени, размеру, дате изменения или возрасту;
- направление сортировки;
- количество строк на странице.

Количество файлов на странице:

- 30 по умолчанию;
- 50;
- 70;
- 100;
- 200.

Выбранное значение сохраняется в URL при переходе между страницами.

Для изображений доступен предпросмотр и ссылка «Открыть файл». Предпросмотр выводится только для файлов, которые физически существуют в `uploads`, поэтому старые записи сканирования не должны создавать 404-запросы.

### Удаление

Удаление бывает одиночным и массовым.

Перед отправкой формы:

- показывается подтверждение;
- для массового удаления отображается количество файлов и общий размер;
- кнопки блокируются и показывают индикатор загрузки;
- повторная отправка формы запрещается.

На сервере выполняется повторная проверка:

1. путь нормализуется;
2. файл должен находиться внутри физического каталога `uploads`;
3. запись должна присутствовать в последнем `scan.json`;
4. статус записи должен быть `unused`;
5. файл должен существовать;
6. файл не должен быть исключён настройками;
7. возраст файла должен соответствовать настройке защиты;
8. все источники использования вызываются повторно;
9. только после этого выполняется `unlink()`.

Если запись осталась в результате старого сканирования, а файла уже нет, запись удаляется из `scan.json` и выводится сообщение «Файл уже отсутствует».

Ошибки источников использования блокируют удаление, потому что при недоступной проверке нельзя безопасно считать файл неиспользуемым.

## Статусы файлов

### «Используется» (`used`)

Файл найден в активном содержимом или источнике, который сообщает фактическое использование. Такой файл не удаляется.

### «Файл зарегистрирован» (`registered`)

Файл зарегистрирован в файловых таблицах или связан с объектом, но источник не подтвердил прямое использование. Такой файл также не удаляется автоматически и не предлагается к удалению.

### «Защищён» (`protected`)

Файл исключён настройками или является новым относительно периода защиты.

### «Недостаточно данных» (`insufficient`)

Один или несколько источников завершились ошибкой. Такой статус блокирует удаление, чтобы ошибка базы данных или плагина не привела к потере файла.

### «Не используется» (`unused`)

Файл найден в выбранном каталоге, не найден источниками использования, не исключён и старше периода защиты. Только такие файлы могут быть предложены для ручного удаления.

Приоритет проверки статусов:

```text
used > registered > protected > insufficient > unused
```

## Вкладка «Настройки»

### Папки для сканирования

Выберите один или несколько каталогов внутри `uploads`. Доступны физически существующие каталоги, включая корень `uploads`.

Если каталог не выбран, сканирование не запускается.

Сканер:

- не выходит за пределы `uploads`;
- пропускает символические ссылки;
- обрабатывает только обычные файлы;
- не включает исключённые пути.

### Защита новых файлов

Настройка «Не удалять файлы младше» задаётся в днях. Доступные значения:

```text
0, 1, 3, 7, 14, 30, 60, 90
```

Значение по умолчанию: `30` дней.

Возраст рассчитывается по времени изменения файла. При значении `0` защита по возрасту отключается, но остальные проверки безопасности остаются.

### Исключённые директории

Укажите относительные пути внутри `uploads`, по одному на строку. Например:

```text
avatars/custom
images/cache
```

Все файлы внутри указанных каталогов получают защиту и не могут быть удалены.

### Исключённые маски

Доступны стандартные маски:

```text
*.ico
*.svg
*.tmp
*.bak
*.log
```

Можно добавить собственные маски, например:

```text
*.cache
preview-*.jpg
```

Маски сравниваются с именем файла, без учёта регистра.

### Постоянные исключения

Независимо от настроек администратора всегда защищаются:

```text
avatars/noavatar.gif
avatars/noavatar.png
```

## Источники использования

Плагин анализирует следующие источники.

### Новости

Из всех строковых полей таблицы `*_news` извлекаются пути из HTML и URL-атрибутов:

- `src`;
- `href`;
- `data-src`;
- `data-original`;
- `poster`;
- абсолютные и относительные URL, ведущие в `uploads`.

### Зарегистрированные файлы

Проверяются таблицы:

```text
<prefix>_files
<prefix>_images
```

Учитываются каталоги `files`, `images` и `dsn` в зависимости от значения `storage`.

### Изображения категорий NGCMS

Связи категорий с таблицей `*_images` проверяются через `image_id`.

### Аватары пользователей

Читается поле `avatar` таблицы:

```text
<prefix>_users
```

Поддерживаются оба формата:

```text
user.jpg
uploads/avatars/user.jpg
```

В первом случае путь нормализуется как `avatars/user.jpg`, во втором используется указанный путь после удаления префикса `uploads/`.

### XFields

Проверяются записи с:

```text
plugin = "xfields"
```

Отдельно учитываются связанные изображения и файлы xfields с положительными `linked_ds` и `linked_id`.

### Аттачи новостей

Проверяются изображения и файлы с:

```text
linked_ds = 1
linked_id > 0
```

### Gallery

Источник запускается только если плагин `gallery` активен и нужные таблицы существуют. Изображения активных галерей сопоставляются по:

```text
i.folder = g.name
```

### EShop

Источники запускаются только если плагин `eshop` активен.

Изображения товаров читаются из:

```text
<prefix>_eshop_images
```

Физические пути:

```text
uploads/eshop/products/<product_id>/<filepath>
uploads/eshop/products/<product_id>/thumb/<filepath>
```

Изображения категорий читаются из:

```text
<prefix>_eshop_categories.image
```

Физические пути:

```text
uploads/eshop/categories/<image>
uploads/eshop/categories/thumb/<image>
```

Временные каталоги импорта `temp` не считаются постоянным использованием.

Если `gallery` или `eshop` выключен, плагин очистки не выполняет для него даже запрос проверки таблиц. Если активный плагин установлен неполностью и ожидаемой таблицы нет, источник пропускается.

## Сканирование

### Полное сканирование из админки

Кнопка «Сканировать» выполняет полный проход выбранных каталогов и сохраняет результат в:

```text
engine/plugins/filecleaner/data/scan.json
```

В `scan.json` сохраняются:

- время сканирования;
- путь;
- размер;
- время изменения;
- статус;
- признак регистрации источником;
- список ошибок источников.

### Cursor-сканирование и cron

Cron обрабатывает файлы порциями по 500 файлов через:

```php
filecleaner_scan_step(500)
```

Промежуточное состояние хранится в:

```text
engine/plugins/filecleaner/data/scan-progress.json
```

После завершения формируется обычный `scan.json`.

Cron выполняет только сканирование. Автоматического удаления файлов нет.

### Настройки cron

Доступны:

- включение и выключение;
- час от `0` до `23`;
- минута `0`, `15`, `30` или `45`.

Значения по умолчанию:

```text
Включено: да
Час: 3
Минута: 15
```

Задача регистрируется как:

```text
plugin: filecleaner
handler: scan
```

## Журналы

### Журнал удаления

Файл:

```text
engine/plugins/filecleaner/data/deletions.log
```

В журнал записываются дата, путь и размер удалённого файла.

На вкладке журнала:

- выводится 30 записей на страницу;
- есть отдельная пагинация;
- записи старше 30 дней автоматически удаляются при чтении журнала;
- доступна ручная очистка журнала удаления.

### Журнал операций

Файл:

```text
engine/plugins/filecleaner/data/operations.log
```

Записываются операции сканирования, прогресс cron, удаление файлов и обработка отсутствующих файлов.

На отдельной вкладке:

- выводится 30 записей на страницу;
- есть отдельная пагинация;
- записи старше 30 дней автоматически удаляются при чтении журнала;
- доступна отдельная ручная очистка журнала операций.

### Формат операций

Каждая строка журнала операций является JSON-объектом:

```json
{
  "date": "2026-09-17T03:15:00+03:00",
  "action": "scan_step",
  "status": "completed",
  "details": {
    "files": 500,
    "source_errors": []
  }
}
```

## Файлы данных плагина

```text
engine/plugins/filecleaner/data/scan.json
engine/plugins/filecleaner/data/scan-progress.json
engine/plugins/filecleaner/data/deletions.log
engine/plugins/filecleaner/data/operations.log
```

Не редактируйте эти файлы во время активного сканирования или удаления. Для очистки журналов используйте кнопки в админке.

## Диагностика

### Файл отображается как «Не используется», но удалить его нельзя

Проверьте:

1. не устарел ли `scan.json`;
2. существует ли файл физически в `uploads`;
3. не изменился ли путь после последнего сканирования;
4. не активировался ли после сканирования источник использования;
5. не изменились ли настройки защиты и исключений.

Запустите новое полное сканирование и повторите проверку.

### Сообщение «Файл не прошёл повторную проверку безопасности»

Это означает, что не выполнено одно из условий:

- файл отсутствует в последнем результате сканирования;
- статус не равен `unused`;
- путь нельзя безопасно сопоставить с `uploads`;
- запись или файл уже были изменены.

Это защитное поведение, а не разрешение на принудительное удаление.

### Сообщение «Файл зарегистрирован или используется»

После сканирования файл был найден одним из источников. Удалять его вручную через обход проверки не следует.

### Статус «Недостаточно данных»

Один из SQL-источников или источников контента завершился ошибкой. Сначала проверьте журнал операций и наличие таблиц/плагинов, затем повторите сканирование.

### Ошибки 404 для предпросмотра

Старые версии результата могли содержать уже удалённые файлы. Текущая админка не выводит записи, для которых физический файл отсутствует. Выполните новое сканирование для обновления списка.

### Источник gallery или eshop выдаёт ошибку отсутствующей таблицы

Убедитесь, что соответствующий плагин активен и установлен. Неактивные `gallery` и `eshop` не должны проверяться. Для активного, но неполного плагина источник пропускается, если таблица отсутствует.

## Ограничения

- Плагин не восстанавливает удалённые файлы.
- Плагин не удаляет автоматически файлы cron-задачей.
- Ссылки, сформированные динамически или сохранённые нестандартным способом, могут потребовать дополнительного source callback.
- Если сторонний плагин хранит файлы вне известных таблиц и не выводит их в контенте, он может потребовать отдельного источника.
- Статус `registered` означает регистрацию в базе, а не доказанное отображение на странице.
- Проверка выполняется только в выбранных каталогах.
- Права ОС могут запретить удаление даже после успешной логической проверки.

## Расширение источниками

Источники регистрируются в `filecleaner.php` через:

```php
filecleaner_register_source('source_name', static function (): array {
    return [
        [
            'path' => 'images/example/file.jpg',
            'status' => 'used',
            'source' => 'custom_plugin',
        ],
    ];
});
```

Допустимы также старые источники, возвращающие массив строковых путей:

```php
return ['images/example/file.jpg'];
```

Для записей используются статусы:

```text
used
registered
```

Источник должен возвращать нормализуемые пути относительно `uploads`. При ошибке источник должен выбрасывать исключение: плагин пометит потенциально удаляемые файлы как `insufficient` и запретит опасное удаление.

## Безопасная рабочая процедура

1. Сделайте резервную копию базы данных и `uploads`.
2. Выберите только нужные каталоги.
3. Настройте защиту новых файлов минимум на 30 дней.
4. Добавьте каталоги кеша и системные файлы в исключения.
5. Выполните сканирование.
6. Проверьте список со статусом «Не используется».
7. Откройте несколько файлов и проверьте, что они действительно не нужны.
8. Удаляйте небольшими партиями.
9. После удаления выполните новое сканирование.
10. Проверьте журнал удаления и журнал операций.

Никогда не удаляйте файлы со статусами «Используется», «Файл зарегистрирован», «Защищён» или «Недостаточно данных» вручную через файловую систему без понимания их назначения.

## Удаление новости в админке NGCMS

При удалении новости штатной админкой NGCMS ядро отдельно обрабатывает связанные записи:

```text
<prefix>_files   — файлы новости
<prefix>_images  — изображения новости
```

Обрабатываются записи с:

```text
storage = 1
linked_ds = 1
linked_id = ID новости
```

Для каждой записи выполняется проверка перед удалением физического файла:

1. ищется такой же `folder/name` в других записях файлового менеджера;
2. для файлов в `dsn` проверяются обе таблицы: `*_files` и `*_images`;
3. все поля других новостей проверяются на ссылку на тот же относительный путь;
4. если файл больше нигде не используется, удаляются оригинал, thumbnail и текущая запись из базы;
5. если файл используется другой новостью или другой записью файлового менеджера, удаляется только связь удаляемой новости, а физический файл сохраняется.

### Дополнительные поля XFields

Изображения XFields для новостей копируются в `uploads/dsn` и сохраняются в `*_images` с `plugin = 'xfields'`, `linked_ds = 1`, `linked_id = ID новости` и `storage = 1`.

Поэтому при удалении новости они проходят тот же штатный цикл ядра, что и обычные изображения новости. Таблица связей XFields очищается фильтром плагина XFields, а запись изображения и физические файлы обрабатываются файловым менеджером.

### Важное ограничение

Проверка ссылок в других новостях специально консервативна: при сомнительном совпадении физический файл сохраняется. Это безопаснее, чем удалить файл, который ещё используется. После удаления новости рекомендуется проверить журнал операций и выполнить сканирование плагином очистки.
