Файловый менеджер в admin-панели

До этого, чтобы посмотреть или поправить конфигурационный файл внутри контейнера, нужно было делать docker exec -it container_name bash и работать в терминале. Для одного файла — терпимо. Для нескольких, или когда нужно что-то скопировать между директориями, загрузить новый конфиг или посмотреть что лежит в папке с логами — это уже раздражает. Я сделал файловый менеджер прямо в admin-панели.
Браузер файловой системы

Интерфейс разделён на две панели. Левая — список файлов и папок в текущей директории, правая — содержимое выбранного файла. Сверху — панель инструментов с хлебными крошками и кнопками действий.
Хлебные крошки показывают текущий путь в виде кликабельных сегментов: root / app / config. Каждый сегмент — это кнопка, клик по которой сразу переходит на этот уровень. Последний сегмент не кликабелен — это текущее местоположение.
Список файлов отсортирован: сначала идут папки, затем файлы, внутри каждой группы — по алфавиту. Это поведение, к которому привыкаешь за годы работы с любым файловым менеджером — папки всегда наверху. У каждой строки три колонки: имя с иконкой, дата последнего изменения и размер. У папок размер не отображается — только прочерк, потому что для директории размер метаданных не несёт смысла. У файлов размер адаптируется: байты, KB или MB в зависимости от порядка числа.
Корнем файловой системы служит переменная окружения FILES_ROOT — по умолчанию /app. Именно здесь начинается файловый менеджер, никакой другой части файловой системы хоста он не видит. Все пути передаются относительными от этого корня — абсолютные пути на клиент никогда не уходят.
Безопасность: path traversal
Все операции с файлами на бэкенде проходят через единую валидацию пути. Входящий путь нормализуется: убираются .., лишние слеши и прочие артефакты. Затем строится абсолютный путь и проверяется, что он находится строго внутри FILES_ROOT. Если нет — запрос отклоняется с кодом 403.
Любая попытка выйти за пределы разрешённой директории через ../../etc/passwd, //root/ или процентное кодирование — не проходит. Проверка выполняется на каждом эндпоинте, в том числе для пути назначения при копировании и переносе.
Просмотр файлов
Клик по файлу в списке открывает его содержимое в правой панели. Два ограничения. Первое: если файл весит больше 5 МБ — показывается сообщение об ошибке. Текстовый редактор для полугигабайтного лога — плохая идея. Второе: бинарные файлы — .png, .zip, скомпилированные бинари — не открываются как текст; вместо этого отображается сообщение «Binary file — cannot display as text». Отображать байты как текст бессмысленно.
В шапке правой панели — имя файла, бейдж с расширением и три кнопки: «Скачать», «Редактировать», «Удалить». Кнопка «Редактировать» не отображается для графических файлов — редактировать изображение как текст не имеет смысла.
Просмотр изображений

Если выбранный файл — изображение (jpg, jpeg, png, gif, webp, svg, bmp, ico, tif, tiff, avif), в правой панели вместо текстового редактора показывается само изображение.
Загрузка изображения проходит с авторизацией — скачать файл напрямую через URL без токена нельзя. Изображение запрашивается как двоичный поток с нужным заголовком авторизации, после чего браузер показывает его внутри интерфейса. При переключении на другой файл или переходе в другую директорию ресурсы освобождаются.
Изображение показывается с сохранением пропорций — вписывается в доступное пространство правой панели.
Редактирование файлов

По кнопке «Редактировать» правая панель переходит в режим редактирования. Это переключение с визуальным сигналом: шапка меняет фоновый цвет, появляется бейдж «Редактирование» с иконкой карандаша, под шапкой добавляется тонкая цветная полоса — акцентный цвет темы. Кнопки меняются: вместо «Редактировать» появляются «Сохранить» и «Отменить».
«Отменить» не делает никакого запроса — просто восстанавливает последнее загруженное содержимое файла и закрывает режим редактирования. «Сохранить» записывает изменения на диск. Если сохранение прошло успешно, режим редактирования закрывается.
CodeMirror 6
Текстовый просмотр и редактирование реализованы через CodeMirror 6 — один из самых зрелых редакторов кода для браузера. В нём работает весь набор функций, к которому привыкаешь в десктопных IDE.
Редактор включает номера строк, гаттер для сворачивания блоков кода, историю изменений с отменой и повтором, подсветку парных скобок, автозакрытие скобок, автодополнение, подсветку активной строки, подсветку всех вхождений выделенного текста и перенос длинных строк. Поиск по файлу — стандартным сочетанием Ctrl+F. Tab вставляет отступ.
Маркеры сворачивания блоков
Маркеры в гаттере — SVG-шевроны вместо текстовых символов. Открытый блок — шеврон вниз, свёрнутый — шеврон вправо. Выглядит аккуратно и работает независимо от высоты строки.
Определение языка
Язык для подсветки синтаксиса определяется автоматически по расширению файла. Поддерживаются: JavaScript и TypeScript во всех вариантах (js, mjs, cjs, ts, jsx, tsx), CSS и препроцессоры (css, scss, less), разметка (html, vue, htm), форматы данных (json, jsonc, yaml, yml), документация (md), Python, SQL, XML и SVG. Файлы с неизвестным расширением открываются без подсветки — просто текст.
Операции с файлами
Копирование и вырезание

