Skip to content

ТЗ — Резервное копирование (cms/backup)

Слой: 🔵 инфра-модуль (обязательный в managed-парке) · Зрелость доноров: ★★ · Донор: — (spatie/laravel-backup + инфра студии) Статус: ТЗ к разработке

Назначение и возможности

Резервное копирование БД и файлов по расписанию, с принудительным снятием бэкапа перед cms:upgrade. Ротация, выгрузка в S3, проверка восстановимости и алерты при сбое.

  • Плановые бэкапы БД + файлов по cron-расписанию
  • Принудительный бэкап перед стартом cms:upgrade (гейт наравне с cms/health)
  • Ретеншн-сетка GFS (дневные/недельные/месячные копии с разным сроком хранения), а не только плоское «N последних копий»
  • Выгрузка в S3-совместимое хранилище вне сервера приложения (используется cms/storage-s3)
  • Шифрование дампа at-rest перед выгрузкой (клиентское шифрование, ключ — только в .env)
  • Тест-рестор во временную БД по собственному расписанию для проверки восстановимости
  • Алерт при сбое бэкапа или провале тест-рестора
  • Ручной бэкап «по кнопке» из Filament перед рискованной операцией

Зависимости и выключение

requires: ядро · suggests: cms/storage-s3

Поведение при выключении: плановые бэкапы не выполняются, cms:upgrade пропускает гейт принудительного бэкапа с явным предупреждением в лог — риск потери данных при сбое апгрейда возрастает, но сам апгрейд не блокируется техническим сбоем модуля.

При отсутствии cms/storage-s3 (suggests не установлен) бэкапы уходят на disk: local с явным предупреждением в лог: локальный диск сервера приложения не эквивалентен внешнему хранилищу по отказоустойчивости — один и тот же физический сбой способен уничтожить и сайт, и его резервные копии. Это деградация, не блокировка: модуль обязан отработать без cms/storage-s3, но обязан сделать риск видимым (health-чек degraded, не ok).

Модель данных

ТаблицаКлючевые поляПримечание
cms_backup_logid, type, disk, size_bytes, status, started_at, finished_at, errorжурнал плановых и ручных бэкапов
cms_backup_restore_testsid, backup_log_id, status, checked_atрезультаты тест-рестора

Заметки: status в обеих таблицах — PHP Enum; FK cms_backup_restore_tests.backup_log_idconstrained() + index(); cms_backup_log — журнальная таблица, кандидат на BRIN по started_at при долгом хранении истории. Модуль работает на уровне инсталляции целиком (вся БД + файлы сайта), а не по измерениям site_id/locale/city_id — при cms/multisite с общей БД бэкап один на все сайты; раздельные бэкапы по сайту вне рамок этого ТЗ (см. «Крайние случаи»).

Матрица «что бэкапится» (обязательный пункт ТЗ, матрица v2.2):

ДанныеВходит в бэкапВосстанавливаетсяПримечание
БД (все таблицы всех модулей)✅ (тип db/full)сразу, штатным pg_restore/аналогвключает настройки (settings-store живёт в БД) и журналы других модулей
Медиафайлы✅ (тип files/full)сразу, копированием на диск/в cms/storage-s3иммутабельны по content-hash, безопасно копируются отдельным проходом от БД
Настройки (SettingsStore)✅ (часть дампа БД)сразу вместе с БДотдельного бэкапа не требует — таблица настроек как любая другая
.env (секреты, конфигурация окружения)✅ (тип full, отдельный файл вне дампа БД)сразу, копированием файла на серверобязателен по ранбуку ядра; шифруется наравне с дампом БД (encrypt_at_rest)
Поисковый индекс (cms/search)❌ не входитпересоздаётся cms:search:reindex после ресторавнешнее хранилище движка (Meilisearch/ES), не часть БД CMS — см. ТЗ поиска
Денормализованные агрегаты/кеш-таблицы модулей❌ не входит отдельно (следуют за БД)пересоздаются командой модуля-владельца при необходимостивладелец декларирует свою восстановительную команду в собственном ранбуке §15

Рестор без запуска восстановительных команд из правой колонки — включая cms:postupgrade и переиндексацию поиска — не считается завершённым, см. ранбук ядра (§15 стандарта).

Входные и выходные данные

Входы

