From e53cf809c43b1bc8c5e04242c3324b5aced5d3a6 Mon Sep 17 00:00:00 2001 From: k0te1ch Date: Sun, 14 Jun 2026 03:09:54 +0300 Subject: [PATCH] fix(ftp): upload postshow episodes into FTP_POSTSHOW_DIR The SFTP publisher read FTP_POSTSHOW_DIR but never used it, so every file landed in the home directory regardless of episode type. Aftershow episodes were meant to go into a dedicated subfolder. Route postshow uploads into FTP_POSTSHOW_DIR (creating it if missing) and keep main episodes at the root. --- app/publishers/FTP/main.py | 24 ++++++++++++++++++- tests/unit/publishers/ftp/test_ftp_handler.py | 21 ++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/app/publishers/FTP/main.py b/app/publishers/FTP/main.py index 0dd3e11..97a50f1 100644 --- a/app/publishers/FTP/main.py +++ b/app/publishers/FTP/main.py @@ -22,6 +22,23 @@ FTP_PASSWORD = config.FTP_PASSWORD FTP_POSTSHOW_DIR = config.FTP_POSTSHOW_DIR +# type_episode в state/UI — "aftershow", в именах файлов и enum — "postshow" +# (см. bot/utils/podcast_methods.py). Принимаем оба, чтобы маршрутизация не +# зависела от того, какой алиас доедет до publisher'а. +_POSTSHOW_ALIASES = frozenset({"aftershow", "postshow"}) + + +def _remote_path(file_name: str, type_episode: str | None) -> str: + """Куда класть файл на SFTP относительно домашнего каталога. + + Postshow-эпизоды уходят в отдельную подпапку FTP_POSTSHOW_DIR; основные — + в корень (home). Раньше всё валилось в корень, потому что FTP_POSTSHOW_DIR + читался, но не применялся — отсюда «aftershow лёг не в ту папку». + """ + if type_episode in _POSTSHOW_ALIASES: + return f"{FTP_POSTSHOW_DIR}/{file_name}" + return file_name + async def upload_to_ftp( path: str, @@ -53,7 +70,12 @@ async def upload_to_ftp( conn.start_sftp_client() as sftp, aiofiles.open(path, "rb") as f, ): - async with sftp.open(file_name, "wb") as remote_file: + remote_path = _remote_path(file_name, type_episode) + if remote_path != file_name: + # Подпапка может ещё не существовать на сервере — создаём идемпотентно. + await sftp.makedirs(FTP_POSTSHOW_DIR, exist_ok=True) + + async with sftp.open(remote_path, "wb") as remote_file: while True: data = await f.read(chunk_size) if not data: diff --git a/tests/unit/publishers/ftp/test_ftp_handler.py b/tests/unit/publishers/ftp/test_ftp_handler.py index 062a6f9..9595aca 100644 --- a/tests/unit/publishers/ftp/test_ftp_handler.py +++ b/tests/unit/publishers/ftp/test_ftp_handler.py @@ -47,6 +47,27 @@ async def test_invalid_payload_skipped(self, mock_producer): mock_producer.send.assert_not_called() +class TestRemotePath: + """Маршрутизация файла по подпапкам в зависимости от type_episode.""" + + def test_main_episode_goes_to_root(self): + from app.publishers.FTP.main import _remote_path + + assert _remote_path("0042_rz_13062026.mp3", "main") == "0042_rz_13062026.mp3" + + def test_none_type_goes_to_root(self): + from app.publishers.FTP.main import _remote_path + + assert _remote_path("0042_rz_13062026.mp3", None) == "0042_rz_13062026.mp3" + + @pytest.mark.parametrize("type_episode", ["aftershow", "postshow"]) + def test_postshow_goes_to_subdir(self, type_episode): + from app.publishers.FTP.main import FTP_POSTSHOW_DIR, _remote_path + + result = _remote_path("0042_postshow_13062026.mp3", type_episode) + assert result == f"{FTP_POSTSHOW_DIR}/0042_postshow_13062026.mp3" + + class TestUploadEventModel: def test_valid_event(self, sample_upload_event_dict): event = UploadEvent(**sample_upload_event_dict)