diff --git a/.DS_Store b/.DS_Store index c0b93f1..99840b4 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/README.md b/README.md index 9fcc0de..d6903c3 100644 --- a/README.md +++ b/README.md @@ -101,4 +101,12 @@ UPLOAD_DIRECTORY=/app/uploads ## Лицензия -[MIT License](LICENSE) \ No newline at end of file +[MIT License](LICENSE) + + + +# Сначала получаем SSL-сертификат +./init-letsencrypt.sh ваш-домен.ru + +# Затем запускаем сервисы +docker-compose -f docker-compose.prod.yml up -d \ No newline at end of file diff --git a/backend/.DS_Store b/backend/.DS_Store index ae32252..5918e07 100644 Binary files a/backend/.DS_Store and b/backend/.DS_Store differ diff --git a/backend/app/.DS_Store b/backend/app/.DS_Store index 093f4c6..b3916a8 100644 Binary files a/backend/app/.DS_Store and b/backend/app/.DS_Store differ diff --git a/backend/app/.env b/backend/app/.env index 83deb68..da639d8 100644 --- a/backend/app/.env +++ b/backend/app/.env @@ -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 \ No newline at end of file diff --git a/backend/app/__pycache__/config.cpython-310.pyc b/backend/app/__pycache__/config.cpython-310.pyc index 38f76e3..7160e95 100644 Binary files a/backend/app/__pycache__/config.cpython-310.pyc and b/backend/app/__pycache__/config.cpython-310.pyc differ diff --git a/backend/app/__pycache__/core.cpython-310.pyc b/backend/app/__pycache__/core.cpython-310.pyc index 31943ba..315f529 100644 Binary files a/backend/app/__pycache__/core.cpython-310.pyc and b/backend/app/__pycache__/core.cpython-310.pyc differ diff --git a/backend/app/__pycache__/main.cpython-310.pyc b/backend/app/__pycache__/main.cpython-310.pyc index b84afa7..9756554 100644 Binary files a/backend/app/__pycache__/main.cpython-310.pyc and b/backend/app/__pycache__/main.cpython-310.pyc differ diff --git a/backend/app/config.py b/backend/app/config.py index 316008c..3324835 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -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", "") diff --git a/backend/app/main.py b/backend/app/main.py index b6747fc..7eb0260 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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(): diff --git a/backend/app/services/__pycache__/catalog_service.cpython-310.pyc b/backend/app/services/__pycache__/catalog_service.cpython-310.pyc index 56cce42..f134e57 100644 Binary files a/backend/app/services/__pycache__/catalog_service.cpython-310.pyc and b/backend/app/services/__pycache__/catalog_service.cpython-310.pyc differ diff --git a/backend/app/services/catalog_service.py b/backend/app/services/catalog_service.py index 569b333..b1b3ffb 100644 --- a/backend/app/services/catalog_service.py +++ b/backend/app/services/catalog_service.py @@ -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": "Внутренняя ошибка сервера при удалении изображения"} # Функции для работы с размерами diff --git a/backend/requirements.txt b/backend/requirements.txt index fc8751d..04caf44 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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 diff --git a/backend/uploads/314eae61-cd89-4426-8cc9-785f2a365214.jpeg b/backend/uploads/314eae61-cd89-4426-8cc9-785f2a365214.jpeg new file mode 100644 index 0000000..d455cf3 Binary files /dev/null and b/backend/uploads/314eae61-cd89-4426-8cc9-785f2a365214.jpeg differ diff --git a/backend/uploads/3cb8aaf7-c31f-4f8d-850a-d8b145816a68.jpeg b/backend/uploads/3cb8aaf7-c31f-4f8d-850a-d8b145816a68.jpeg new file mode 100644 index 0000000..053ae69 Binary files /dev/null and b/backend/uploads/3cb8aaf7-c31f-4f8d-850a-d8b145816a68.jpeg differ diff --git a/backend/uploads/777cacd4-7ddc-472b-a9a1-c02781ec3b10.jpeg b/backend/uploads/777cacd4-7ddc-472b-a9a1-c02781ec3b10.jpeg new file mode 100644 index 0000000..b75035d Binary files /dev/null and b/backend/uploads/777cacd4-7ddc-472b-a9a1-c02781ec3b10.jpeg differ diff --git a/backend/uploads/8b724c16-d644-4678-92f5-59c385e3ec76.jpeg b/backend/uploads/8b724c16-d644-4678-92f5-59c385e3ec76.jpeg new file mode 100644 index 0000000..b0cfc9b Binary files /dev/null and b/backend/uploads/8b724c16-d644-4678-92f5-59c385e3ec76.jpeg differ diff --git a/backend/uploads/a3e5a2ff-833a-4a6c-b14a-6908b596f3e4.jpeg b/backend/uploads/a3e5a2ff-833a-4a6c-b14a-6908b596f3e4.jpeg new file mode 100644 index 0000000..7bd64c6 Binary files /dev/null and b/backend/uploads/a3e5a2ff-833a-4a6c-b14a-6908b596f3e4.jpeg differ diff --git a/backend/uploads/be067d90-03ff-4977-97a4-0f705176c797.jpeg b/backend/uploads/be067d90-03ff-4977-97a4-0f705176c797.jpeg new file mode 100644 index 0000000..de3bdac Binary files /dev/null and b/backend/uploads/be067d90-03ff-4977-97a4-0f705176c797.jpeg differ diff --git a/backend/uploads/c6bf1227-e5a1-4aa8-90f8-07f4f810706d.jpeg b/backend/uploads/c6bf1227-e5a1-4aa8-90f8-07f4f810706d.jpeg new file mode 100644 index 0000000..0b8f9a3 Binary files /dev/null and b/backend/uploads/c6bf1227-e5a1-4aa8-90f8-07f4f810706d.jpeg differ diff --git a/frontend/.DS_Store b/frontend/.DS_Store index 657964a..35db19d 100644 Binary files a/frontend/.DS_Store and b/frontend/.DS_Store differ diff --git a/frontend/lib/catalog.ts b/frontend/lib/catalog.ts index fb76da1..4e02146 100644 --- a/frontend/lib/catalog.ts +++ b/frontend/lib/catalog.ts @@ -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; } /**