diff --git a/cars.db b/cars.db index 56ff929..a924ab7 100644 Binary files a/cars.db and b/cars.db differ diff --git a/dockerfile b/dockerfile index ef20ec4..534e722 100644 --- a/dockerfile +++ b/dockerfile @@ -8,11 +8,14 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . -# Создаем директорию для хранения БД -RUN mkdir -p /app/data +# Создаем директории для хранения файлов +RUN mkdir -p /app/static/images # Указываем порт, который будет слушать приложение EXPOSE 3000 +# Указываем том для хранения загруженных изображений +VOLUME /app/static/images + # Запускаем приложение CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"] diff --git a/main.py b/main.py index 6e4d5f9..f9888a0 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ -from fastapi import FastAPI, Depends, HTTPException, status, Query, Path +from fastapi import FastAPI, Depends, HTTPException, status, Query, Path, UploadFile, File, Form +from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session import crud import models @@ -6,12 +7,17 @@ import schemas from database import engine, get_db from typing import List, Optional import uvicorn +from utils import save_image, delete_image +import json # Создание таблиц в БД models.Base.metadata.create_all(bind=engine) app = FastAPI(title="AutoBro API", description="API для управления базой данных автомобилей") +# Добавляем обработку статических файлов +app.mount("/static", StaticFiles(directory="static"), name="static") + @app.get("/") def read_root(): return {"message": "AutoBro API"} @@ -40,38 +46,104 @@ def get_car( return {"car": car} @app.post("/cars", response_model=schemas.CarResponse, status_code=status.HTTP_201_CREATED) -def create_car( - car: schemas.CarCreate, +async def create_car( + car_data: str = Form(..., description="Данные автомобиля в JSON формате"), + image: UploadFile = File(None, description="Изображение автомобиля"), db: Session = Depends(get_db) ): - db_car = crud.create_car(db=db, car=car) - return {"car": db_car} + try: + # Преобразуем строку JSON в словарь + car_dict = json.loads(car_data) + + # Загружаем изображение, если оно предоставлено + if image: + image_path = await save_image(image) + car_dict["image"] = image_path + + # Создаем объект Pydantic для валидации данных + car = schemas.CarCreate(**car_dict) + + # Создаем запись в БД + db_car = crud.create_car(db=db, car=car) + return {"car": db_car} + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except json.JSONDecodeError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Неверный формат JSON" + ) @app.put("/cars/{car_id}", response_model=schemas.CarResponse) -def update_car( +async def update_car( car_id: int = Path(..., description="ID автомобиля", gt=0), - car_update: schemas.CarUpdate = ..., + car_data: str = Form(None, description="Данные автомобиля в JSON формате"), + image: UploadFile = File(None, description="Изображение автомобиля"), db: Session = Depends(get_db) ): - updated_car = crud.update_car(db=db, car_id=car_id, car_update=car_update) - if updated_car is None: + # Проверяем существование автомобиля + existing_car = crud.get_car(db, car_id=car_id) + if existing_car is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Автомобиль не найден" ) - return {"car": updated_car} + + try: + # Преобразуем строку JSON в словарь, если она предоставлена + car_dict = {} + if car_data: + car_dict = json.loads(car_data) + + # Загружаем новое изображение, если оно предоставлено + if image: + # Удаляем старое изображение, если есть + if existing_car.image: + delete_image(existing_car.image) + + # Сохраняем новое изображение + image_path = await save_image(image) + car_dict["image"] = image_path + + # Создаем объект Pydantic для валидации данных + car_update = schemas.CarUpdate(**car_dict) + + # Обновляем запись в БД + updated_car = crud.update_car(db=db, car_id=car_id, car_update=car_update) + return {"car": updated_car} + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + except json.JSONDecodeError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Неверный формат JSON" + ) @app.delete("/cars/{car_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_car( car_id: int = Path(..., description="ID автомобиля", gt=0), db: Session = Depends(get_db) ): - success = crud.delete_car(db=db, car_id=car_id) - if not success: + # Получаем автомобиль перед удалением + car = crud.get_car(db, car_id=car_id) + if car is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Автомобиль не найден" ) + + # Удаляем изображение, если есть + if car.image: + delete_image(car.image) + + # Удаляем запись из БД + crud.delete_car(db=db, car_id=car_id) return None if __name__ == "__main__": diff --git a/static/images/d5f39167-e347-48b5-8d2b-2b2ce9139e0a.png b/static/images/d5f39167-e347-48b5-8d2b-2b2ce9139e0a.png new file mode 100644 index 0000000..9f1e0c2 Binary files /dev/null and b/static/images/d5f39167-e347-48b5-8d2b-2b2ce9139e0a.png differ diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..1b8d7dc --- /dev/null +++ b/utils.py @@ -0,0 +1,53 @@ +import os +import uuid +from fastapi import UploadFile +from pathlib import Path + +# Путь для сохранения изображений +IMAGES_DIR = Path("static/images") + +# Разрешенные типы файлов +ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"} + +async def save_image(file: UploadFile) -> str: + """ + Сохраняет изображение в директорию и возвращает относительный путь к нему + """ + if not file: + return None + + # Получаем расширение файла + _, ext = os.path.splitext(file.filename) + if ext.lower() not in ALLOWED_EXTENSIONS: + raise ValueError(f"Неподдерживаемый формат файла. Поддерживаемые форматы: {', '.join(ALLOWED_EXTENSIONS)}") + + # Создаем уникальное имя файла + filename = f"{uuid.uuid4()}{ext}" + file_path = IMAGES_DIR / filename + + # Создаем директорию, если не существует + os.makedirs(IMAGES_DIR, exist_ok=True) + + # Сохраняем файл + contents = await file.read() + with open(file_path, "wb") as f: + f.write(contents) + + # Возвращаем относительный путь для хранения в базе данных + return f"/static/images/{filename}" + +def delete_image(image_path: str) -> None: + """ + Удаляет файл изображения, если оно существует + """ + if not image_path: + return + + # Удаляем первый слэш из пути, если есть + if image_path.startswith("/"): + image_path = image_path[1:] + + full_path = Path(image_path) + + if full_path.exists(): + os.unlink(full_path)