diff --git a/api/endpoints/assets.py b/api/endpoints/assets.py index f00fa44..e2243d1 100644 --- a/api/endpoints/assets.py +++ b/api/endpoints/assets.py @@ -1,13 +1,18 @@ -from fastapi import APIRouter +from typing import List, Optional + +from aiogram.types import BufferedInputFile +from fastapi import APIRouter, UploadFile, File, Form from fastapi.openapi.models import MediaType from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.responses import Response, JSONResponse +from models.Asset import Asset, AssetType from repos.dao import DAO router = APIRouter(prefix="/api/assets", tags=["Assets"]) + @router.get("/{asset_id}") async def get_asset(asset_id: str, request: Request) -> Response: dao = request.app.state.dao @@ -15,15 +20,73 @@ async def get_asset(asset_id: str, request: Request) -> Response: # 2. Проверка на существование if not asset: raise HTTPException(status_code=404, detail="Asset not found") - return Response(content=asset.data, media_type="image/png") + headers = { + # Кэшировать на 1 год (31536000 сек) + "Cache-Control": "public, max-age=31536000, immutable" + } + return Response(content=asset.data, media_type="image/png", headers=headers) @router.get("") -async def get_assets(request: Request) -> JSONResponse: +async def get_assets(request: Request) -> List[Asset]: dao: DAO = request.app.state.dao assets = await dao.assets.get_assets() - assets_links = [] - for asset in assets: - assets_links.append("/api/assets/{}".format(asset.id)) - return JSONResponse(content=assets_links) + return assets + + +@router.post("/upload", response_model=Asset) +async def upload_asset( + request: Request, + # Файл обязателен + file: UploadFile = File(...), + # Остальные поля принимаем как Form-data (не JSON!) + name: str = Form(...), + type: AssetType = Form(...), + linked_char_id: Optional[str] = Form(None) +): + """ + Загружает файл, отправляет его в ТГ (для получения ID) и сохраняет в БД. + """ + # 1. Читаем байты файла + file_content = await file.read() + + if not file_content: + raise HTTPException(status_code=400, detail="File is empty") + + # 2. Получаем необходимые зависимости из state + bot = request.app.state.bot # Бот нужен, чтобы получить tg_file_id + admin_id = request.app.state.admin_id # Куда отправлять файл "на хранение" + dao = request.app.state.assets_dao + + # 3. Отправляем файл в Telegram, чтобы получить tg_doc_file_id + # (Это обязательно, так как ваша модель требует этот ID) + try: + tg_msg = await bot.send_document( + chat_id=admin_id, + document=BufferedInputFile(file_content, filename=file.filename), + caption=f"📥 Uploaded via API: {name}" + ) + # Получаем ID документа из ответа ТГ + tg_doc_id = tg_msg.document.file_id + + # Если это картинка, можно попытаться достать и photo_id (для превью) + # Но send_document обычно возвращает именно документ. + + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to upload to Telegram: {e}") + + # 4. Создаем объект Asset + # Pydantic сам подставит created_at и вычислит link + new_asset = Asset( + name=name, + type=type, + linked_char_id=linked_char_id, + data=file_content, # Сохраняем байты в БД + tg_doc_file_id=tg_doc_id # ID из телеграма + ) + + # 5. Сохраняем через DAO + saved_asset = await dao.save_asset(new_asset) + + return saved_asset diff --git a/api/endpoints/character_router.py b/api/endpoints/character_router.py index d7c54b6..9fc6045 100644 --- a/api/endpoints/character_router.py +++ b/api/endpoints/character_router.py @@ -28,8 +28,8 @@ async def get_character_assets(character_id: str, request: Request) -> List[Asse @router.get("/{character_id}", response_model=Character) -async def get_character_by_id(character_id: int, request: Request) -> Character: +async def get_character_by_id(character_id: str, request: Request) -> Character: dao: DAO = request.app.state.dao - character = await dao.chars.get_character_by_id(character_id) + character = await dao.chars.get_character(character_id) return character diff --git a/models/Asset.py b/models/Asset.py index 7a6cfd7..130a476 100644 --- a/models/Asset.py +++ b/models/Asset.py @@ -22,10 +22,10 @@ class Asset(BaseModel): # --- CALCULATED FIELD --- @computed_field - def link(self) -> str: + def url(self) -> str: """ Это поле автоматически вычислится и попадет в model_dump() / .json() """ if self.id: - return f"/api/assets/{self.id}" + return f"/assets/{self.id}" return "" diff --git a/repos/assets_repo.py b/repos/assets_repo.py index e1b693e..a33d3fc 100644 --- a/repos/assets_repo.py +++ b/repos/assets_repo.py @@ -28,9 +28,12 @@ class AssetsRepo: return assets async def get_asset(self, asset_id: str, with_data: bool = True) -> Asset: + projection = {"_id": 1, "name": 1, "type": 1, "tg_doc_file_id": 1} + if with_data: + projection["data"] = 1 + res = await self.collection.find_one({"_id": ObjectId(asset_id)}, - {"_id": 1, "name": 1, "type": 1, "tg_doc_file_id": 1, - "data": 0 if not with_data else 1}) + projection) res["id"] = str(res.pop("_id")) return Asset(**res) diff --git a/requirements.txt b/requirements.txt index 919c804..696b62e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,6 +34,7 @@ pydantic==2.10.6 pydantic_core==2.27.2 pymongo==4.16.0 python-dotenv==1.2.1 +python-multipart==0.0.22 requests==2.32.5 rsa==4.9.1 sniffio==1.3.1