diff --git a/README.md b/README.md index 99fb364..b12b3fa 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ Telegram-бот — только часть рабочего окружения ## Текущее состояние Сейчас реализованы Telegram-бот-секретарь версии `0.2.2` и backend версии -`0.2.1`: возобновляемая выгрузка и нормализация документов ЦБД Минюста КР. +`0.2.2`: возобновляемая выгрузка и нормализация документов ЦБД Минюста КР. | Компонент | Версия | Состояние | |---|---:|---| | Telegram-бот | `0.2.2` | на Synology работает `0.2.1`; обновление после слияния | -| Backend | `0.2.1` | реализованы выгрузка и нормализация документов ЦБД Минюста КР | +| Backend | `0.2.2` | реализованы выгрузка и нормализация документов ЦБД Минюста КР | | Frontend | — | ещё не создан | -| Сбор и обработка правовых данных | `0.2.1` | реализованы архиватор и нормализатор ЦБД Минюста КР | +| Сбор и обработка правовых данных | `0.2.2` | реализованы архиватор и нормализатор ЦБД Минюста КР | | RAG и база знаний | — | ещё не созданы | ## Структура репозитория @@ -58,4 +58,4 @@ python3 -m unittest discover -s tools/telegram-bot -v --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.2 · Frontend — не создан diff --git a/backend/README.md b/backend/README.md index 8b841d1..1d36a0f 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,6 +1,6 @@ # Backend Акылдаш -Версия: `0.2.1` +Версия: `0.2.2` Первая backend-область проекта — загрузка правовых документов из официального Open Data API ЦБД Минюста Кыргызской Республики. Код расположен в @@ -68,6 +68,8 @@ python3 backend/normalization/minjust_cbd.py в `data/minjust-normalized`. Пути можно изменить параметрами `--input` и `--output`; `--refresh` принудительно обрабатывает неизменившиеся документы, `--log-level` задаёт уровень журнала. +Каталоги `--input` и `--output` не должны совпадать, содержать друг друга или +пересекаться через разрешённые абсолютные пути. ```text data/minjust-normalized/ @@ -98,4 +100,4 @@ PYTHONPATH=backend python3 -m unittest backend/test_minjust_normalization.py -v --- -Акылдаш · Backend v0.2.1 · Frontend — не создан +Акылдаш · Backend v0.2.2 · Frontend — не создан diff --git a/backend/ingestion/minjust_cbd.py b/backend/ingestion/minjust_cbd.py index a46e364..e40db76 100644 --- a/backend/ingestion/minjust_cbd.py +++ b/backend/ingestion/minjust_cbd.py @@ -21,7 +21,7 @@ from datetime import datetime, timezone from pathlib import Path from typing import Callable, Iterable -APP_VERSION = "0.2.1" +APP_VERSION = "0.2.2" API_BASE_URL = "https://cbd.minjust.gov.kg/api/v1/OpenData/" LANGUAGES = {"Rus": "ru", "Kyr": "ky"} IMAGE_LANGUAGES = {"Russian": "ru", "Kyrgyz": "ky"} diff --git a/backend/normalization/minjust_cbd.py b/backend/normalization/minjust_cbd.py index b1b40a6..9631b21 100644 --- a/backend/normalization/minjust_cbd.py +++ b/backend/normalization/minjust_cbd.py @@ -23,7 +23,7 @@ from pathlib import Path from typing import Callable from urllib.parse import urlsplit -APP_VERSION = "0.2.1" +APP_VERSION = "0.2.2" SCHEMA_VERSION = "1" NORMALIZER_VERSION = "1.0.0" LANGUAGES = ("ru", "ky") @@ -234,7 +234,10 @@ class SafeHtmlParser(HTMLParser): return safe def safe_image(self, value: str) -> bool: - parsed = urlsplit(value) + try: + parsed = urlsplit(value) + except ValueError: + return False if parsed.scheme or parsed.netloc or not parsed.path or parsed.path.startswith(("/", "\\")): return False candidate = (self.edition_directory / parsed.path.replace("\\", "/")).resolve() @@ -249,7 +252,10 @@ def safe_link(value: str) -> bool: value = value.strip() if not value or value.startswith(("//", "\\\\")): return False - parsed = urlsplit(value) + try: + parsed = urlsplit(value) + except ValueError: + return False return parsed.scheme.lower() in {"", "http", "https", "mailto"} and not ( not parsed.scheme and parsed.netloc ) @@ -531,6 +537,23 @@ def publish_directory(staged: Path, target: Path) -> None: shutil.rmtree(backup) +def recover_directory(target: Path) -> None: + backup = target.parent / f".{target.name}.previous" + if not backup.exists(): + return + if target.exists(): + shutil.rmtree(backup) + else: + os.replace(backup, target) + + +def validate_roots(input_root: Path, output: Path) -> None: + source = input_root.resolve() + destination = output.resolve() + if source == destination or source.is_relative_to(destination) or destination.is_relative_to(source): + raise ValueError("--input and --output must not overlap") + + def normalize_archive( input_root: Path = Path("data/minjust-cbd"), output: Path = Path("data/minjust-normalized"), @@ -538,6 +561,7 @@ def normalize_archive( refresh: bool = False, progress: Callable[[NormalizeResult, int], None] | None = None, ) -> NormalizeResult: + validate_roots(input_root, output) document_root = input_root / "documents" if not document_root.is_dir(): raise FileNotFoundError(f"Document directory not found: {document_root}") @@ -563,10 +587,12 @@ def normalize_archive( for source_directory in directories: discovered += 1 code = source_directory.name + target = output / "documents" / code checksum = None try: + recover_directory(target) files, checksum = source_inventory(source_directory, input_root) - if not refresh and known.get(code) == ( + if target.is_dir() and not refresh and known.get(code) == ( checksum, SCHEMA_VERSION, NORMALIZER_VERSION, "success" ): skipped += 1 @@ -574,7 +600,7 @@ def normalize_archive( with tempfile.TemporaryDirectory(dir=staging_root) as temporary: staged = Path(temporary) / code normalize_document(source_directory, input_root, staged, files, checksum) - publish_directory(staged, output / "documents" / code) + publish_directory(staged, target) with connection: connection.execute( """ diff --git a/backend/test_minjust_normalization.py b/backend/test_minjust_normalization.py index 98d31c7..bfc54a6 100644 --- a/backend/test_minjust_normalization.py +++ b/backend/test_minjust_normalization.py @@ -1,4 +1,5 @@ import json +import os import sqlite3 import tempfile import unittest @@ -55,7 +56,7 @@ class MinjustNormalizationTest(unittest.TestCase): document, 10, { - "ru": '
Статья 1 Закон
ссылка', + "ru": 'Статья 1 Закон
ссылкасломанная ссылка1. Кыргызча жобо
"}) @@ -73,6 +74,7 @@ class MinjustNormalizationTest(unittest.TestCase): self.assertNotIn("script", safe_html) self.assertNotIn("style=", safe_html) self.assertNotIn("javascript:", safe_html) + self.assertNotIn("http://[", safe_html) self.assertIn("Статья 1 Закон", safe_html) parsed = json.loads(fragments) self.assertEqual(parsed[0]["type"], "article") @@ -116,6 +118,35 @@ class MinjustNormalizationTest(unittest.TestCase): ("success", None, None), ) + def test_rejects_overlapping_input_and_output(self): + with tempfile.TemporaryDirectory() as temporary: + source = Path(temporary) / "source" + metadata = self.document(source) / "metadata.json" + original = metadata.read_bytes() + + for output in (source, source / "normalized", source.parent): + with self.subTest(output=output): + with self.assertRaisesRegex(ValueError, "must not overlap"): + normalize_archive(source, output) + self.assertEqual(metadata.read_bytes(), original) + + def test_recovers_interrupted_directory_publication_before_skip(self): + with tempfile.TemporaryDirectory() as temporary: + base = Path(temporary) + source = base / "source" + output = base / "normalized" + self.document(source) + first = normalize_archive(source, output) + target = output / "documents/1" + backup = output / "documents/.1.previous" + os.replace(target, backup) + + second = normalize_archive(source, output) + + self.assertEqual((first.normalized, second.skipped), (1, 1)) + self.assertTrue((target / "document.json").is_file()) + self.assertFalse(backup.exists()) + if __name__ == "__main__": unittest.main() diff --git a/docs/README.md b/docs/README.md index a7b63cb..13b2d5c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -33,4 +33,4 @@ --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.2 · Frontend — не создан diff --git a/docs/decisions/001-telegram-workspace-mvp.md b/docs/decisions/001-telegram-workspace-mvp.md index 0b7bfa2..616363a 100644 --- a/docs/decisions/001-telegram-workspace-mvp.md +++ b/docs/decisions/001-telegram-workspace-mvp.md @@ -78,4 +78,4 @@ Telegram позволяет запретить пользователям отп --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.2 · Frontend — не создан diff --git a/docs/operations/project-status.md b/docs/operations/project-status.md index de5b13a..a2cf417 100644 --- a/docs/operations/project-status.md +++ b/docs/operations/project-status.md @@ -5,7 +5,7 @@ - Telegram-бот: `0.2.2` - Telegram-бот на Synology: `0.2.1` -- Backend: `0.2.1` +- Backend: `0.2.2` - Frontend: не создан ## Краткий итог @@ -68,10 +68,10 @@ - Локальный Git-репозиторий восстановлен и привязан к Gitea. - Репозиторий организован как основа всего проекта, а не отдельного бота. - Бот развёрнут в Container Manager на Synology; автозапуск после перезапуска менеджера проверен. -- Реализован backend-загрузчик ЦБД Минюста КР версии `0.2.1` без внешних зависимостей. +- Реализован backend-загрузчик ЦБД Минюста КР версии `0.2.2` без внешних зависимостей. - Загрузчик сохраняет метаданные, редакции RU/KY и изображения, а прогресс — в SQLite. - Пилотная выгрузка двух документов и возобновление без повторного скачивания проверены на живом API. -- Реализован backend-нормализатор версии `0.2.1` без внешних зависимостей. +- Реализован backend-нормализатор версии `0.2.2` без внешних зависимостей. - Нормализатор создаёт канонические метаданные, безопасный HTML, чистый текст и адресуемые фрагменты RU/KY. - SQLite-манифест обеспечивает возобновление, повтор ошибок и пропуск неизменившихся документов. @@ -134,6 +134,9 @@ ### 2026-08-12 +- Нормализатор запрещает пересекающиеся каталоги источника и результата, + восстанавливает прерванную публикацию и отбрасывает некорректные URL. +- Версия backend обновлена до `0.2.2`. - Загрузчик пересоздаёт временный список документов на текущей странице не только после HTTP 404, но и после исчерпания повторов запроса списка. - Версия backend обновлена до `0.2.1`. @@ -194,4 +197,4 @@ --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.2 · Frontend — не создан diff --git a/docs/operations/telegram-workspace-plan.md b/docs/operations/telegram-workspace-plan.md index 084ec2f..7dabeaa 100644 --- a/docs/operations/telegram-workspace-plan.md +++ b/docs/operations/telegram-workspace-plan.md @@ -398,4 +398,4 @@ Git сохраняет актуальную версию --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.2 · Frontend — не создан diff --git a/docs/product/frontend-search-sps-plan.md b/docs/product/frontend-search-sps-plan.md index fa69f7d..552e0a9 100644 --- a/docs/product/frontend-search-sps-plan.md +++ b/docs/product/frontend-search-sps-plan.md @@ -258,4 +258,4 @@ runtime-зависимостями frontend. Регистрация в стор --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.2 · Frontend — не создан diff --git a/docs/product/minjust-document-normalization-agent-task.md b/docs/product/minjust-document-normalization-agent-task.md index 67febfc..ff85ecc 100644 --- a/docs/product/minjust-document-normalization-agent-task.md +++ b/docs/product/minjust-document-normalization-agent-task.md @@ -214,4 +214,4 @@ python3 backend/normalization/minjust_cbd.py --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.2 · Frontend — не создан diff --git a/docs/product/project-overview.md b/docs/product/project-overview.md index 254f1b2..8c8b926 100644 --- a/docs/product/project-overview.md +++ b/docs/product/project-overview.md @@ -44,4 +44,4 @@ --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.2 · Frontend — не создан diff --git a/docs/team/ai-skills-for-beginners.md b/docs/team/ai-skills-for-beginners.md index a022048..0ad0508 100644 --- a/docs/team/ai-skills-for-beginners.md +++ b/docs/team/ai-skills-for-beginners.md @@ -180,4 +180,4 @@ --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.2 · Frontend — не создан diff --git a/tools/telegram-bot/README.md b/tools/telegram-bot/README.md index 0b2ae58..066a23a 100644 --- a/tools/telegram-bot/README.md +++ b/tools/telegram-bot/README.md @@ -48,4 +48,4 @@ python3 -m unittest -v --- -Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.1 · Frontend — не создан +Акылдаш · Telegram-бот v0.2.2 · Backend v0.2.2 · Frontend — не создан