правки для настройки локальных nats smb minio

This commit is contained in:
PotapovaA 2026-09-16 16:11:36 +03:00
parent 72b32d790f
commit 8b83f155c2
7 changed files with 42 additions and 126 deletions

1
.gitignore vendored
View File

@ -322,6 +322,7 @@ dist
# Custom ignore # Custom ignore
.local/ .local/
/dev-servers/
.DS_Store .DS_Store
Dockerfile Dockerfile
docker-compose.yml docker-compose.yml

View File

@ -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

View File

@ -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()

View File

@ -24,8 +24,8 @@ from src.core.clients import S3Storage, PollWorker
from src.core.async_cache import AsyncInMemoryCache from src.core.async_cache import AsyncInMemoryCache
# from src.core.logger import setup_logger # from src.core.logger import setup_logger
from nats.aio.client import Client as NATS from nats.aio.client import Client as NATS
import nkeys
import base64 import base64
import nkeys
from loguru import logger from loguru import logger
import os import os
@ -42,18 +42,6 @@ VERSION = "1.0.0"
UP_TIME = datetime.now(timezone.utc) 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): async def sub_cb(msg):
return None return None
@ -110,22 +98,35 @@ async def lifespan(app):
app.state.storage = S3Storage(s3_client, minio.MINIO_BUCKET) app.state.storage = S3Storage(s3_client, minio.MINIO_BUCKET)
logger.info("MinIO подключён, бакет: {}", 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") missing = [name for name in ("MINIO", "NATS", "SMB")
if getattr(settings.OPENBAO, name) is None] if getattr(settings.OPENBAO, name) is None]
if missing: if missing:
logger.info("Распознавание файлов отключено: не настроены {}", ", ".join(missing)) logger.info("Распознавание файлов отключено: не настроены {}", ", ".join(missing))
else: 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() cache = AsyncInMemoryCache()
app.state.poller = PollWorker(nc, cache, app.state.storage) app.state.poller = PollWorker(nc, cache, app.state.storage)
await app.state.poller.start() await app.state.poller.start()
@ -138,10 +139,11 @@ async def lifespan(app):
for worker in (app.state.poller, app.state.pdf_poller): for worker in (app.state.poller, app.state.pdf_poller):
if worker is not None: if worker is not None:
await worker.stop() await worker.stop()
for sub in app.state.nats_subs: if not settings.DEBUG:
await sub.unsubscribe() for sub in app.state.nats_subs:
if app.state.nats is not None: await sub.unsubscribe()
await app.state.nats.close() if app.state.nats is not None:
await app.state.nats.close()
if s3_client is not None: if s3_client is not None:
await s3_client.close_session() await s3_client.close_session()
if smb_registered: if smb_registered:

View File

@ -65,7 +65,7 @@ class PDFPollWorker:
logger.exception("Не получилось достать данные из кэша") logger.exception("Не получилось достать данные из кэша")
jobs = [] 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: for job in jobs:
search_fname = f"{job.file_id}" search_fname = f"{job.file_id}"
@ -85,8 +85,9 @@ class PDFPollWorker:
logger.info("Начинаю обработку файла по пути smb_dir={smb_dir}", smb_dir=new_dir) logger.info("Начинаю обработку файла по пути smb_dir={smb_dir}", smb_dir=new_dir)
files = [] files = []
for fname in smbclient.listdir(new_dir): for fname in smbclient.listdir(new_dir, port=settings.OPENBAO.SMB.SMB_PORT):
with smbclient.open_file(f'{new_dir}/{fname}', 'r') as f: with smbclient.open_file(f'{new_dir}/{fname}', 'r',
port=settings.OPENBAO.SMB.SMB_PORT) as f:
files.append(json.loads(f.read())) files.append(json.loads(f.read()))
s3_json_dict = { s3_json_dict = {
'file_id': job.file_id, 'file_id': job.file_id,

View File

@ -108,7 +108,8 @@ class PollWorker:
if row['subject'] == 'file.img_uploaded': if row['subject'] == 'file.img_uploaded':
last_part = row['row_payload']['s3_key'].split('/')[-1] 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']) data = await self.storage.get_object(row['row_payload']['s3_key'])
content = await data.read() content = await data.read()
f.write(content) f.write(content)
@ -177,4 +178,4 @@ class PollWorker:
file_stmt = update(StoredFile).where(StoredFile.current_recognition_job_id == row_id).values(status=None) file_stmt = update(StoredFile).where(StoredFile.current_recognition_job_id == row_id).values(status=None)
await session.execute(stmt) await session.execute(stmt)
await session.execute(file_stmt) await session.execute(file_stmt)
await session.commit() await session.commit()

View File

@ -1,4 +1,5 @@
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import Field
class DBSettings(BaseSettings): class DBSettings(BaseSettings):
@ -13,11 +14,12 @@ class MinioSettings(BaseSettings):
class NATSSettings(BaseSettings): class NATSSettings(BaseSettings):
NATS_URL: str NATS_URL: str
NATS_SEED: str NATS_JWT: str = Field(min_length=1)
NATS_JWT: str NATS_SEED: str = Field(min_length=1)
class SMBSettings(BaseSettings): class SMBSettings(BaseSettings):
SMB_HOST: str SMB_HOST: str
SMB_PORT: int = 445
SMB_PATH: str SMB_PATH: str
SMB_LOGIN: str SMB_LOGIN: str
SMB_PASSWORD: str SMB_PASSWORD: str