From 21bc00ee337213454405c6c9ebba656ea90cb337 Mon Sep 17 00:00:00 2001 From: andrei Date: Tue, 3 Jun 2025 14:11:02 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=BB=D0=BE=D0=B3=D0=BE=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=B0=D0=BD=D0=B4=D1=8B=20=D0=B8=20filename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API/app/contollers/teams_router.py | 34 ++++++++- ..._0007_добавил_filename_для_лого_команды.py | 32 +++++++++ API/app/domain/entities/team.py | 1 + API/app/domain/entities/team_logo.py | 6 ++ API/app/domain/models/teams.py | 1 + API/app/infrastructure/teams_service.py | 69 ++++++++++++++++++- 6 files changed, 141 insertions(+), 2 deletions(-) create mode 100644 API/app/database/migrations/versions/b6c6c906cd2b_0007_добавил_filename_для_лого_команды.py create mode 100644 API/app/domain/entities/team_logo.py diff --git a/API/app/contollers/teams_router.py b/API/app/contollers/teams_router.py index 22769fd..536586b 100644 --- a/API/app/contollers/teams_router.py +++ b/API/app/contollers/teams_router.py @@ -1,10 +1,12 @@ from typing import Optional -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, UploadFile, File from sqlalchemy.ext.asyncio import AsyncSession +from starlette.responses import FileResponse from app.database.session import get_db from app.domain.entities.team import TeamEntity +from app.domain.entities.team_logo import TeamLogoEntity from app.infrastructure.dependencies import get_current_user, require_admin from app.infrastructure.teams_service import TeamsService @@ -97,3 +99,33 @@ async def delete_team( ): teams_service = TeamsService(db) return await teams_service.delete_team(team_id) + + +@router.get( + "/{team_id}/file/", + response_class=FileResponse, + summary="Download logo file by team ID", + description="Returns the image file for the given team ID", +) +async def download_photo_file( + team_id: int, + db: AsyncSession = Depends(get_db), +): + teams_service = TeamsService(db) + return await teams_service.get_photo_file_by_team_id(team_id) + + +@router.post( + "/{team_id}/upload/", + response_model=TeamLogoEntity, + summary="Upload a new photo for a profile", + description="Uploads a new photo file and associates it with the given profile ID", +) +async def upload_photo( + team_id: int, + file: UploadFile = File(...), + db: AsyncSession = Depends(get_db), + user=Depends(get_current_user), +): + teams_service = TeamsService(db) + return await teams_service.upload_photo(team_id, file) diff --git a/API/app/database/migrations/versions/b6c6c906cd2b_0007_добавил_filename_для_лого_команды.py b/API/app/database/migrations/versions/b6c6c906cd2b_0007_добавил_filename_для_лого_команды.py new file mode 100644 index 0000000..d000ff7 --- /dev/null +++ b/API/app/database/migrations/versions/b6c6c906cd2b_0007_добавил_filename_для_лого_команды.py @@ -0,0 +1,32 @@ +"""0007_добавил filename для лого команды + +Revision ID: b6c6c906cd2b +Revises: 61271acdd22f +Create Date: 2025-06-03 14:01:32.752389 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b6c6c906cd2b' +down_revision: Union[str, None] = '61271acdd22f' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('teams', sa.Column('logo_filename', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('teams', 'logo_filename') + # ### end Alembic commands ### diff --git a/API/app/domain/entities/team.py b/API/app/domain/entities/team.py index 3defb14..69e4594 100644 --- a/API/app/domain/entities/team.py +++ b/API/app/domain/entities/team.py @@ -10,6 +10,7 @@ class TeamEntity(BaseModel): title: str description: Optional[str] = None logo: Optional[str] = None + logo_filename: Optional[str] = None git_url: Optional[str] = None is_active: bool diff --git a/API/app/domain/entities/team_logo.py b/API/app/domain/entities/team_logo.py new file mode 100644 index 0000000..ba5041f --- /dev/null +++ b/API/app/domain/entities/team_logo.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class TeamLogoEntity(BaseModel): + filename: str + file_path: str diff --git a/API/app/domain/models/teams.py b/API/app/domain/models/teams.py index 13b4fa4..5af0975 100644 --- a/API/app/domain/models/teams.py +++ b/API/app/domain/models/teams.py @@ -10,6 +10,7 @@ class Team(AdvancedBaseModel): title = Column(VARCHAR(150), nullable=False) description = Column(VARCHAR(150)) logo = Column(String) + logo_filename = Column(String) git_url = Column(String) is_active = Column(Boolean, default=False, nullable=False, server_default='false') diff --git a/API/app/infrastructure/teams_service.py b/API/app/infrastructure/teams_service.py index d1cede0..3dcfe60 100644 --- a/API/app/infrastructure/teams_service.py +++ b/API/app/infrastructure/teams_service.py @@ -1,10 +1,17 @@ +import os +import uuid from typing import Optional, Any, Coroutine -from fastapi import HTTPException, status +import aiofiles +from fastapi import HTTPException, status, UploadFile +from magic import magic from sqlalchemy.ext.asyncio import AsyncSession +from starlette.responses import FileResponse +from werkzeug.utils import secure_filename from app.application.teams_repository import TeamsRepository from app.domain.entities.team import TeamEntity +from app.domain.entities.team_logo import TeamLogoEntity from app.domain.models import Team @@ -75,6 +82,36 @@ class TeamsService: return self.model_to_entity(result) + async def get_photo_file_by_team_id(self, team_id: int) -> FileResponse: + team = await self.teams_repository.get_by_id(team_id) + + if not team: + raise HTTPException(404, "Команда с таким ID не найдена") + + if not os.path.exists(team.logo): + raise HTTPException(404, "Файл логотипа не найден") + + return FileResponse( + team.logo, + media_type=self.get_media_type(team.logo), + filename=team.logo_filename, + ) + + async def upload_photo(self, team_id: int, file: UploadFile): + team = await self.teams_repository.get_by_id(team_id) + + self.validate_file_type(file) + + team.logo = await self.save_file(file) + team.logo_filename = self.generate_filename(file) + + team = await self.teams_repository.update(team) + + return TeamLogoEntity( + filename=team.logo_filename, + file_path=team.logo, + ) + @staticmethod def model_to_entity(team_model: Team) -> TeamEntity: return TeamEntity( @@ -82,6 +119,7 @@ class TeamsService: title=team_model.title, description=team_model.description, logo=team_model.logo, + logo_filename=team_model.logo_filename, git_url=team_model.git_url, is_active=team_model.is_active, ) @@ -92,6 +130,7 @@ class TeamsService: title=team_entity.title, description=team_entity.description, logo=team_entity.logo, + logo_filename=team_entity.logo_filename, git_url=team_entity.git_url, is_active=team_entity.is_active, ) @@ -100,3 +139,31 @@ class TeamsService: team_model.id = team_entity.id return team_model + + async def save_file(self, file: UploadFile, upload_dir: str = "uploads/team_logos") -> str: + os.makedirs(upload_dir, exist_ok=True) + filename = self.generate_filename(file) + file_path = os.path.join(upload_dir, filename) + + async with aiofiles.open(file_path, 'wb') as out_file: + content = await file.read() + await out_file.write(content) + return file_path + + @staticmethod + def validate_file_type(file: UploadFile): + mime = magic.Magic(mime=True) + file_type = mime.from_buffer(file.file.read(1024)) + file.file.seek(0) + + if file_type not in ["image/jpeg", "image/png"]: + raise HTTPException(400, "Invalid file type") + + @staticmethod + def generate_filename(file: UploadFile): + return secure_filename(f"{uuid.uuid4()}_{file.filename}") + + @staticmethod + def get_media_type(filename: str) -> str: + extension = filename.split('.')[-1].lower() + return f"image/{extension}" if extension in ['jpeg', 'jpg', 'png'] else "application/octet-stream"