diff --git a/.gitignore b/.gitignore index 143abe8..dea0407 100644 --- a/.gitignore +++ b/.gitignore @@ -322,6 +322,7 @@ dist # Custom ignore .local/ +/dev-servers/ .DS_Store Dockerfile docker-compose.yml diff --git a/api/LOCAL_MINIO.md b/api/LOCAL_MINIO.md deleted file mode 100644 index 96de2ba..0000000 --- a/api/LOCAL_MINIO.md +++ /dev/null @@ -1,58 +0,0 @@ -# MinIO на Mac (Apple Silicon) - -MinIO запускается отдельным процессом, данные сохраняются в `.local/minio/data` -в корне проекта. Папка `.local/` и `api/.env` исключены из Git. - -## Запуск - -Из папки `api`, в активированном окружении: - -```bash -python run_minio.py -``` - -Остановка — Ctrl+C. Данные сохраняются после перезапуска. - -При первоначальной настройке сервер уже запущен в фоне. Его журнал находится -в `.local/minio/server.log`, PID — в `.local/minio/minio.pid` (пути от корня -проекта). Не запускай второй экземпляр на тех же портах. Фоновый процесс не -настроен на автоматический запуск после перезагрузки Mac. - -- S3 API: http://127.0.0.1:9000 -- Консоль: http://127.0.0.1:9001 -- Бакет: `dkb-files` -- Логин и пароль: `OPENBAO__MINIO__MINIO_ADMIN_LOGIN` и - `OPENBAO__MINIO__MINIO_ADMIN_PASSWORD` в `api/.env`. - -Названия переменных окружения регистрозависимы; используй написание из примеров. - -MinIO слушает только локальный интерфейс. Для локального HTTP в `.env` задано: - -```dotenv -OPENBAO__MINIO__MINIO_URL=127.0.0.1:9000 -OPENBAO__MINIO__MINIO_BUCKET=dkb-files -OPENBAO__MINIO__MINIO_SECURE=false -``` - -Адрес указывается без `http://`. Для TLS используется `MINIO_SECURE=true` -(значение по умолчанию). - -После запуска MinIO запусти API в другом терминале: - -```bash -uvicorn src.app:app --reload -``` - -MinIO работает независимо от NATS и SMB. Загруженные файлы сохраняются, но -задания распознавания остаются в очереди до настройки остальных интеграций. - -## Повторная сборка бинарника - -Из корня проекта, после установки Go (`brew install go`): - -```bash -mkdir -p .local/minio/bin -GOBIN="$PWD/.local/minio/bin" go install github.com/minio/minio@RELEASE.2025-10-15T17-29-55Z -``` - -Исходники: https://github.com/minio/minio diff --git a/api/run_minio.py b/api/run_minio.py deleted file mode 100644 index 2e23896..0000000 --- a/api/run_minio.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Run local MinIO on macOS: python run_minio.py (Ctrl+C to stop).""" - -import os -from pathlib import Path - -from dotenv import dotenv_values - - -def main() -> None: - api_dir = Path(__file__).resolve().parent - local_dir = api_dir.parent / ".local" / "minio" - binary = local_dir / "bin" / "minio" - if not binary.is_file(): - raise SystemExit("MinIO не собран. См. LOCAL_MINIO.md") - values = {**dotenv_values(api_dir / ".env"), **os.environ} - prefix = "OPENBAO__MINIO__" - credentials = [prefix + "MINIO_ADMIN_LOGIN", prefix + "MINIO_ADMIN_PASSWORD"] - if any(not values.get(key) for key in credentials): - raise SystemExit("Укажи логин и пароль MinIO в api/.env") - data_dir = local_dir / "data" - data_dir.mkdir(parents=True, exist_ok=True) - env = dict(os.environ) - env.update( - MINIO_ROOT_USER=values[credentials[0]], - MINIO_ROOT_PASSWORD=values[credentials[1]], - ) - os.execve(binary, [str(binary), "server", str(data_dir), - "--address", "127.0.0.1:9000", - "--console-address", "127.0.0.1:9001"], env) - - -if __name__ == "__main__": - main() diff --git a/api/src/app.py b/api/src/app.py index 512661a..28920b6 100644 --- a/api/src/app.py +++ b/api/src/app.py @@ -24,8 +24,8 @@ from src.core.clients import S3Storage, PollWorker from src.core.async_cache import AsyncInMemoryCache # from src.core.logger import setup_logger from nats.aio.client import Client as NATS -import nkeys import base64 +import nkeys from loguru import logger import os @@ -42,18 +42,6 @@ VERSION = "1.0.0" UP_TIME = datetime.now(timezone.utc) -def jwt_cb(): - return settings.OPENBAO.NATS.NATS_JWT.encode('utf-8') - - -def sig_cb(nonce): - if (isinstance(nonce, str)): - nonce = nonce.encode('utf-8') - kp = nkeys.from_seed(settings.OPENBAO.NATS.NATS_SEED.encode('utf-8')) - signed = kp.sign(nonce) - return base64.b64encode(signed) - - async def sub_cb(msg): return None @@ -110,22 +98,35 @@ async def lifespan(app): app.state.storage = S3Storage(s3_client, minio.MINIO_BUCKET) logger.info("MinIO подключён, бакет: {}", minio.MINIO_BUCKET) + smb = settings.OPENBAO.SMB + if smb is not None: + smbclient.register_session(smb.SMB_HOST, smb.SMB_LOGIN, smb.SMB_PASSWORD, + port=smb.SMB_PORT) + smb_registered = True + logger.info("SMB подключён") + + nats_settings = settings.OPENBAO.NATS + if nats_settings is not None: + nc = NATS() + app.state.nats = nc + if not settings.DEBUG: + keypair = nkeys.from_seed(nats_settings.NATS_SEED.encode("utf-8")) + + def user_jwt_cb(): + return nats_settings.NATS_JWT.encode("utf-8") + + def signature_cb(nonce): + return base64.b64encode(keypair.sign(nonce.encode("utf-8"))) + + await nc.connect(nats_settings.NATS_URL, user_jwt_cb=user_jwt_cb, + signature_cb=signature_cb, max_reconnect_attempts=2) + logger.info("NATS подключён") + missing = [name for name in ("MINIO", "NATS", "SMB") if getattr(settings.OPENBAO, name) is None] if missing: logger.info("Распознавание файлов отключено: не настроены {}", ", ".join(missing)) else: - smb = settings.OPENBAO.SMB - smbclient.register_session(smb.SMB_HOST, smb.SMB_LOGIN, smb.SMB_PASSWORD) - smb_registered = True - nc = NATS() - app.state.nats = nc - if not settings.DEBUG: - await nc.connect( - settings.OPENBAO.NATS.NATS_URL, - user_jwt_cb=jwt_cb, signature_cb=sig_cb, - max_reconnect_attempts=2, - ) cache = AsyncInMemoryCache() app.state.poller = PollWorker(nc, cache, app.state.storage) await app.state.poller.start() @@ -138,10 +139,11 @@ async def lifespan(app): for worker in (app.state.poller, app.state.pdf_poller): if worker is not None: await worker.stop() - for sub in app.state.nats_subs: - await sub.unsubscribe() - if app.state.nats is not None: - await app.state.nats.close() + if not settings.DEBUG: + for sub in app.state.nats_subs: + await sub.unsubscribe() + if app.state.nats is not None: + await app.state.nats.close() if s3_client is not None: await s3_client.close_session() if smb_registered: diff --git a/api/src/core/PDFWorker/PDFTempPoller.py b/api/src/core/PDFWorker/PDFTempPoller.py index f231424..9d31ccc 100644 --- a/api/src/core/PDFWorker/PDFTempPoller.py +++ b/api/src/core/PDFWorker/PDFTempPoller.py @@ -65,7 +65,7 @@ class PDFPollWorker: logger.exception("Не получилось достать данные из кэша") jobs = [] - all_files = smbclient.listdir(self.smb_path) + all_files = smbclient.listdir(self.smb_path, port=settings.OPENBAO.SMB.SMB_PORT) for job in jobs: search_fname = f"{job.file_id}" @@ -85,8 +85,9 @@ class PDFPollWorker: logger.info("Начинаю обработку файла по пути smb_dir={smb_dir}", smb_dir=new_dir) files = [] - for fname in smbclient.listdir(new_dir): - with smbclient.open_file(f'{new_dir}/{fname}', 'r') as f: + for fname in smbclient.listdir(new_dir, port=settings.OPENBAO.SMB.SMB_PORT): + with smbclient.open_file(f'{new_dir}/{fname}', 'r', + port=settings.OPENBAO.SMB.SMB_PORT) as f: files.append(json.loads(f.read())) s3_json_dict = { 'file_id': job.file_id, diff --git a/api/src/core/clients.py b/api/src/core/clients.py index a0092b0..4ffc401 100644 --- a/api/src/core/clients.py +++ b/api/src/core/clients.py @@ -108,7 +108,8 @@ class PollWorker: if row['subject'] == 'file.img_uploaded': last_part = row['row_payload']['s3_key'].split('/')[-1] - with smbclient.open_file(f'{settings.OPENBAO.SMB.SMB_PATH}/Файлы/{last_part}', 'wb') as f: + with smbclient.open_file(f'{settings.OPENBAO.SMB.SMB_PATH}/Файлы/{last_part}', 'wb', + port=settings.OPENBAO.SMB.SMB_PORT) as f: data = await self.storage.get_object(row['row_payload']['s3_key']) content = await data.read() f.write(content) @@ -177,4 +178,4 @@ class PollWorker: file_stmt = update(StoredFile).where(StoredFile.current_recognition_job_id == row_id).values(status=None) await session.execute(stmt) await session.execute(file_stmt) - await session.commit() \ No newline at end of file + await session.commit() diff --git a/api/src/core/config.py b/api/src/core/config.py index 1336ccc..91ecada 100644 --- a/api/src/core/config.py +++ b/api/src/core/config.py @@ -1,4 +1,5 @@ from pydantic_settings import BaseSettings, SettingsConfigDict +from pydantic import Field class DBSettings(BaseSettings): @@ -13,11 +14,12 @@ class MinioSettings(BaseSettings): class NATSSettings(BaseSettings): NATS_URL: str - NATS_SEED: str - NATS_JWT: str + NATS_JWT: str = Field(min_length=1) + NATS_SEED: str = Field(min_length=1) class SMBSettings(BaseSettings): SMB_HOST: str + SMB_PORT: int = 445 SMB_PATH: str SMB_LOGIN: str SMB_PASSWORD: str