Кнопки «Копировать» и «Вырезать» доступны через контекстное меню правой кнопкой мыши. После выбора операции файл или папка попадает во внутренний буфер обмена.
Как только буфер не пуст, в панели инструментов появляется кнопка «Вставить» с указанием режима: «Вставить (копия)» или «Вставить (перемещение)». Кнопка цветная — выделяется на фоне остальных инструментов, чтобы напомнить, что буфер обмена активен.
При наведении на кнопку «Вставить» появляется тултип с деталями: режим операции, имя файла и полный путь. Если в буфере несколько объектов — имена перечислены через запятую.
Рядом с текстом кнопки — крестик для сброса буфера. Передумал — нажал крестик, буфер очищается, кнопка исчезает. Без этой кнопки единственный способ сбросить операцию — выполнить вставку куда попало.
Вставка

Клик «Вставить» копирует или переносит все объекты из буфера в текущую директорию. Копирование работает для файлов и папок с любой глубиной вложенности. Перенос сохраняет структуру, а при попытке перенести между разными файловыми системами — автоматически переключается на копирование с удалением оригинала.
После вставки буфер обмена обнуляется и директория перезагружается.
Переименование

Кнопка «Переименовать» открывает модальное окно с полем ввода, предзаполненным текущим именем файла или папки. Enter подтверждает, Escape закрывает. Кнопка подтверждения заблокирована, пока поле пустое. Работает одинаково и для файлов, и для папок.
Удаление
Перед удалением показывается модальное окно с подтверждением. Для одиночного файла — его имя и предупреждение о необратимости. Для папки добавляется предупреждение: «Папка и всё её содержимое будут удалены». При групповом удалении в окне отображается список всех выбранных элементов с иконками.
Никакого браузерного confirm() — диалог встроен в интерфейс. Это принципиально: браузерный диалог выглядит инородно и не позволяет показать детали операции. Папка удаляется рекурсивно со всем содержимым в один запрос.
Создание файлов и папок
Новый файл

Кнопка «Новый файл» в панели инструментов открывает модальное окно с полем ввода имени. Подсказка в поле — «например, index.js» — намекает, что расширение влияет на подсветку синтаксиса. Enter создаёт файл, Escape закрывает.
После создания файл сразу открывается в правой панели в режиме редактирования — ждать дополнительного клика не нужно. Не нужно сначала создавать файл, потом искать его в списке и только потом нажимать «Редактировать».
Новая папка

Кнопка «Новая папка» — аналогичное модальное окно с одним полем для имени. После создания директория перезагружается.
Скачивание файла
«Скачать» в шапке правой панели загружает текущий файл. Скачивание проходит с авторизацией — напрямую через URL без токена файл не скачать. Браузер сохраняет файл с правильным именем и не пытается его открыть во вкладке.
Загрузка файлов

Загрузка открывается в модальном окне по кнопке «Загрузить» в панели инструментов.
Слоты для файлов
По умолчанию в попапе пять строк — пять слотов для файлов. Каждый слот — кнопка «Выбрать файл...», которая открывает системный диалог выбора файла. После выбора строка показывает имя файла и его размер. Рядом с каждой строкой — кнопка удаления. Если файл передумали загружать — убираем его из списка, не закрывая попап.
Кнопка «+» добавляет ещё один слот. Нажимать можно сколько угодно раз. Это нужно, когда нужно загрузить больше пяти файлов из разных директорий, которые нельзя выбрать одним системным диалогом.
Drag and drop
В верхней части попапа — зона перетаскивания. Перетащить сюда можно как отдельные файлы, так и папку целиком. При наведении с файлом зона меняет стиль — рамка из пунктирной становится сплошной с акцентным цветом, фон чуть светлеет.
Если перетащить папку — она обходится рекурсивно: все вложенные файлы на всех уровнях вложенности добавляются в список загрузки с сохранением структуры поддиректорий. Папка с сотнями файлов и несколькими уровнями вложенности загрузится целиком и воспроизведётся на сервере в точно такой же структуре.
Загрузка
После подбора файлов — кнопка «Загрузить (N)», где N — количество файлов в очереди. Кнопка неактивна, если ни один файл не выбран, и пока идёт загрузка.
Файлы загружаются последовательно. Прогресс отображается полосой и счётчиком «X / N файлов загружено». После завершения окно закрывается, директория перезагружается. Лимит — 100 МБ на файл.
Мульти-селект и групповые операции

Выделение
Обычный клик по файлу или папке открывает его содержимое в правой панели — стандартное поведение. Но можно выделять несколько элементов.
Ctrl+Click (или Cmd+Click на Mac) добавляет элемент к выделению или убирает его, если он уже выделен. Shift+Click выделяет диапазон от последнего кликнутого элемента до текущего — как в любом нормальном файловом менеджере. Каждый элемент в режиме выделения показывает чекбокс слева от имени; в обычном режиме чекбоксы скрыты, но появляются при наведении.
Action bar
Когда выделен хотя бы один элемент, под панелью инструментов появляется action bar. В нём три кнопки: «Копировать», «Вырезать», «Удалить». «Снять выделение» — справа.
«Копировать» и «Вырезать» кладут все выделенные объекты в буфер обмена — с тем же механизмом тултипа, что и для одиночных файлов. Если в буфере несколько элементов, тултип показывает количество: «5 элементов» вместо одного имени.
«Удалить» открывает модальное окно со списком всех выделенных элементов — чтобы было видно, что именно будет удалено. Удаление идёт последовательно для каждого элемента.
Контекстное меню
Правая кнопка мыши на любом файле или папке открывает контекстное меню с четырьмя пунктами: «Копировать», «Вырезать», «Переименовать», «Удалить». Клик в любом другом месте закрывает меню. Меню открывается точно под курсором.