ИсточникДанные/поляЧем валидируется
Filament, кнопка «Создать бэкап сейчас»type (db|files|full)whitelist-enum в FormRequest
API POST /api/v1/admin/backup/runtype, опционально disk (local|s3)FormRequest, оба поля — whitelist-enum
Cron (ScheduleRegistrar)триггер планового бэкапа, без пользовательских данныхнастройки backup.schedule_cron, backup.scheduled_type
cms:upgrade (внутренний вызов)запрос статуса гейта «бэкап свежий и валиден»без пользовательского ввода — вызов публичного сервиса модуля
cms/storage-s3 (ответ сервис-вызова)результат выгрузки: успех/ошибка, ключ объектадоверенный ответ модуля по suggests, не пользовательский вход

Выходы

ПотребительДанныеФормат
Filament, журнал бэкаповстроки cms_backup_log + cms_backup_restore_testsтаблица UI, keyset-пагинация
API GET /api/v1/admin/backup/logсписок записей журналаJSON {data, meta}
API GET /api/v1/admin/backup/{id}/downloadpresigned URL на файл бэкапаJSON {data: {url, expires_at}}
cms:upgrade (гейт)булев статус «бэкап свежий и валиден»возврат сервиса / cms:backup:gate --json
cms/healthстатус последнего бэкапа и последнего тест-рестораагрегат health-чека
Подписчики событийBackupCompleted/BackupFailed/BackupRestoreTestFailedpayload события (EventBus)
cms/storage-s3 / локальный дискфайл дампа БД и/или архив медиабинарный объект по ключу

Whitelist-принцип: всё, что не перечислено во входной таблице (произвольные пути на диске, чужие диск-адаптеры, версии дампа, размер/тип за пределами enum), отвергается на границе FormRequest — модуль не принимает свободный текст там, где ожидается перечисление.

Настройки (группа backup)

КлючТипДефолтaffectsPageCacheОписание
backup.enabledbooltrueнетВключить плановые бэкапы (обязателен в managed-парке)
backup.schedule_cronstring0 3 * * *нетРасписание плановых бэкапов
backup.scheduled_typestringfullнетТип планового бэкапа (db|files|full)
backup.retention_copiesint14нетОбщий потолок копий вне сетки ниже (защита от неограниченного роста при некорректной сетке)
backup.retention_daysint90нетАбсолютный потолок хранения — копии старше N дней удаляются вне зависимости от сетки
backup.retention_dailyint7нетРетеншн-сетка (GFS): сколько ежедневных копий хранить
backup.retention_weeklyint4нетРетеншн-сетка (GFS): сколько еженедельных копий хранить (последняя копия недели)
backup.retention_monthlyint12нетРетеншн-сетка (GFS): сколько ежемесячных копий хранить (последняя копия месяца)
backup.diskstrings3нетДиск назначения (local/s3) — s3 обязателен для соответствия принципу «вне сервера»
backup.max_duration_minutesint120нетТаймаут джобы бэкапа, после которого фиксируется сбой
backup.upgrade_gate_enabledbooltrueнетПринудительный бэкап перед cms:upgrade
backup.restore_test_enabledbooltrueнетKill-switch тест-рестора (матрица v2.2): выключает только рискованную операцию поднятия временной БД, не бэкап и не модуль целиком
backup.restore_test_cronstring0 5 * * 0нетСобственное расписание тест-рестора — реже, чем плановый бэкап (по умолчанию раз в неделю)
backup.encrypt_at_restbooltrueнетКлиентское шифрование дампа перед выгрузкой на диск/S3
backup.encryption_key_refstringнетСсылка на ключ шифрования в .env (сам ключ в настройках не хранится, §6 стандарта)

API

МетодПутьДоступНазначение
POST/api/v1/admin/backup/runadmin (backup.manage)Ручной запуск бэкапа
GET/api/v1/admin/backup/logadmin (backup.view)Журнал бэкапов и тест-рестора
GET/api/v1/admin/backup/{id}/downloadadmin (backup.manage)Presigned URL на скачивание файла бэкапа

Журнал — keyset-пагинация (не OFFSET). Скачивание не проксируется через приложение (не грузит сервер дампом в десятки ГБ) — выдаётся временная подписанная ссылка на объект в хранилище.

Компоненты

