добавил minio
10
README.md
@ -101,4 +101,12 @@ UPLOAD_DIRECTORY=/app/uploads
|
||||
|
||||
## Лицензия
|
||||
|
||||
[MIT License](LICENSE)
|
||||
[MIT License](LICENSE)
|
||||
|
||||
|
||||
|
||||
# Сначала получаем SSL-сертификат
|
||||
./init-letsencrypt.sh ваш-домен.ru
|
||||
|
||||
# Затем запускаем сервисы
|
||||
docker-compose -f docker-compose.prod.yml up -d
|
||||
BIN
backend/.DS_Store
vendored
BIN
backend/app/.DS_Store
vendored
@ -2,3 +2,9 @@ CDEK_LOGIN=q8AQtmLL7kPg6TuDo1eB2uqelJS4tHn2
|
||||
CDEK_PASSWORD=RmAmgvSgSl1yirlz9QupbzOJVqhCxcP5
|
||||
# CDEK_BASE_URL=https://api.cdek.ru/v2
|
||||
CDEK_BASE_URL=https://api.edu.cdek.ru/v2
|
||||
|
||||
MINIO_ENDPOINT_URL = http://45.129.128.113:9000
|
||||
MINIO_ACCESS_KEY = ZIK_DressedForSuccess
|
||||
MINIO_SECRET_KEY = ZIK_DressedForSuccess_/////ZIK_DressedForSuccess_!
|
||||
MINIO_BUCKET_NAME = dressedforsuccess
|
||||
MINIO_USE_SSL = false
|
||||
@ -27,18 +27,17 @@ MAIL_SERVER = os.getenv("MAIL_SERVER", "smtp.example.com")
|
||||
MAIL_TLS = True
|
||||
MAIL_SSL = False
|
||||
|
||||
# Настройки загрузки файлов
|
||||
# Настройки загрузки файлов (старые, для информации)
|
||||
# UPLOAD_DIRECTORY = os.getenv("UPLOAD_DIRECTORY", "uploads")
|
||||
UPLOAD_DIRECTORY = "/"
|
||||
ALLOWED_UPLOAD_EXTENSIONS = {"jpg", "jpeg", "png", "gif", "webp"}
|
||||
MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10 МБ
|
||||
|
||||
# Создаем директорию для загрузок при запуске, если её нет
|
||||
uploads_dir = Path(UPLOAD_DIRECTORY)
|
||||
uploads_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
products_dir = uploads_dir / "products"
|
||||
products_dir.mkdir(parents=True, exist_ok=True)
|
||||
# Настройки MinIO/S3
|
||||
MINIO_ENDPOINT_URL = os.getenv("MINIO_ENDPOINT_URL", "http://localhost:9000")
|
||||
MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin")
|
||||
MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin")
|
||||
MINIO_BUCKET_NAME = os.getenv("MINIO_BUCKET_NAME", "images")
|
||||
MINIO_USE_SSL = os.getenv("MINIO_USE_SSL", "false").lower() == "true"
|
||||
|
||||
# Настройки корзины
|
||||
CART_EXPIRATION_DAYS = 30 # Срок хранения корзины
|
||||
@ -71,11 +70,18 @@ class Settings(BaseSettings):
|
||||
"http://localhost:8080",
|
||||
]
|
||||
|
||||
# Настройки для загрузки файлов
|
||||
UPLOAD_DIRECTORY: str = UPLOAD_DIRECTORY
|
||||
# Старые настройки для загрузки файлов (удалить или закомментировать)
|
||||
# UPLOAD_DIRECTORY: str = UPLOAD_DIRECTORY
|
||||
MAX_UPLOAD_SIZE: int = MAX_UPLOAD_SIZE
|
||||
ALLOWED_UPLOAD_EXTENSIONS: list = list(ALLOWED_UPLOAD_EXTENSIONS)
|
||||
|
||||
# Настройки MinIO/S3
|
||||
MINIO_ENDPOINT_URL: str = MINIO_ENDPOINT_URL
|
||||
MINIO_ACCESS_KEY: str = MINIO_ACCESS_KEY
|
||||
MINIO_SECRET_KEY: str = MINIO_SECRET_KEY
|
||||
MINIO_BUCKET_NAME: str = MINIO_BUCKET_NAME
|
||||
MINIO_USE_SSL: bool = MINIO_USE_SSL
|
||||
|
||||
# Настройки для платежных систем (пример)
|
||||
PAYMENT_GATEWAY_API_KEY: str = os.getenv("PAYMENT_GATEWAY_API_KEY", "")
|
||||
PAYMENT_GATEWAY_SECRET: str = os.getenv("PAYMENT_GATEWAY_SECRET", "")
|
||||
|
||||
@ -40,13 +40,6 @@ async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError):
|
||||
# Подключаем роутеры
|
||||
app.include_router(router, prefix="/api")
|
||||
|
||||
# Создаем директорию для загрузок, если она не существует
|
||||
uploads_dir = Path(settings.UPLOAD_DIRECTORY)
|
||||
uploads_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Монтируем статические файлы
|
||||
app.mount("/uploads", StaticFiles(directory=settings.UPLOAD_DIRECTORY), name="uploads")
|
||||
|
||||
# Корневой маршрут
|
||||
@app.get("/")
|
||||
async def root():
|
||||
|
||||
@ -3,10 +3,10 @@ from fastapi import HTTPException, status, UploadFile
|
||||
from typing import List, Dict, Any, Optional
|
||||
import os
|
||||
import uuid
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import traceback
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from app.config import settings
|
||||
from app.repositories import catalog_repo, review_repo
|
||||
@ -393,7 +393,7 @@ def upload_product_image(
|
||||
alt_text: str = ""
|
||||
) -> dict:
|
||||
"""
|
||||
Загружает изображение для продукта и создает запись в базе данных.
|
||||
Загружает изображение для продукта в MinIO и создает запись в базе данных.
|
||||
|
||||
Args:
|
||||
db: Сессия базы данных
|
||||
@ -403,13 +403,15 @@ def upload_product_image(
|
||||
alt_text: Альтернативный текст для изображения
|
||||
|
||||
Returns:
|
||||
dict: Словарь с данными созданного изображения
|
||||
dict: Словарь с данными созданного изображения (image_url содержит ключ объекта)
|
||||
|
||||
Raises:
|
||||
HTTPException: В случае ошибки при загрузке или сохранении изображения
|
||||
"""
|
||||
s3_client = None
|
||||
secure_filename = ""
|
||||
try:
|
||||
logging.info(f"Попытка загрузки изображения для продукта с id={product_id}")
|
||||
logging.info(f"Попытка загрузки изображения для продукта с id={product_id} в MinIO")
|
||||
|
||||
if file is None:
|
||||
error_msg = "Файл не предоставлен"
|
||||
@ -421,60 +423,63 @@ def upload_product_image(
|
||||
logging.error(error_msg)
|
||||
raise HTTPException(status_code=400, detail=error_msg)
|
||||
|
||||
logging.info(f"Получен файл: {file.filename}, размер: {file.size if hasattr(file, 'size') else 'unknown'}")
|
||||
logging.info(f"Получен файл: {file.filename}, content_type: {file.content_type}")
|
||||
|
||||
# Исправляем импорт модели Product для ORM запроса
|
||||
from app.models.catalog_models import Product as ProductModel, ProductImage as ProductImageModel
|
||||
|
||||
# Проверяем, что продукт существует
|
||||
product = db.query(ProductModel).filter(ProductModel.id == product_id).first()
|
||||
if not product:
|
||||
error_msg = f"Продукт с ID {product_id} не найден"
|
||||
logging.error(error_msg)
|
||||
raise HTTPException(status_code=404, detail=error_msg)
|
||||
|
||||
# Создаем директорию для изображений, если ее нет
|
||||
os.makedirs(settings.UPLOAD_DIRECTORY, exist_ok=True)
|
||||
logging.info(f"Директория для загрузки: {settings.UPLOAD_DIRECTORY}")
|
||||
|
||||
# Получаем расширение файла
|
||||
file_ext = os.path.splitext(file.filename)[1].lower() if file.filename else ""
|
||||
|
||||
logging.info(f"Расширение файла: {file_ext}")
|
||||
|
||||
# Проверяем допустимость расширения (удаляем точку перед сравнением)
|
||||
if file_ext.lstrip('.') not in settings.ALLOWED_UPLOAD_EXTENSIONS:
|
||||
error_msg = f"Расширение файла {file_ext} не разрешено. Разрешенные расширения: {', '.join(settings.ALLOWED_UPLOAD_EXTENSIONS)}"
|
||||
error_msg = f"Расширение файла {file_ext} не разрешено."
|
||||
logging.error(error_msg)
|
||||
raise HTTPException(status_code=400, detail=error_msg)
|
||||
|
||||
# Генерируем безопасное имя файла
|
||||
secure_filename = f"{uuid.uuid4()}{file_ext}"
|
||||
file_path = os.path.join(settings.UPLOAD_DIRECTORY, secure_filename)
|
||||
logging.info(f"Генерация имени файла: {secure_filename}, полный путь: {file_path}")
|
||||
logging.info(f"Генерация ключа объекта MinIO: {secure_filename}")
|
||||
|
||||
# Сохраняем файл на диск
|
||||
try:
|
||||
with open(file_path, "wb") as f:
|
||||
content = file.file.read()
|
||||
if not content:
|
||||
error_msg = "Загруженный файл пуст"
|
||||
logging.error(error_msg)
|
||||
raise HTTPException(status_code=400, detail=error_msg)
|
||||
|
||||
f.write(content)
|
||||
logging.info(f"Файл успешно сохранен на диск, размер содержимого: {len(content)} байт")
|
||||
s3_client = boto3.client(
|
||||
's3',
|
||||
endpoint_url=settings.MINIO_ENDPOINT_URL,
|
||||
aws_access_key_id=settings.MINIO_ACCESS_KEY,
|
||||
aws_secret_access_key=settings.MINIO_SECRET_KEY,
|
||||
use_ssl=settings.MINIO_USE_SSL,
|
||||
config=boto3.session.Config(signature_version='s3v4')
|
||||
)
|
||||
logging.info(f"Клиент S3 инициализирован для эндпоинта: {settings.MINIO_ENDPOINT_URL}")
|
||||
except Exception as e:
|
||||
error_msg = f"Ошибка при сохранении файла: {str(e)}"
|
||||
error_msg = f"Ошибка инициализации S3 клиента: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
logging.error(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=error_msg)
|
||||
|
||||
# Получаем URL для файла
|
||||
file_url = f"/uploads/{secure_filename}"
|
||||
logging.info(f"URL файла: {file_url}")
|
||||
raise HTTPException(status_code=500, detail="Ошибка конфигурации хранилища")
|
||||
|
||||
try:
|
||||
s3_client.upload_fileobj(
|
||||
file.file,
|
||||
settings.MINIO_BUCKET_NAME,
|
||||
secure_filename,
|
||||
ExtraArgs={'ContentType': file.content_type}
|
||||
)
|
||||
logging.info(f"Файл успешно загружен в MinIO бакет '{settings.MINIO_BUCKET_NAME}' с ключом '{secure_filename}'")
|
||||
except ClientError as e:
|
||||
error_msg = f"Ошибка при загрузке файла в MinIO: {e.response['Error']['Message']}"
|
||||
logging.error(error_msg)
|
||||
logging.error(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail="Ошибка при сохранении файла в хранилище")
|
||||
except Exception as e:
|
||||
error_msg = f"Неизвестная ошибка при загрузке файла в MinIO: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
logging.error(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail="Ошибка при сохранении файла в хранилище")
|
||||
|
||||
image_object_key = secure_filename
|
||||
logging.info(f"Ключ объекта для сохранения в БД: {image_object_key}")
|
||||
|
||||
# Если это основное изображение, обновляем остальные изображения продукта
|
||||
if is_primary:
|
||||
logging.info("Обновление флагов для других изображений (is_primary=False)")
|
||||
db.query(ProductImageModel).filter(
|
||||
@ -482,11 +487,10 @@ def upload_product_image(
|
||||
ProductImageModel.is_primary == True
|
||||
).update({"is_primary": False})
|
||||
|
||||
# Создаем запись в базе данных
|
||||
logging.info("Создание записи изображения в базе данных")
|
||||
new_image = ProductImageModel(
|
||||
product_id=product_id,
|
||||
image_url=file_url,
|
||||
image_url=image_object_key,
|
||||
alt_text=alt_text,
|
||||
is_primary=is_primary
|
||||
)
|
||||
@ -497,21 +501,24 @@ def upload_product_image(
|
||||
logging.info("Запись успешно добавлена в базу данных")
|
||||
db.refresh(new_image)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
error_msg = f"Ошибка при сохранении записи в базу данных: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
logging.error(traceback.format_exc())
|
||||
|
||||
# Удаляем загруженный файл при ошибке
|
||||
logging.warning(f"Пытаемся удалить объект '{secure_filename}' из MinIO из-за ошибки БД.")
|
||||
try:
|
||||
os.remove(file_path)
|
||||
logging.info(f"Файл {file_path} успешно удален из-за ошибки при создании записи в БД")
|
||||
except:
|
||||
logging.error(f"Не удалось удалить файл {file_path} после ошибки")
|
||||
if s3_client and secure_filename:
|
||||
s3_client.delete_object(
|
||||
Bucket=settings.MINIO_BUCKET_NAME,
|
||||
Key=secure_filename
|
||||
)
|
||||
logging.info(f"Объект '{secure_filename}' успешно удален из MinIO.")
|
||||
except Exception as delete_err:
|
||||
logging.error(f"Не удалось удалить объект '{secure_filename}' из MinIO после ошибки БД: {str(delete_err)}")
|
||||
|
||||
raise HTTPException(status_code=500, detail=error_msg)
|
||||
raise HTTPException(status_code=500, detail="Ошибка сохранения данных изображения")
|
||||
|
||||
# Возвращаем данные изображения напрямую как словарь
|
||||
# вместо использования Pydantic-моделей
|
||||
result = {
|
||||
"id": new_image.id,
|
||||
"product_id": new_image.product_id,
|
||||
@ -526,14 +533,22 @@ def upload_product_image(
|
||||
return result
|
||||
|
||||
except HTTPException as http_error:
|
||||
# Пробрасываем HTTP-исключения далее
|
||||
raise http_error
|
||||
except Exception as e:
|
||||
# Логируем ошибки и преобразуем их в HTTP-исключения
|
||||
error_msg = f"Неожиданная ошибка при загрузке изображения: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
logging.error(traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=error_msg)
|
||||
if s3_client and secure_filename:
|
||||
logging.warning(f"Пытаемся удалить объект '{secure_filename}' из MinIO из-за неожиданной ошибки.")
|
||||
try:
|
||||
s3_client.delete_object(
|
||||
Bucket=settings.MINIO_BUCKET_NAME,
|
||||
Key=secure_filename
|
||||
)
|
||||
logging.info(f"Объект '{secure_filename}' успешно удален из MinIO.")
|
||||
except Exception as delete_err:
|
||||
logging.error(f"Не удалось удалить объект '{secure_filename}' из MinIO после ошибки: {str(delete_err)}")
|
||||
raise HTTPException(status_code=500, detail="Внутренняя ошибка сервера при загрузке файла")
|
||||
|
||||
|
||||
def update_product_image(db: Session, image_id: int, image: ProductImageUpdate) -> Dict[str, Any]:
|
||||
@ -596,33 +611,72 @@ def update_product_image(db: Session, image_id: int, image: ProductImageUpdate)
|
||||
|
||||
|
||||
def delete_product_image(db: Session, image_id: int) -> Dict[str, Any]:
|
||||
# Добавляем импорт ORM-модели для корректной работы с БД
|
||||
from app.models.catalog_models import ProductImage as ProductImageModel
|
||||
|
||||
# Получаем информацию об изображении перед удалением
|
||||
image = catalog_repo.get_product_image(db, image_id)
|
||||
if not image:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Изображение не найдено"
|
||||
)
|
||||
image_key_to_delete = None # Сохраняем ключ для удаления из MinIO
|
||||
s3_client = None
|
||||
|
||||
# Удаляем запись из БД
|
||||
success = catalog_repo.delete_product_image(db, image_id)
|
||||
|
||||
# Удаляем файл с диска
|
||||
if success:
|
||||
try:
|
||||
# Получаем путь к файлу из URL
|
||||
file_path = Path(settings.UPLOAD_DIRECTORY) / image.image_url.lstrip("/uploads/")
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
except Exception:
|
||||
# Если не удалось удалить файл, просто логируем ошибку
|
||||
# В реальном приложении здесь должно быть логирование
|
||||
pass
|
||||
|
||||
return {"success": success}
|
||||
try:
|
||||
# Получаем информацию об изображении перед удалением
|
||||
db_image = catalog_repo.get_product_image(db, image_id)
|
||||
if not db_image:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Изображение не найдено"
|
||||
)
|
||||
|
||||
image_key_to_delete = db_image.image_url # Получаем ключ объекта MinIO из БД
|
||||
logging.info(f"Получен ключ объекта для удаления из MinIO: {image_key_to_delete}")
|
||||
|
||||
# Удаляем запись из БД
|
||||
success = catalog_repo.delete_product_image(db, image_id)
|
||||
|
||||
# Если запись из БД удалена успешно, удаляем объект из MinIO
|
||||
if success and image_key_to_delete:
|
||||
logging.info(f"Запись из БД удалена, попытка удаления объекта '{image_key_to_delete}' из MinIO.")
|
||||
try:
|
||||
# Инициализация клиента S3
|
||||
s3_client = boto3.client(
|
||||
's3',
|
||||
endpoint_url=settings.MINIO_ENDPOINT_URL,
|
||||
aws_access_key_id=settings.MINIO_ACCESS_KEY,
|
||||
aws_secret_access_key=settings.MINIO_SECRET_KEY,
|
||||
use_ssl=settings.MINIO_USE_SSL,
|
||||
config=boto3.session.Config(signature_version='s3v4')
|
||||
)
|
||||
|
||||
s3_client.delete_object(
|
||||
Bucket=settings.MINIO_BUCKET_NAME,
|
||||
Key=image_key_to_delete
|
||||
)
|
||||
logging.info(f"Объект '{image_key_to_delete}' успешно удален из MinIO бакета '{settings.MINIO_BUCKET_NAME}'.")
|
||||
except ClientError as e:
|
||||
# Если не удалось удалить объект из MinIO, логируем ошибку, но не прерываем процесс,
|
||||
# так как запись из БД уже удалена. Возможно, потребуется ручная очистка.
|
||||
logging.error(f"Ошибка при удалении объекта '{image_key_to_delete}' из MinIO: {e.response['Error']['Message']}")
|
||||
logging.error(traceback.format_exc())
|
||||
# Можно рассмотреть вариант возврата другого статуса или сообщения об ошибке
|
||||
except Exception as e:
|
||||
logging.error(f"Неизвестная ошибка при удалении объекта '{image_key_to_delete}' из MinIO: {str(e)}")
|
||||
logging.error(traceback.format_exc())
|
||||
elif not success:
|
||||
logging.error(f"Не удалось удалить запись изображения с ID {image_id} из БД.")
|
||||
elif not image_key_to_delete:
|
||||
logging.warning(f"Ключ объекта MinIO не найден для изображения с ID {image_id}. Удаление из MinIO пропущено.")
|
||||
|
||||
return {"success": success}
|
||||
|
||||
except HTTPException as http_error:
|
||||
# Если изображение не найдено, пробрасываем ошибку
|
||||
raise http_error
|
||||
except Exception as e:
|
||||
# Общая ошибка
|
||||
error_msg = f"Неожиданная ошибка при удалении изображения с ID {image_id}: {str(e)}"
|
||||
logging.error(error_msg)
|
||||
logging.error(traceback.format_exc())
|
||||
# Важно: Не пытаемся откатить транзакцию здесь, если catalog_repo.delete_product_image уже сделал commit
|
||||
# Если catalog_repo.delete_product_image вызывает исключение до commit, rollback произойдет там.
|
||||
return {"success": False, "error": "Внутренняя ошибка сервера при удалении изображения"}
|
||||
|
||||
|
||||
# Функции для работы с размерами
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
aiofiles==24.1.0
|
||||
aiogram==3.17.0
|
||||
aiohappyeyeballs==2.4.4
|
||||
@ -106,3 +105,4 @@ python-multipart==0.0.6
|
||||
email-validator==2.1.0
|
||||
setuptools==75.8.0
|
||||
cachetools
|
||||
boto3
|
||||
|
||||
BIN
backend/uploads/314eae61-cd89-4426-8cc9-785f2a365214.jpeg
Normal file
|
After Width: | Height: | Size: 330 KiB |
BIN
backend/uploads/3cb8aaf7-c31f-4f8d-850a-d8b145816a68.jpeg
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
backend/uploads/777cacd4-7ddc-472b-a9a1-c02781ec3b10.jpeg
Normal file
|
After Width: | Height: | Size: 318 KiB |
BIN
backend/uploads/8b724c16-d644-4678-92f5-59c385e3ec76.jpeg
Normal file
|
After Width: | Height: | Size: 370 KiB |
BIN
backend/uploads/a3e5a2ff-833a-4a6c-b14a-6908b596f3e4.jpeg
Normal file
|
After Width: | Height: | Size: 400 KiB |
BIN
backend/uploads/be067d90-03ff-4977-97a4-0f705176c797.jpeg
Normal file
|
After Width: | Height: | Size: 360 KiB |
BIN
backend/uploads/c6bf1227-e5a1-4aa8-90f8-07f4f810706d.jpeg
Normal file
|
After Width: | Height: | Size: 307 KiB |
BIN
frontend/.DS_Store
vendored
@ -221,60 +221,56 @@ export interface CollectionUpdate {
|
||||
}
|
||||
|
||||
/**
|
||||
* Нормализует изображение продукта, добавляя базовый URL при необходимости
|
||||
* @param imageUrl URL изображения
|
||||
* @returns Нормализованный URL
|
||||
* Нормализует URL изображения, добавляя базовый URL MinIO при необходимости.
|
||||
* @param imageUrl Ключ объекта MinIO или полный URL.
|
||||
* @returns Полный URL изображения или плейсхолдер.
|
||||
*/
|
||||
export function normalizeProductImage(imageUrl: string | null | undefined): string {
|
||||
const minioBaseUrl = process.env.NEXT_PUBLIC_MINIO_URL;
|
||||
const placeholder = '/placeholder.jpg'; // Путь к вашему плейсхолдеру
|
||||
|
||||
if (!imageUrl) {
|
||||
if (apiStatus.debugMode) {
|
||||
// console.log('Изображение отсутствует, возвращаю placeholder');
|
||||
// console.log('Ключ/URL изображения отсутствует, возвращаю плейсхолдер');
|
||||
}
|
||||
return '/placeholder.jpg';
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
|
||||
if (imageUrl.startsWith('http') || imageUrl.startsWith('data:')) {
|
||||
// Если это уже полный URL (http, https, data URI, blob)
|
||||
if (imageUrl.startsWith('http') || imageUrl.startsWith('data:') || imageUrl.startsWith('blob:')) {
|
||||
if (apiStatus.debugMode) {
|
||||
// console.log(`Изображение уже содержит полный URL: ${imageUrl}`);
|
||||
// console.log(`Изображение уже содержит полный URL/Data URI/Blob: ${imageUrl}`);
|
||||
}
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// Если путь начинается с '/uploads', это путь к загруженному файлу
|
||||
if (imageUrl.startsWith('/uploads')) {
|
||||
result = `${PUBLIC_BASE_URL}${imageUrl}`;
|
||||
if (apiStatus.debugMode) {
|
||||
// console.log(`Обработка изображения /uploads: ${imageUrl} -> ${result}`);
|
||||
// Если это старый путь /uploads (на всякий случай, если где-то остались)
|
||||
if (imageUrl.startsWith('/uploads/')) {
|
||||
// В идеале, этот блок нужно будет удалить после миграции всех данных
|
||||
const oldUrl = `${PUBLIC_BASE_URL}${imageUrl}`;
|
||||
if (apiStatus.debugMode) {
|
||||
// console.warn(`Обнаружен старый формат URL /uploads/: ${imageUrl}. Формируется URL: ${oldUrl}`);
|
||||
}
|
||||
return result;
|
||||
return oldUrl;
|
||||
}
|
||||
|
||||
// Если путь начинается с '/', это относительный путь
|
||||
if (imageUrl.startsWith('/')) {
|
||||
result = `${PUBLIC_BASE_URL}${imageUrl}`;
|
||||
// Если есть базовый URL MinIO и imageUrl не пустой - формируем полный URL
|
||||
if (minioBaseUrl && typeof imageUrl === 'string' && imageUrl.trim() !== '') {
|
||||
// Убираем возможный слэш в конце базового URL и в начале ключа
|
||||
const cleanBaseUrl = minioBaseUrl.endsWith('/') ? minioBaseUrl.slice(0, -1) : minioBaseUrl;
|
||||
const cleanImageUrl = imageUrl.startsWith('/') ? imageUrl.slice(1) : imageUrl;
|
||||
const fullUrl = `${cleanBaseUrl}/${cleanImageUrl}`;
|
||||
if (apiStatus.debugMode) {
|
||||
// console.log(`Обработка относительного пути: ${imageUrl} -> ${result}`);
|
||||
// console.log(`Формирование URL MinIO: ${minioBaseUrl} + ${imageUrl} -> ${fullUrl}`);
|
||||
}
|
||||
return result;
|
||||
return fullUrl;
|
||||
}
|
||||
|
||||
// Для всех остальных случаев (например, 'uploads/...')
|
||||
if (imageUrl.startsWith('uploads/')) {
|
||||
result = `${PUBLIC_BASE_URL}/${imageUrl}`;
|
||||
if (apiStatus.debugMode) {
|
||||
// console.log(`Обработка пути uploads: ${imageUrl} -> ${result}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Во всех остальных случаях, предполагаем что это путь к загруженному файлу
|
||||
result = `${PUBLIC_BASE_URL}/uploads/${imageUrl}`;
|
||||
// Если базовый URL MinIO не задан или ключ пустой, возвращаем плейсхолдер
|
||||
if (apiStatus.debugMode) {
|
||||
// console.log(`Обработка произвольного пути: ${imageUrl} -> ${result}`);
|
||||
// console.warn(`Не удалось сформировать URL MinIO (базовый URL: ${minioBaseUrl}, ключ: ${imageUrl}). Возвращаю плейсхолдер.`);
|
||||
}
|
||||
return result;
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||