# Загрузка изображений для обсуждения в чате

Страница: https://vm.ya-erm.ru/civilization/upload/. Ссылка «Загрузить картинку» в меню игры открывает её в отдельной вкладке.

2026-10-28 добавлен отдельный SVG favicon: золотистая картинка с горами и солнцем на тёмном фоне, отличающийся от листа с карандашом у заметок. Источник — `services/image-upload/favicon.svg`; сборщик копирует его в `dist/upload/` и подставляет хеш в URL. Проверены размеры 16/32/64 px, публичный ответ 200, MIME `image/svg+xml` и точные байты SVG; JS загрузчика не менялся. [Отчёт](data/service-favicons-verification.json).

По дополнению 2026-10-28 ссылка «Общие заметки» постоянно видна в верхней навигации загрузчика, ещё до выбора файлов; открывается в отдельной вкладке, сохраняя текущую загрузку. В заметках теперь можно загрузить картинку непосредственно из окна «Изображение» кнопкой «Обзор» или вставкой картинки из системного буфера в поле ссылки. Используются тот же каталог проекта, публичные URL и лимит 20 МиБ. Локальная и публичная проверка двух браузеров прошла; подробности — [общие заметки](shared-notes.md), [отчёт](data/browser-image-notes-verification.json).

Можно выбрать несколько файлов, перетащить их на страницу, вставить картинку сочетанием Ctrl+V / ⌘V или кнопкой «Вставить из буфера». По уточнению пользователя 2026-10-28 передача начинается автоматически сразу после добавления, без кнопки подтверждения. Новые картинки можно добавлять во время передачи и после её завершения: они дополняют очередь, предыдущие результаты остаются на странице. Если браузер запрещает чтение буфера кнопкой, страница предлагает обычную вставку клавишами.

По уточнению пользователя 2026-10-28 максимальная ширина контейнера увеличена с 760 до 960 px. Проверка Chromium на основном сайте подтвердила 960 px при ширине окна 1365 px и 354 px при окне 390 px без горизонтальной прокрутки.

У каждого изображения своя карточка: название, размер файла, превью, отдельные прогресс, ошибка, отмена и повтор. Непосредственно под картинкой после сохранения появляется **путь к этому файлу** и кнопки «Путь», «Ссылка», «Markdown». На компьютере поле и три кнопки стоят в одной строке; на телефоне поле занимает строку над тремя кнопками без горизонтальной прокрутки. Прежнее общее поле пути к файлу/папке заменено результатами отдельных картинок. Кнопка «Markdown всех картинок» собирает успешно загруженные файлы; «Очистить список» убирает карточки, сохраняя файлы на сервере.

Очередь последовательно отправляет файлы; добавление не блокируется, отмена или ошибка одного файла не останавливает остальные. Один выбор нескольких файлов создаёт общий датированный набор; последующий выбор, вставка или перетаскивание — новый набор. Индивидуальный повтор использует прежний набор и номер файла, проверяя сохранение перед отправкой. После успеха локальный Blob URL освобождается, превью использует публичную ссылку, а исходный File больше не удерживается. Список текущей вкладки не восстанавливается после перезагрузки страницы; сохранённые файлы остаются в проекте.

По дополнению пользователя 2026-10-28 у каждой картинки также есть **публичная ссылка** и кнопка «Копировать Markdown». Весь набор можно скопировать одним Markdown-блоком и вставить в общие заметки. На странице явно указано, что картинка доступна всем, у кого есть ссылка. По последующему замечанию пользователя дублирующий раздел «Уже загружали картинки?» удалён: старый путь к файлу или папке можно вставить непосредственно в окне «Изображение» общих заметок, повторно загружать файлы не требуется. Поддержка старых путей в API сохранена и проверена.

## Хранение