Filament: страница журнала бэкапов, кнопка «Создать бэкап сейчас», ссылка «Скачать» на завершённых записях. Команды: cms:backup:run --json, cms:backup:restore-test --json, cms:backup:gate --json (используется cms:upgrade), cms:backup:clone-to-staging --json (ревизия ядра №2, п. 6) — рестор свежего бэкапа в staging-окружение с обязательными трансформациями: подмена домена, robots noindex, включение kill-switch всех модулей с внешними эффектами (почта, вебхуки, пиксели, платежи — стандарт §6); без завершённых трансформаций команда падает, а не оставляет «полуживой» staging, способный рассылать письма. Демо-контент (матрица v2.2): не применимо — у модуля нет публичных блоков/виджетов, галерея/playground его не показывают; журнал наполняется собственными плановыми бэкапами сразу после установки.

События и обмен

СобытиеКогдаPayload
BackupCompletedбэкап успешно завершёнtype, disk, size_bytes, duration_ms
BackupFailedбэкап завершился ошибкойtype, error
BackupRestoreTestFailedтест-рестор не прошёлbackup_log_id, error

FilterBus и provides-контракты не используются.

Таблица взаимодействий

Сущность/модульКаналНаправлениеЧто происходит
cms/storage-s3сервис-вызов (по suggests)outВыгрузка файла бэкапа на S3-совместимое хранилище
cms:upgrade (ядро)прямой вызов публичного сервиса модуляinЗапрос гейта «бэкап свежий и валиден» перед стартом апгрейда
cms/healthсервис-вызов / health-чек ядраoutОтдаёт статус последнего бэкапа и тест-рестора в агрегат /api/v1/system/health
ScheduleRegistrar (ядро)прямой вызов при bootoutРегистрация cron-расписания планового бэкапа
Подписчики (cms/notifications-bus, если включён)шина событийoutBackupFailed/BackupRestoreTestFailed → алерт админам
БД приложения (все таблицы всех модулей)нет прямого канала — снимок средствами СУБДoutДамп читает данные не через сервисы модулей, а через штатный механизм консистентного снапшота СУБД (см. «Крайние случаи») — единственное осознанное исключение из правила «чужие таблицы только через сервисы владельца», так как задача бэкапа — снять физическую копию всей БД целиком

Фоновая работа

Именованная очередь backup для плановых и ручных бэкапов, тест-рестора во временную БД и выгрузки на S3; расписание планового бэкапа — backup.schedule_cron, расписание тест-рестора — отдельное backup.restore_test_cron (реже, чем сам бэкап — не каждый снимок гоняется через полный рестор во временную БД, это дороже по ресурсам), оба — через ScheduleRegistrar ядра, без прямого Schedule:: в boot. Все внешние вызовы (запись в S3, восстановление во временную БД) — только из очереди.

Запуск бэкапа защищён мьютексом на тип операции (WithoutOverlapping по ключу backup:{type}): повторный запуск того же типа, пока предыдущий выполняется, не ставит вторую джобу поверх первой — либо игнорируется с понятным ответом, либо становится в очередь после завершения текущей (см. «Крайние случаи»).

Ретеншн-сетка (GFS): ротация не «N последних копий» плоским списком, а три независимых счётчика — retention_daily/retention_weekly/retention_monthly. После каждого успешного бэкапа ротация помечает копию как daily-кандидат; при смене календарной недели/месяца последняя копия периода дополнительно помечается weekly/monthly и не удаляется, пока не выбывает по своему счётчику. retention_copies/retention_days остаются абсолютным потолком поверх сетки — защита от неверно сконфигурированной сетки, не основной механизм. Ротация — идемпотентная джоба после BackupCompleted, не отдельное расписание.

Мини-ранбук (§15 стандарта):

СимптомЧто проверитьКоманда
Плановый бэкап не запустилсястатус ScheduleRegistrar, свободное место на диске/S3cms:backup:run --json --type=full (ручной прогон), df -h
cms:upgrade заблокирован гейтомсвежесть последнего успешного бэкапа и тест-рестораcms:backup:gate --json
Тест-рестор стабильно падает (schema_mismatch)версия схемы дампа vs миграции целевой БДcms:backup:restore-test --json, сверка cms:map --json
Health-чек модуля красный, бэкапы формально успешныналичие свежего успешного тест-рестора (см. «Крайние случаи»)cms:backup:restore-test --json
Диск/S3 заполнен, дампы падаютретеншн-сетка отрабатывает штатно, retention_* не заниженыcms:backup:run --json (лог ошибки), проверка квоты S3

