Compare commits

...

2 Commits

Author SHA1 Message Date
dc96d7a15a Stop tracking static/ folder 2025-07-07 20:06:35 +05:00
5c4d5bb5a2 add: car image support 2025-07-07 19:50:54 +05:00
5 changed files with 143 additions and 14 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
__pycache__ __pycache__
venv venv
static

BIN
cars.db

Binary file not shown.

View File

@ -8,11 +8,14 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . . COPY . .
# Создаем директорию для хранения БД # Создаем директории для хранения файлов
RUN mkdir -p /app/data RUN mkdir -p /app/static/images
# Указываем порт, который будет слушать приложение # Указываем порт, который будет слушать приложение
EXPOSE 3000 EXPOSE 3000
# Указываем том для хранения загруженных изображений
VOLUME /app/static/images
# Запускаем приложение # Запускаем приложение
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"] CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "3000"]

90
main.py
View File

@ -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 from sqlalchemy.orm import Session
import crud import crud
import models import models
@ -6,12 +7,17 @@ import schemas
from database import engine, get_db from database import engine, get_db
from typing import List, Optional from typing import List, Optional
import uvicorn import uvicorn
from utils import save_image, delete_image
import json
# Создание таблиц в БД # Создание таблиц в БД
models.Base.metadata.create_all(bind=engine) models.Base.metadata.create_all(bind=engine)
app = FastAPI(title="AutoBro API", description="API для управления базой данных автомобилей") app = FastAPI(title="AutoBro API", description="API для управления базой данных автомобилей")
# Добавляем обработку статических файлов
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/") @app.get("/")
def read_root(): def read_root():
return {"message": "AutoBro API"} return {"message": "AutoBro API"}
@ -40,38 +46,104 @@ def get_car(
return {"car": car} return {"car": car}
@app.post("/cars", response_model=schemas.CarResponse, status_code=status.HTTP_201_CREATED) @app.post("/cars", response_model=schemas.CarResponse, status_code=status.HTTP_201_CREATED)
def create_car( async def create_car(
car: schemas.CarCreate, car_data: str = Form(..., description="Данные автомобиля в JSON формате"),
image: UploadFile = File(None, description="Изображение автомобиля"),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
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) db_car = crud.create_car(db=db, car=car)
return {"car": db_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) @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_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) 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( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="Автомобиль не найден" detail="Автомобиль не найден"
) )
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} 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) @app.delete("/cars/{car_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_car( def delete_car(
car_id: int = Path(..., description="ID автомобиля", gt=0), car_id: int = Path(..., description="ID автомобиля", gt=0),
db: Session = Depends(get_db) 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( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="Автомобиль не найден" detail="Автомобиль не найден"
) )
# Удаляем изображение, если есть
if car.image:
delete_image(car.image)
# Удаляем запись из БД
crud.delete_car(db=db, car_id=car_id)
return None return None
if __name__ == "__main__": if __name__ == "__main__":

53
utils.py Normal file
View File

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