# Матрица файловых выгрузок источников ## Пользовательский контракт Frontend отправляет администраторский `POST /api/v2/organization-source-records/export-ticket/` с массивом `sources` и выбранным `format`. В ответ он получает короткоживущий одноразовый ticket и передаёт его обычной HTML-формой в `POST /api/v2/organization-source-records/export-download/`. Поэтому браузер сохраняет потоковый ZIP напрямую на диск, не удерживая весь архив как Blob в JavaScript. Ticket передаётся в теле формы, не попадает в URL и после первого запроса становится недействительным. Имя скачиваемого архива формируется по выбранным источникам и времени создания запроса: `_YYYYMMDD_HHMMSS.zip`, а для нескольких источников их имена соединяются через `__`. Это же имя возвращается в `file_name` при выдаче ticket и затем используется в `Content-Disposition`. Совместимый администраторский `POST /api/v2/organization-source-records/export/` по-прежнему сразу возвращает тот же ZIP API-клиентам. Крупный XLSX может состоять из нескольких файлов `*-part-001.xlsx`, `*-part-002.xlsx` и далее. При скачивании endpoint не читает таблицы записей источников и не строит CSV, XLSX или JSON заново. Он упаковывает файлы последнего полностью опубликованного ночного поколения и сразу потоково отправляет ZIP без временной копии всего архива. Поэтому `Content-Length` у ответа отсутствует. Если ни одного поколения ещё нет, API отвечает `503` с кодом `source_export_not_ready`. Каждое поколение содержит только текущий календарный год в timezone сервиса. Для записей с предметной датой год определяется по ней, для записей без такой даты — по `created_at`, для финансовых отчётов — по году `financial_lines`. Вложенные финансовые строки других лет исключаются. После смены года поколение прошлого года не раздаётся: до первой успешной сборки нового года API отвечает `503 source_export_not_ready`. ## Матрица | Группа API | Файл | CSV | XLSX | JSON | |---|---|:---:|:---:|:---:| | `financial_indicators` | `financial-indicators` | — | — | да | | `government_procurements` | `public-procurements` | да | да | да | | `industrial_production` | `manufacturers-and-products` | да | да | да | | `planned_inspections` | `planned-inspections` | да | да | да | | `bankruptcy` | `bankruptcy-procedures` | да | да | да | | `defense_suppliers` | `defense-unreliable-suppliers` | да | да | да | | `arbitration` | `arbitration-cases` | да | да | да | | `security_registries` | `information-security-registries` | да | да | да | | `vacancies` | `labor-vacancies` | да | да | да | Итого формируется 25 логических артефактов: один JSON для финансовых показателей и по три формата для остальных восьми групп. Физических файлов может быть больше из-за разбиения крупных XLSX. Если финансовые показатели выбраны вместе с другим форматом, в ZIP для них всё равно включается JSON. Все форматы начинают строку организации с полей `Наименование`, `ИНН`, `ОГРН`, `КПП`, `ОКПО`, после которых следуют поля исходной записи и развёрнутого `payload`. Все записи текущего года включаются в публичные файлы, а техническое наименование внешнего поставщика нейтрализуется. Исходные значения в БД сохраняются для работы интеграции и дедупликации. ## Ночная генерация Celery Beat запускает `organizations.tasks.refresh_source_record_export_artifacts` ежедневно в `05:30 Europe/Moscow`, после ежедневного обновления organization sources в `04:30`. Генератор: 1. читает каждую группу из БД один раз без глобальной сортировки миллионов строк; выборка текущего года использует функциональный индекс по году строковой `record_date`, а записи без предметной даты — индекс `created_at`; 2. пишет compact JSON-массив на диск и использует его как готовый JSON без второй копии; 3. потоково создаёт CSV и XLSX без накопления всех строк в памяти; 4. разбивает XLSX по умолчанию по 100 000 строк на отдельные файлы, ограничивая временный XML и не превышая лимит Excel; 5. записывает размеры файлов, номера частей и календарный `export_year` в manifest; 6. атомарно переключает `current.json` только после готовности всей матрицы; 7. сохраняет текущее и предыдущее поколения по умолчанию. При ошибке незавершённое поколение удаляется, а download endpoint продолжает отдавать предыдущую успешную версию. ### Расчёт диска Атомарная публикация требует одновременно хранить уже опубликованные поколения и одно новое поколение в staging. Минимальный запас под артефакты рассчитывается как `(SOURCE_RECORD_EXPORT_GENERATIONS_TO_KEEP + 1) * размер поколения`, плюс рабочий запас файловой системы. Исторический снимок dev от 2026-08-03 до ограничения по году занимал 16,55 ГБ (54 физических файла); актуальный размер годового поколения нужно брать из `total_size` результата команды сборки. Временная копия целого ZIP при скачивании не создаётся. На локальном снимке от 2026-08-04 поколение за 2026 год содержит 122 582 записи и занимает 809 768 139 байт (772,3 MiB) для всей матрицы из 25 файлов. Архив со всеми источниками оценивается в 410,1 MiB для JSON, 289,9 MiB для CSV и 72,3 MiB для XLSX; ZIP использует `ZIP_STORED`, поэтому к сумме файлов добавляется только небольшой служебный overhead. ## Хранение и первый запуск Каталог задаётся через `SOURCE_RECORD_EXPORT_DIRECTORY`, по умолчанию — `media/source-record-exports`. Он должен быть общим read-write volume для web и Celery worker. В Docker Compose используется `./media:/app/media`. После первого развёртывания готовое поколение можно создать сразу, не ожидая ночного расписания: ```bash PYTHONPATH=src uv run python src/manage.py build_source_record_exports ``` Доступные настройки: | Настройка | Значение по умолчанию | Назначение | |---|---:|---| | `SOURCE_RECORD_EXPORT_DIRECTORY` | `media/source-record-exports` | Общий каталог артефактов | | `SOURCE_RECORD_EXPORT_GENERATIONS_TO_KEEP` | `2` | Число сохраняемых успешных поколений | | `SOURCE_RECORD_EXPORT_LOCK_TTL_SECONDS` | `21600` | TTL распределённой блокировки Celery | | `SOURCE_RECORD_EXPORT_XLSX_ROWS_PER_FILE` | `100000` | Максимум строк данных в одной XLSX-части | | `SOURCE_RECORD_EXPORT_DOWNLOAD_TICKET_TTL_SECONDS` | `300` | Срок действия одноразового browser-download ticket |