Производительность и кеш

Ожидаемые объёмы (managed-парк студии, десятки сайтов): БД проекта — от единиц МБ до нескольких ГБ, медиатека — от сотен МБ до десятков ГБ на активный проект. При retention_copies=14 и одном плановом бэкапе в сутки cms_backup_log растёт на ~14 строк на сайт в устойчивом состоянии (старые ротируются) — таблица журнала сама по себе не тяжёлая; тяжесть — в объёме файлов на диске/S3, не в БД.

Горячие пути и бюджет: список журнала в Filament/API — 1 keyset-запрос по индексу (status, started_at); health-чек — 1 индексированный запрос «последняя успешная запись» на сайт. Сам бэкап и тест-рестор выполняются вне HTTP-бюджета (только в очереди) — их стоимость измеряется не в количестве SQL-запросов, а в времени дампа, полосе сети до S3 и месте на диске; это отдельный бюджет, контролируемый backup.max_duration_minutes.

Индексы: составной (status, started_at) на cms_backup_log под фильтр «последние неудачные»/health- агрегат; BRIN по started_at вместо B-tree — журнал растёт монотонно по времени, BRIN даёт компактный индекс почти без обслуживания при кратно меньшем размере; FK-индекс backup_log_id на cms_backup_restore_tests — уже объявлен в модели данных.

Кеш: собственных тегов кеша модуль не объявляет — не участвует в page-cache и не инвалидирует чужие теги. Статус последнего бэкапа для health-агрегата не кешируется тегами (данные меняются редко и чтение уже дешёвое по индексу); специальный кеш-слой избыточен.

Безопасность

Границы входа: ручной запуск бэкапа и доступ к журналу — только под явными permissions, без формы пользовательского ввода (кроме выбора диска/типа из whitelist-enum). Креды S3-хранилища — только .env через cms/storage-s3, не в БД. Тест-рестор выполняется в изолированную временную БД, никогда не затрагивает рабочую — явная проверка имени целевой БД перед восстановлением (совпадение с боевым именем БД — фатальная ошибка, операция прерывается до первого DDL).

Специфичные векторы атаки:

  • Подмена диска/типа бэкапаdisk/type принимаются только как enum из FormRequest, не как произвольная строка пути: нельзя указать путь вне зарегистрированных дисков Laravel filesystem.
  • Публичный доступ к дампу — бакет S3 обязан быть приватным; скачивание только через presigned URL с ограниченным TTL (GET /backup/{id}/download), не постоянная публичная ссылка.
  • Утечка через error-поле журнала — сообщение об ошибке перед записью в cms_backup_log и выдачей через API очищается от абсолютных путей ФС, кредов подключения и стектрейсов; в журнал и в ответ API уходит человеко-читаемая причина, детали — только в лог-канал модуля (аудитория «разработчик», см. §12 стандарта).
  • Двойной клик / гонка ручного и планового запуска — без мьютекса на тип операции возможны два параллельных дампа, конкурирующих за ввод-вывод и потенциально дающих несогласованный второй файл; закрывается блокировкой очереди (см. «Фоновая работа»).
  • Данные с ПДн внутри дампа — сам дамп БД по определению содержит все ПДн проекта; хранение на диске/S3 обязано использовать шифрование at-rest средствами хранилища (SSE на S3), доступ — только под backup.manage/backup.view, не более широкий, чем у прод-БД.

ПДн-паспорт (матрица v2.2). Собственные таблицы модуля (cms_backup_log, cms_backup_restore_tests) ПДн не хранят — только метаданные операций. Сам файл дампа — по определению максимально широкий контейнер ПДн инсталляции (все таблицы всех модулей разом), поэтому: backup.encrypt_at_rest=true шифрует дамп до выгрузки ключом из backup.encryption_key_ref (.env, не БД); срок жизни копии ограничен ретеншн-сеткой (retention_daily/weekly/monthly + абсолютный потолок retention_days), после чего файл физически удаляется с диска/S3, не просто снимается с учёта в журнале. Модуль не регистрирует обработчик в PrivacyRegistry и не участвует в «выгрузить всё по субъекту»/«забыть по запросу» напрямую (152-ФЗ) — эти операции применяются к рабочей БД модулями-владельцами данных; удалённый субъект продолжит существовать в уже снятых копиях до истечения их ретеншна — это осознанный компромисс архивного хранения, а не пробел (задокументировано, не «забыто» негласно).

