Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"""Хранение вложений: MinIO (S3-совместимое), креды в env.
|
||||
|
||||
Если MinIO не настроен (локальная разработка без docker-compose), объекты
|
||||
сохраняются в локальную папку DATA_DIR/attachments — API и карточки при этом
|
||||
не меняются, в БД хранится только ключ объекта.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from .. import config
|
||||
|
||||
log = logging.getLogger("leadradar.objectstore")
|
||||
|
||||
|
||||
def configured() -> bool:
|
||||
return bool(config.MINIO_ENDPOINT and config.MINIO_ACCESS_KEY and config.MINIO_SECRET_KEY)
|
||||
|
||||
|
||||
_client = None
|
||||
_client_checked = False
|
||||
|
||||
|
||||
def client():
|
||||
"""MinIO-клиент (создаётся лениво). Бросает ошибку, если не настроен."""
|
||||
global _client, _client_checked
|
||||
if _client is not None:
|
||||
return _client
|
||||
if not configured():
|
||||
raise RuntimeError(
|
||||
"MinIO не настроен: задайте LEADRADAR_MINIO_ENDPOINT / _ACCESS_KEY / _SECRET_KEY / _BUCKET"
|
||||
)
|
||||
from minio import Minio
|
||||
from minio.error import S3Error
|
||||
|
||||
_client = Minio(
|
||||
config.MINIO_ENDPOINT,
|
||||
access_key=config.MINIO_ACCESS_KEY,
|
||||
secret_key=config.MINIO_SECRET_KEY,
|
||||
secure=config.MINIO_SECURE,
|
||||
)
|
||||
if not _client_checked:
|
||||
_client_checked = True
|
||||
try:
|
||||
if not _client.bucket_exists(config.MINIO_BUCKET):
|
||||
_client.make_bucket(config.MINIO_BUCKET)
|
||||
except S3Error as exc:
|
||||
log.warning("bucket check failed: %s", exc)
|
||||
return _client
|
||||
|
||||
|
||||
def _local_path(object_key: str) -> Path:
|
||||
# object_key вида projects/{card_id}/{ts}_{name}; не даём выйти за пределы FILES_DIR
|
||||
safe = Path(object_key).name
|
||||
parent = Path(object_key).parent
|
||||
return (config.FILES_DIR / parent / safe).resolve()
|
||||
|
||||
|
||||
def put(card_id: str, name: str, data: bytes, content_type: str) -> str:
|
||||
"""Сохраняет объект (MinIO или локально), возвращает objectKey."""
|
||||
import time
|
||||
|
||||
object_key = f"projects/{card_id}/{int(time.time() * 1000)}_{name}"
|
||||
if configured():
|
||||
client().put_object(
|
||||
config.MINIO_BUCKET,
|
||||
object_key,
|
||||
io.BytesIO(data),
|
||||
length=len(data),
|
||||
content_type=content_type or "application/octet-stream",
|
||||
)
|
||||
else:
|
||||
path = _local_path(object_key)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(data)
|
||||
log.info("MinIO не настроен — файл сохранён локально: %s", path)
|
||||
return object_key
|
||||
|
||||
|
||||
def get(object_key: str):
|
||||
"""Возвращает поток (бинарный) для скачивания."""
|
||||
if configured():
|
||||
try:
|
||||
return client().get_object(config.MINIO_BUCKET, object_key)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.warning("minio get failed: %s", exc)
|
||||
raise
|
||||
path = _local_path(object_key)
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(object_key)
|
||||
return io.BytesIO(path.read_bytes())
|
||||
|
||||
|
||||
def remove(object_key: str) -> None:
|
||||
if configured():
|
||||
try:
|
||||
client().remove_object(config.MINIO_BUCKET, object_key)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.warning("minio remove failed: %s", exc)
|
||||
return
|
||||
try:
|
||||
path = _local_path(object_key)
|
||||
if path.is_file():
|
||||
path.unlink()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log.warning("local remove failed: %s", exc)
|
||||
Reference in New Issue
Block a user