## Дизайн ### 1. docker-compose.yml — весь каталог media Было: ```yaml volumes: - ../../media/media:/srv/publisher/media:ro ``` Стало: ```yaml volumes: # ВЕСЬ каталог медиа (не media/media): краулер с 2026-09-12 качает в # media/, старые лежали в media/media/. Обе раскладки # доступны в контейнере: /srv/publisher/media/ и # /srv/publisher/media/media/. - ../../media:/srv/publisher/media:ro ``` ### 2. main.py — резолвинг медиа-пути в контейнере В `publish()` для Telegram-каналов заменить прямую проверку на устойчивый поиск: Было: ```python if req.card.media and Path(req.card.media).exists(): media_msg = telegram.send_media(ch, req.card.media, caption=req.card.text[:1000]) res.media_message_id = media_msg msg = telegram.send_message(ch, req.card.text) res.message_id = msg ``` Стало — хелпер `_resolve_media(card.media)`: ```python def _resolve_media(media: str | None) -> str | None: """Находит реальный путь медиа-файла в контейнере (CWD /srv/publisher). Пробует варианты в порядке приоритета: 1. media как есть -> /srv/publisher/media/ (новая раскладка) 2. media/media/ -> старая раскладка (media/media/) Возвращает существующий путь или None (медиа молча пропускается). """ if not media: return None base = Path(media).name # card.media из веба — относительный 'media/' → резолвится от CWD /srv/publisher cands = [Path(media), Path("media") / base, Path("media/media") / base] for c in cands: try: if c.exists() and c.is_file(): return str(c) except OSError: continue return None ``` Использование: ```python media_path = _resolve_media(req.card.media) if media_path: media_msg = telegram.send_media(ch, media_path, caption=req.card.text[:1000]) res.media_message_id = media_msg msg = telegram.send_message(ch, req.card.text) res.message_id = msg else: msg = telegram.send_message(ch, req.card.text) res.message_id = msg ``` Для GoToSocial — тот же `_resolve_media` в `_publish_gotosocial`: ```python media_ids = [] media_path = _resolve_media(card.media) if media_path: media_ids.append(gotosocial.upload_media(media_path)) ``` ### 3. Что НЕ меняем - `publisher/card.py` — медиа-нормализация на стороне веба остаётся. - БД, `media_path` — без миграций. - `web/app.py`, `web/store.py` — без изменений. ## Порядок деплоя 1. Правки compose + main.py. 2. `docker compose -f services/publisher/docker-compose.yml up -d` (volume изменился — контейнер пересоздастся). 3. Проверка: `docker exec vesti-publisher python -c "import os; print(os.path.exists('media/linux_education_2772.jpg'))"` → True. 4. e2e: publish поста (текст+медиа) в канал → `media_message_id` задан, затем удалить тестовые сообщения (delete_message).