diff --git a/bot.py b/bot.py index 3156027..e39ee36 100644 --- a/bot.py +++ b/bot.py @@ -1,7 +1,6 @@ import asyncio import logging import os -import shutil import zipfile from pathlib import Path from tempfile import TemporaryDirectory @@ -14,11 +13,15 @@ from aiogram.fsm.storage.memory import MemoryStorage from aiogram.client.bot import DefaultBotProperties # Импорт наших конвертеров -from md_to_docx import MarkdownToDocxConverter, DocumentSettings +from md2gost import MarkdownToDocxConverter, DocumentSettings from rep_to_txt import generate_complete_project_structure # Конфигурация -BOT_TOKEN = "**************************" # @my_convbot +BOT_TOKEN = os.getenv("BOT_TOKEN", "XXXXXXXXXXXXXXXXXXXXXXXX") # @my_convbot + +MAX_FILE_SIZE = 20 * 1024 * 1024 # больше Telegram-боту не скачать +MAX_UNPACKED_SIZE = 200 * 1024 * 1024 # защита от zip-бомбы +MAX_FILES_IN_ARCHIVE = 5000 # Инициализация бота bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML)) @@ -60,15 +63,19 @@ async def help_handler(msg: Message) -> None: async def handle_document(msg: Message) -> None: """Обработка загруженных документов""" document = msg.document - file_name = document.file_name - file_size = document.file_size - - if file_size > 20 * 1024 * 1024: + # у документа может не быть имени; путь берём только из имени файла, без каталогов + file_name = Path(document.file_name or "file").name + file_size = document.file_size or 0 + + if file_size > MAX_FILE_SIZE: await msg.answer("❌ Файл слишком большой! Максимум 20 МБ") return - + file_ext = Path(file_name).suffix.lower() - + if file_ext not in (".md", ".zip"): + await msg.answer("❌ Неподдерживаемый формат файла! Пришлите .md или .zip") + return + status_msg = await msg.answer("⏳ Обрабатываю файл...") try: @@ -78,19 +85,15 @@ async def handle_document(msg: Message) -> None: input_path = os.path.join(temp_dir, file_name) await bot.download_file(file_info.file_path, input_path) + # конвертация — в отдельном потоке, чтобы бот не замирал для остальных if file_ext == '.md': # Конвертация MD → DOCX - output_path = await convert_md_to_docx(input_path, temp_dir) + output_path = await asyncio.to_thread(convert_md_to_docx, input_path, temp_dir) output_name = Path(file_name).stem + '.docx' - - elif file_ext in ['.zip']: - # Анализ архива → TXT - output_path = await analyze_archive(input_path, temp_dir, file_ext) - output_name = Path(file_name).stem + '_structure.txt' - else: - await status_msg.edit_text("❌ Неподдерживаемый формат файла!") - return + # Анализ архива → TXT + output_path = await asyncio.to_thread(analyze_archive, input_path, temp_dir) + output_name = Path(file_name).stem + '_structure.txt' # Отправка результата with open(output_path, 'rb') as output_file: @@ -102,11 +105,20 @@ async def handle_document(msg: Message) -> None: await status_msg.edit_text("✅ Конвертация завершена!") + except ArchiveTooLarge as e: + await status_msg.edit_text(f"❌ {e}") + except zipfile.BadZipFile: + await status_msg.edit_text("❌ Архив повреждён или это не .zip") except Exception as e: - logger.error(f"Ошибка обработки файла: {e}") + logger.exception("Ошибка обработки файла") await status_msg.edit_text(f"❌ Ошибка обработки: {str(e)}") -async def convert_md_to_docx(md_path: str, temp_dir: str) -> str: + +class ArchiveTooLarge(Exception): + pass + + +def convert_md_to_docx(md_path: str, temp_dir: str) -> str: """Конвертация Markdown в DOCX""" output_path = os.path.join(temp_dir, "output.docx") @@ -123,13 +135,23 @@ async def convert_md_to_docx(md_path: str, temp_dir: str) -> str: return output_path -async def analyze_archive(archive_path: str, temp_dir: str, file_ext: str) -> str: +def check_archive(zip_ref: zipfile.ZipFile) -> None: + """Архив на 20 МБ может распаковаться в гигабайты и забить диск — проверяем до распаковки""" + infos = zip_ref.infolist() + if len(infos) > MAX_FILES_IN_ARCHIVE: + raise ArchiveTooLarge(f"В архиве больше {MAX_FILES_IN_ARCHIVE} файлов") + if sum(info.file_size for info in infos) > MAX_UNPACKED_SIZE: + raise ArchiveTooLarge(f"Распакованный архив больше {MAX_UNPACKED_SIZE // 1024 // 1024} МБ") + + +def analyze_archive(archive_path: str, temp_dir: str) -> str: """Анализ архива и создание структуры проекта""" extract_dir = os.path.join(temp_dir, "extracted") os.makedirs(extract_dir, exist_ok=True) - + # Извлечение архива with zipfile.ZipFile(archive_path, 'r') as zip_ref: + check_archive(zip_ref) zip_ref.extractall(extract_dir) # Поиск основной папки проекта diff --git a/rep_to_txt.py b/rep_to_txt.py index e1f90d9..2851858 100644 --- a/rep_to_txt.py +++ b/rep_to_txt.py @@ -1,3 +1,4 @@ +import argparse import os IGNORE_PATTERNS = { @@ -100,13 +101,11 @@ def process_single_file(relative_path, file_path): file_ext = os.path.splitext(relative_path)[1].lower() - # Обнаружение двоичных файлов и генерация URL-адресов + # Обнаружение двоичных файлов (вместо изображений раньше выводилась ссылка-заготовка + # https://raw.githubusercontent.com/.../ — она никуда не вела) if file_ext in BINARY_EXTENSIONS or is_likely_binary(file_path): - # GitHub raw URL - if file_ext in {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico'}: - # Структура URL - настраивается на основе фактического хранилища - github_url = f"https://raw.githubusercontent.com/.../{relative_path.replace(os.sep, '/')}" - content_lines.append(github_url) + if file_ext in {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp', '.bmp'}: + content_lines.append("[Image - content not displayed]") else: content_lines.append("[Binary file - content not displayed]") else: @@ -141,25 +140,22 @@ def is_likely_binary(file_path): chunk = f.read(8192) # Обнаружение нулевого байта - надежный бинарный индикатор return b'\x00' in chunk - except: + except OSError: return True +def main(argv=None): + parser = argparse.ArgumentParser(description="Дерево каталогов проекта и содержимое текстовых файлов в один .txt") + parser.add_argument("path", help="папка проекта") + parser.add_argument("-o", "--output", help="куда сохранить (по умолчанию <папка>_rep.txt)") + args = parser.parse_args(argv) + + project_path = os.path.abspath(args.path) + output = args.output or os.path.basename(project_path.rstrip(os.sep)) + "_rep.txt" + with open(output, "w", encoding="utf-8") as f: + f.write(generate_complete_project_structure(project_path)) + print(output) + + if __name__ == "__main__": - # Конфигурация: измените путь к целевому каталогу проекта - project_path = r"D:\Programs\GitHub\deev.space\static" - # project_path = r"D:/Programs/GitHub/openoffice" - # project_path = "." - - print("Приступаем к формированию комплексной структуры проекта...") - tree_output = generate_complete_project_structure(project_path) - - output_filename = project_path.split('\\')[-1] + "_rep.txt" - try: - with open(output_filename, "w", encoding="utf-8") as f: - f.write(tree_output) - print(f"\nПолная проектная документация, сохраненная в: {output_filename}") - except Exception as e: - print(f"Предупреждение: Не удалось сохранить файл - {e}") - - print("Формирование структуры проекта успешно завершено!") \ No newline at end of file + main()