Матрица ролей:

РольПросмотр журналаРучной запуск/скачивание
studio
админ
менеджер
редактор

cms/backup — модуль обязательного managed-парка (§3 стандарта): его disable недоступен клиентским ролям вообще, только studio с подтверждением и записью в аудит — таблица выше описывает права внутри включённого модуля, не решение о его выключении.

Права: backup.view, backup.manage.

UX-требования

Админ. Пустое состояние журнала (бэкапов ещё не было) — подсказка «Бэкапов пока нет, нажмите «Создать бэкап сейчас», чтобы снять первую копию». Кнопка «Создать бэкап сейчас» блокируется/показывает статус «выполняется» пока джоба не завершится — предотвращает случайный повторный клик (гонка гасится и на сервере мьютексом, UI — первая линия защиты от лишней нагрузки). Массовых операций над записями журнала нет — ротация автоматическая по ретеншн-сетке (retention_daily/weekly/monthly + потолок retention_copies/retention_days), вручную бэкапы из UI не удаляются. Ошибки — на человеческом языке: «Не удалось создать бэкап: недостаточно места на диске» вместо голого исключения. Восстановление рабочей БД из бэкапа не выполняется через Filament — это осознанно вынесенная за UI DevOps-операция (см. «Крайние случаи»), обязательно завершаемая cms:postupgrade и переиндексацией по ранбуку ядра, чтобы исключить случайный клик поверх продакшена; единственная доступная в UI «рестор»-операция — безопасный тест-рестор во временную БД, необратимых операций с данными пользователя UI модуля не предлагает.

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

Крайние случаи и типовые баги

  • Двойной клик «Создать бэкап сейчас» / параллельный ручной и плановый запуск → мьютекс на backup:{type} в очереди: второй запуск отклоняется с понятным сообщением «бэкап уже выполняется» вместо параллельного дампа.
  • Бэкап во время активной записи в БД (консистентность снапшота) → дамп БД снимается механизмом СУБД, дающим консистентный снапшот на момент старта (pg_dump с транзакционным снапшотом / --single-transaction), а не файловым копированием «вслепую»; файловый бэкап медиатеки — отдельный проход, возможен небольшой временной разрыв между снятием БД и файлов — не критично, так как файлы медиатеки иммутабельны по content-hash и не переписываются задним числом.
  • Диск/S3 кончилось место посреди бэкапа → джоба падает с BackupFailed, частично записанный файл удаляется в finally (не остаётся битый файл, который выглядит как валидная копия), алерт уходит немедленно, ротация не запускается, пока нет ни одной валидной свежей копии.
  • Восстановление бэкапа на другой (более новой/старой) версии CMS → дамп содержит схему своей версии на момент снятия; рестор на другую версию не применяется молча поверх чужой схемы — тест-рестор ядра сверяет версию/хеш схемы дампа с текущими миграциями целевой БД и явно помечает несовместимость (status: schema_mismatch), требуя прогона миграций до принятия дампа как валидного.
  • Проверка восстановимости — обязательный принцип: ⚠️ бэкап без успешного рестор-теста не считается бэкапом. Health-чек модуля обязан деградировать до degraded, даже если сам дамп формально завершился успехом (BackupCompleted), если последний cms_backup_restore_tests для него не прошёл или устарел дольше окна, заданного backup.restore_test_enabled-циклом. В исходном ТЗ health-чек был описан как «отражает статус» без явного порога деградации — уточнение: отсутствие свежего успешного тест-рестора обязано понижать статус агрегата cms/health, а не оставаться незамеченным до реальной аварии.
  • Отсутствие suggests-модуля cms/storage-s3 → фактический диск — local вместо настроенного s3, с предупреждением в лог и понижением health-статуса модуля (см. «Зависимости и выключение»).
  • Выключение модуля посреди выполняющейся джобы → уже запущенная джоба доигрывается (воркер не проверяет состояние модуля на каждый тик), новые плановые и ручные запуски не создаются; cms:upgrade видит модуль выключенным и пропускает гейт с явным предупреждением.
  • Пустой бэкап (например, свежий сайт до первой значимой миграции/загрузки медиа) → не считается ошибкой, но помечается в журнале отдельным флагом (например, size_bytes ≈ 0 + заметка), чтобы не создавать ложное чувство защищённости у администратора.
  • Огромный бэкап (медиатека на десятки ГБ) → выгрузка потоковая/многочастная (multipart upload) в S3, таймаут джобы — по backup.max_duration_minutes для типа files/full, прогресс виден в Filament, а не тишина до самого завершения или падения.
  • Противоречивая комбинация настроек: backup.enabled=false при backup.upgrade_gate_enabled=true → гейт не блокирует апгрейд флагом настройки, а проверяет фактическое состояние (наличие свежего валидного бэкапа); при выключенном модуле бэкапов нет физически, поэтому апгрейд идёт по ветке «модуль выключен, предупреждение в лог», как и в общем случае выключения, а не зависает на недостижимом условии.
  • Multisite с общей БД → модуль не различает сайты внутри одного бэкапа (снимается вся БД инсталляции целиком); при необходимости пер-сайтовых копий это отдельная задача вне текущего ТЗ — явно зафиксировано как ограничение, не молчаливый пробел.
  • Потерян/недоступен backup.encryption_key_ref → уже зашифрованные копии физически невосстановимы (это следствие самого шифрования, не баг) — модуль обязан явно предупреждать в UI при смене ключа: «Старые копии останутся нечитаемыми без прежнего ключа»; ключ не хранится нигде, кроме .env, ротация ключа — ручная DevOps-операция вне UI модуля.
  • Тест-рестор реже, чем сам бэкап (restore_test_cron реже schedule_cron) → между снятием копии и подтверждением её восстановимости возможен разрыв в днях; health-чек ориентируется на «последний пройденный тест-рестор», не на «последний бэкап», поэтому окно временно повышенного риска видно администратору явно, а не скрыто зелёным статусом.