Картинки сохраняются без преобразования в **/root/civilization/uploads/images/**, за пределами публичной папки сайта. Каждый набор получает одну директорию с московскими датой и временем, например:

- Один файл: /root/civilization/uploads/images/2026-10-28_02-30-15-123_MSK/скриншот.png.
- Несколько: /root/civilization/uploads/images/2026-10-28_02-30-15-123_MSK/.

Миллисекунды и при необходимости счётчик __2 предотвращают совпадение имён папок без случайных названий. Имена файлов сохраняются в безопасном виде; в группе добавляется порядковый номер, чтобы одинаковые имена не перезаписывали картинки. Родительские пути и управляющие символы из исходного имени не используются. Папки имеют права 0700, файлы 0600.

Файлы находятся в проекте и переживают перезапуск контейнера и очистку /tmp. Автоматическое удаление не настроено. uploads/ исключён из Git и обычного копирования статического сайта; агент читает возвращённый путь напрямую. По явному запросу пользователя картинки, включая прежние загрузки, теперь публично доступны через `/civilization/images/<датированная-папка>/<имя-файла>`. Публичные ссылки содержат кодированные UTF-8 имена; все зарегистрированные форматы выдаются с соответствующим image MIME. Просмотр зависит от поддержки формата браузером: PNG/JPEG/WebP и другие поддерживаемые им форматы отображаются в заметках.

Маршрут выдаёт только обычные файлы изображений из датированных папок: нельзя получить каталог всех загрузок, `.sessions`, недописанные файлы, файлы через символьные ссылки или обойти родительский путь. Работают GET, HEAD, ETag/304 и часовой кеш. Установлены nosniff и CSP sandbox без скриптов, в том числе для прямого открытия SVG. Каталоги и файлы в проекте сохраняют прежние права доступа; Caddy проксирует выдачу через сервис, дополнительное публичное монтирование диска не требуется.

Лимит **20 × 1024 × 1024 = 20 971 520 байт на каждый файл** проверяется клиентом и сервером, включая реальное число полученных байтов. Поддерживаются сигнатуры PNG, JPEG, GIF, WebP, BMP, TIFF, ICO, AVIF, HEIC, SVG и PCX. Это определение формата, а не полная проверка декодирования; изображение с поддельной сигнатурой может сохраниться. Предпросмотр зависит от поддержки формата браузером. Большой список ограничен 64 КБ метаданных. При ошибке или отмене удаляется недописанный файл, завершённые изображения сохраняются. Неудачный набор может оставить пустую датированную папку для последующего повтора.

## Тайм-аут и восстановление результата

Пользователь сообщил о зависании до сообщения «Время загрузки истекло». На быстром соединении исходный сбой не воспроизвёлся: старый вариант принял 533 КБ и 5 МБ через Chromium/HTTP3. В старом коде был обнаружен общий предел 120 секунд и на клиенте, и на сервере; он мог прервать даже продолжающуюся передачу. На сервере также найден уже сохранённый пользовательский файл, поэтому отдельно обработана потеря ответа после сохранения. Эти наблюдения не устанавливают точную сетевую причину пользовательского случая.

Общий двухминутный предел убран. Передача прерывается после **90 секунд без прогресса**, а после отправки тела отводится 30 секунд на подтверждение сохранения. Через три секунды ожидания страница дополнительно запрашивает результат: если файл уже сохранён, показывает его путь независимо от потерянного POST-ответа. Сетевой сбой вызывает одну автоматическую попытку с тем же набором и номером файла. Перед повтором запрашивается состояние; завершённый файл повторно не передаётся.

Сервер хранит состояние наборов в uploads/images/.sessions/, сохраняет результат до отправки ответа и узнаёт его после перезапуска. Запрос уже завершённого файла возвращает тот же путь. Незавершённую передачу можно повторить в той же папке. Страница различает передачу байтов и ожидание сохранения, позволяет отменить запрос и разблокирует управление после ошибки.

## Сервис и публикация

scripts/image-upload-server.mjs — потоковый приёмник на Node.js без внешних зависимостей. На точном пути /civilization/api/image-upload используются:

- POST ?action=create — создание набора по списку {name, size}.
- POST ?batch=<непредсказуемый идентификатор>&file=<номер> — тело одного изображения.
- GET ?batch=<идентификатор> — состояние собственных файлов и их пути.
- GET ?path=<путь из загрузчика> — ссылки на указанный файл или изображения указанной папки.
- Прежний POST без параметров остаётся совместим с одиночными загрузками.

Сохранённые записи ответа теперь содержат `path`, `url` и `markdown`. Старые записи состояния получают ссылки при чтении; менять или повторно сохранять исходную картинку не требуется.

На сервере одновременно допускаются четыре запроса передачи. Чужой Origin, cross-site запросы, неизвестная длина и chunked-тело отклоняются. Запись одного набора выполняется последовательно. Журнал содержит статус, число принятых байтов, длительность и код ошибки без содержимого картинок или секретного идентификатора набора. Основания: [Node HTTP requestTimeout](https://nodejs.org/api/http.html#serverrequesttimeout), [Clipboard.read](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/read), [перетаскивание файлов](https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop).

deployment/image-upload.compose.yml запускает отдельный контейнер node:22-alpine в существующей сети Caddy. Порт на хост не публикуется. Контейнер работает от UID 65534, с файловой системой только для чтения, без capabilities; записывает только в смонтированный каталог uploads/images, внутри контейнера — /uploads. Отдельная настройка UPLOAD_PUBLIC_DIRECTORY возвращает настоящий путь проекта на хосте. Ограничены память, CPU и процессы; Docker перезапускает сервис.

Редактируемая страница перенесена в `services/image-upload/`; `python3 scripts/build-image-upload.py` генерирует `dist/upload/` и версию URL скрипта по хешу. Публикация: python3 scripts/deploy-image-upload.py с доступом к Docker и серверной копии сайта. Скрипт собирает страницу, сверяет активный Caddy с его файлом, сохраняет конфигурацию для отката, поднимает сервис и проверяет ответ. Копируются только файлы страницы загрузки и при необходимости ссылка в меню; добавлен маршрут `/civilization/images/*`. Остальные игровые файлы не публикуются. Уже загруженные картинки из /tmp копируются в датированные папки проекта с проверкой SHA-256; старые оригиналы не удаляются. Запись о переносе в reference-original/image-upload-deployment/legacy-imports.json предотвращает повторное копирование. Старая настройка tmpfiles удаляется, если её содержимое точно совпадает с созданным ранее. Общая публикация scripts/publish-game.py исключает независимые папки upload/notes.

## Проверки

node tests/image-upload.test.mjs проверяет точные байты и границу 20 МБ, отказ при превышении на байт, ошибочные запросы, очистку незавершённого файла, датированные группы, сохранение одинаковых имён, безопасные пути, повтор без дубликатов и получение результата после перезапуска. Отдельный сценарий подтверждает, что отсутствие прогресса закрывает запрос, а последовательная передача с прогрессом переживает несколько интервалов ожидания.

PLAYWRIGHT_BROWSERS_PATH=/tmp/civ3-browser-check/browsers node tests/browser-image-upload.cjs проверяет локальный HTTP-сервис. Для основного сайта задаётся CIV3_UPLOAD_URL=https://vm.ya-erm.ru/civilization/upload/. Проверяются автоматическая передача 5 МБ с точным совпадением на диске, три кнопки и строка пути под отдельным превью, добавление через выбор/перетаскивание/настоящий системный буфер во время намеренно задержанного запроса, сохранность прежних карточек, общий набор одного выбора и разные имена, потерянный ответ без дубликатов, предел 20 МБ, частичный успех, индивидуальный повтор, отмена без остановки последующих файлов, мобильная ширина и очистка списка без удаления файлов. Все созданные тестом картинки и записи состояния удаляются после проверки; пользовательские файлы не затрагиваются.

Обновление опубликовано. Все локальные и публичные проверки прошли без ошибок JavaScript: [browser-image-upload-verification.json](data/browser-image-upload-verification.json). Новый [публичный HTTP-отчёт](data/image-upload-http-verification.json) подтверждает приём ровно 20 МБ на файл, набор суммарно больше 20 МБ, отказ при превышении одного файла на байт, совпадение сохранённых байтов и состояние обоих завершённых файлов. Тестовые данные удалены.

Предыдущая пользовательская картинка перенесена с проверкой SHA-256 в /root/civilization/uploads/images/2026-10-28_02-15-47-411_MSK/image.png. Старый оригинал сохранён в /tmp. Первые локальные неудачи браузерного драйвера были связаны с подготовкой системного буфера для проверки запрета чтения и снятием сетевой заглушки после отмены; драйвер исправлен, все проверки повторены без ослабления условий.

Для публичных картинок прошли четыре сценария `tests/image-upload.test.mjs`, включая байты, HEAD/304, Unicode/скобки в именах, получение ссылок по прежнему пути и папке, изоляцию метаданных и запрет символьных ссылок. Локальный и публичный сквозной `tests/browser-image-notes.cjs` подтвердили загрузку → копирование ссылки/Markdown → просмотр обоими участниками → вставку прежнего файла/папки и публичного URL → сохранение заметок на диске. Отчёт: [browser-image-notes-verification.json](data/browser-image-notes-verification.json). Проверочные картинки и отдельный тестовый документ удалены; пользовательские файлы не менялись.

Автозагрузка и карточки 2026-10-28 опубликованы; обновлённые `browser-image-upload.cjs` и `browser-image-notes.cjs` прошли локально и на основном HTTPS-сайте без ошибок страницы. Подтверждены сохранность старых результатов при добавлении новых, копирование во время активной передачи, отдельные повтор/отмена, продолжение очереди после отмены, строка кнопок под превью, реальные байты PNG и системного буфера, совместный Markdown и мобильная ширина 390 px. Первый локальный прогон обнаружил замыкание обработчика превью на изменяемую переменную очереди: исправлено на отдельную константу каждой итерации до публикации, после чего оба сценария прошли. Изменились только HTML/JS загрузчика; API, сервисы и пользовательские заметки не менялись. Публичные файлы сверены с `dist/upload/`; тестовые наборы удалены.