Донорский код

Донор: — (используется пакет spatie/laravel-backup поверх инфраструктуры студии; собственного кода-донора нет). Миграция legacy-данных (§16 стандарта, матрица v2.2): не применимо — модуль работает только с данными текущей инсталляции, донорских бэкапов для переноса нет.

Тесты и приёмка

  • [ ] Контрактный тест: cms:upgrade не стартует без успешного принудительного бэкапа при включённом гейте
  • [ ] Health-чек модуля отражает время последнего успешного бэкапа и статус тест-рестора
  • [ ] Health-чек деградирует до degraded при отсутствии свежего успешного тест-рестора, даже если последний бэкап формально успешен (принцип «бэкап без рестор-теста не считается бэкапом»)
  • [ ] При выключении модуля cms:upgrade продолжает работать с явным предупреждением в лог
  • [ ] Ретеншн-сетка держит заданное число дневных/недельных/месячных копий, лишние удаляются, не свежие
  • [ ] Копии старше backup.retention_days/сверх backup.retention_copies удаляются даже при живой сетке
  • [ ] Зашифрованный (encrypt_at_rest=true) дамп не читается без ключа encryption_key_ref; расшифровка тест-рестором подтверждена тестом
  • [ ] Тест-рестор идёт по собственному расписанию restore_test_cron, независимому от schedule_cron
  • [ ] Алерт уходит при сбое бэкапа или провале тест-рестора
  • [ ] Параллельный ручной запуск при уже выполняющемся бэкапе того же типа блокируется мьютексом, не создаёт двух одновременных дампов
  • [ ] Дамп БД использует консистентный транзакционный снапшот — тест на параллельную запись во время дампа
  • [ ] Сбой посреди выгрузки (недостаточно места) фиксирует BackupFailed и не оставляет частично записанный файл в хранилище
  • [ ] Восстановление на несовместимую версию схемы CMS явно помечается schema_mismatch, не применяется молча
  • [ ] Права backup.view/backup.manage разграничивают просмотр и ручной запуск/скачивание
  • [ ] Контрактный набор cms-testing пройден, пакет протестирован в testbench-изоляции
  • [ ] Feature-тест на каждый роут модуля; тестовая БД только backup_test, migrate:fresh запрещён

Внутренняя база знаний студии