Update files
This commit is contained in:
parent
1cdfd57cdf
commit
678783d3be
|
|
@ -0,0 +1,60 @@
|
||||||
|
# Server Configuration
|
||||||
|
NODE_ENV=production
|
||||||
|
PORT=3000
|
||||||
|
|
||||||
|
# Database Configuration (удаленный сервер)
|
||||||
|
MONGODB_URI=mongodb://103.80.87.247:27017/nakama
|
||||||
|
|
||||||
|
# JWT Secrets
|
||||||
|
JWT_SECRET=your_jwt_secret_change_me_32chars_minimum
|
||||||
|
JWT_ACCESS_SECRET=your_access_secret_change_me_32chars
|
||||||
|
JWT_REFRESH_SECRET=your_refresh_secret_change_me_32chars
|
||||||
|
JWT_ACCESS_EXPIRES_IN=300
|
||||||
|
JWT_REFRESH_EXPIRES_IN=604800
|
||||||
|
|
||||||
|
# Telegram Bot Configuration
|
||||||
|
TELEGRAM_BOT_TOKEN=your_telegram_bot_token
|
||||||
|
MODERATION_BOT_TOKEN=your_moderation_bot_token
|
||||||
|
MODERATION_OWNER_USERNAMES=glpshchn00
|
||||||
|
MODERATION_CHANNEL_USERNAME=@reichenbfurry
|
||||||
|
|
||||||
|
# Gelbooru API
|
||||||
|
GELBOORU_API_KEY=your_gelbooru_api_key
|
||||||
|
GELBOORU_USER_ID=your_gelbooru_user_id
|
||||||
|
|
||||||
|
# Frontend URL
|
||||||
|
FRONTEND_URL=http://localhost:5173
|
||||||
|
VITE_API_URL=http://localhost:3000/api
|
||||||
|
|
||||||
|
# CORS Configuration
|
||||||
|
CORS_ORIGIN=*
|
||||||
|
|
||||||
|
# Redis (optional)
|
||||||
|
REDIS_URL=
|
||||||
|
|
||||||
|
# MinIO Configuration (S3-compatible object storage)
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=103.80.87.247 # IP вашего MinIO сервера
|
||||||
|
MINIO_PORT=9000 # API порт (обычно 9000, консоль на 9901)
|
||||||
|
MINIO_USE_SSL=false
|
||||||
|
MINIO_ACCESS_KEY=minioadmin # Получите из MinIO Console
|
||||||
|
MINIO_SECRET_KEY=minioadmin # Получите из MinIO Console
|
||||||
|
MINIO_BUCKET=nakama-media
|
||||||
|
MINIO_REGION=us-east-1
|
||||||
|
MINIO_PUBLIC_URL= # Опционально: CDN URL
|
||||||
|
MINIO_PUBLIC_BUCKET=false
|
||||||
|
|
||||||
|
# File Upload (fallback для локального хранилища)
|
||||||
|
MAX_FILE_SIZE=10485760
|
||||||
|
UPLOADS_DIR=uploads
|
||||||
|
|
||||||
|
# Rate Limiting
|
||||||
|
RATE_LIMIT_GENERAL=100
|
||||||
|
RATE_LIMIT_POSTS=10
|
||||||
|
RATE_LIMIT_INTERACTIONS=20
|
||||||
|
|
||||||
|
# Cache TTL (seconds)
|
||||||
|
CACHE_TTL_POSTS=300
|
||||||
|
CACHE_TTL_USERS=600
|
||||||
|
CACHE_TTL_SEARCH=180
|
||||||
|
|
||||||
|
|
@ -0,0 +1,347 @@
|
||||||
|
# ✅ MinIO интеграция завершена!
|
||||||
|
|
||||||
|
## 🎉 Что было сделано
|
||||||
|
|
||||||
|
### 1. Добавлен MinIO клиент
|
||||||
|
- ✅ Установлен пакет `minio` в package.json
|
||||||
|
- ✅ Создана утилита `/backend/utils/minio.js` с полным API
|
||||||
|
- ✅ Поддержка загрузки, удаления, получения URL файлов
|
||||||
|
|
||||||
|
### 2. Создан универсальный middleware загрузки
|
||||||
|
- ✅ `/backend/middleware/upload.js` - автоматически выбирает MinIO или локальное хранилище
|
||||||
|
- ✅ Поддержка изображений и видео
|
||||||
|
- ✅ Валидация типов файлов
|
||||||
|
- ✅ Автоматическая очистка при ошибках
|
||||||
|
|
||||||
|
### 3. Обновлены роуты
|
||||||
|
- ✅ `/backend/routes/posts.js` - использует новый middleware
|
||||||
|
- ✅ `/backend/routes/modApp.js` - публикация в канал через MinIO
|
||||||
|
- ✅ Fallback на локальное хранилище если MinIO недоступен
|
||||||
|
|
||||||
|
### 4. Обновлена конфигурация
|
||||||
|
- ✅ `/backend/config/index.js` - добавлены MinIO настройки
|
||||||
|
- ✅ `/backend/server.js` - автоматическая инициализация MinIO
|
||||||
|
- ✅ `docker-compose.yml` - добавлен MinIO сервис
|
||||||
|
|
||||||
|
### 5. Создана документация
|
||||||
|
- ✅ `MINIO_SETUP.md` - полное руководство по настройке
|
||||||
|
- ✅ `ENV_EXAMPLE.txt` - пример конфигурации
|
||||||
|
- ✅ Инструкции по миграции существующих файлов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### Шаг 1: Установите зависимости
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/glpshchn/Desktop/nakama
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 2: Обновите .env файл
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавьте MinIO настройки:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# MinIO Configuration
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=minio
|
||||||
|
MINIO_PORT=9000
|
||||||
|
MINIO_USE_SSL=false
|
||||||
|
MINIO_ACCESS_KEY=minioadmin
|
||||||
|
MINIO_SECRET_KEY=your_secure_password_here
|
||||||
|
MINIO_BUCKET=nakama-media
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 3: Запустите Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
docker-compose build
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 4: Проверьте MinIO
|
||||||
|
|
||||||
|
Откройте в браузере:
|
||||||
|
- **MinIO Console:** http://localhost:9001
|
||||||
|
- **Логин:** minioadmin / your_secure_password_here
|
||||||
|
|
||||||
|
### Шаг 5: Создайте тестовый пост
|
||||||
|
|
||||||
|
Создайте пост с изображением в приложении. Файл автоматически загрузится в MinIO!
|
||||||
|
|
||||||
|
Проверьте в MinIO Console:
|
||||||
|
- Object Browser → nakama-media → posts/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Варианты использования
|
||||||
|
|
||||||
|
### Вариант 1: MinIO в Docker (для начала)
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- ✅ Быстрая настройка
|
||||||
|
- ✅ Всё в одном месте
|
||||||
|
- ✅ Удобно для разработки
|
||||||
|
|
||||||
|
**Настройка:**
|
||||||
|
```env
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=minio # Имя сервиса в Docker
|
||||||
|
MINIO_PORT=9000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Вариант 2: MinIO на отдельном сервере (рекомендуется)
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- ✅ Централизованное хранилище
|
||||||
|
- ✅ Легко масштабировать
|
||||||
|
- ✅ Независимость от основного сервера
|
||||||
|
|
||||||
|
**Настройка:**
|
||||||
|
```bash
|
||||||
|
# На сервере 103.80.87.247 установите MinIO
|
||||||
|
# (см. MINIO_SETUP.md раздел "Отдельный сервер")
|
||||||
|
|
||||||
|
# В .env приложения:
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=103.80.87.247
|
||||||
|
MINIO_PORT=9000
|
||||||
|
MINIO_USE_SSL=false
|
||||||
|
MINIO_ACCESS_KEY=nakama_app
|
||||||
|
MINIO_SECRET_KEY=secure_key_here
|
||||||
|
MINIO_BUCKET=nakama-media
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Вариант 3: MinIO + CDN (для продакшена)
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- ✅ Максимальная производительность
|
||||||
|
- ✅ Глобальное кэширование
|
||||||
|
- ✅ Экономия трафика
|
||||||
|
|
||||||
|
**Настройка:**
|
||||||
|
```env
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=103.80.87.247
|
||||||
|
MINIO_PORT=9000
|
||||||
|
MINIO_USE_SSL=true
|
||||||
|
MINIO_ACCESS_KEY=nakama_app
|
||||||
|
MINIO_SECRET_KEY=secure_key_here
|
||||||
|
MINIO_BUCKET=nakama-media
|
||||||
|
MINIO_PUBLIC_URL=https://cdn.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Миграция существующих файлов
|
||||||
|
|
||||||
|
Если у вас уже есть файлы в `backend/uploads/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установите MinIO Client
|
||||||
|
wget https://dl.min.io/client/mc/release/linux-amd64/mc
|
||||||
|
chmod +x mc
|
||||||
|
|
||||||
|
# Настройте подключение
|
||||||
|
./mc alias set myminio http://localhost:9000 minioadmin your_password
|
||||||
|
|
||||||
|
# Синхронизируйте файлы
|
||||||
|
./mc mirror backend/uploads/posts myminio/nakama-media/posts/
|
||||||
|
./mc mirror backend/uploads/avatars myminio/nakama-media/avatars/
|
||||||
|
|
||||||
|
# Проверьте
|
||||||
|
./mc ls myminio/nakama-media/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Как это работает
|
||||||
|
|
||||||
|
### До (локальное хранилище):
|
||||||
|
```
|
||||||
|
Пользователь загружает фото
|
||||||
|
↓
|
||||||
|
Multer сохраняет в backend/uploads/
|
||||||
|
↓
|
||||||
|
URL: /uploads/posts/12345.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### После (с MinIO):
|
||||||
|
```
|
||||||
|
Пользователь загружает фото
|
||||||
|
↓
|
||||||
|
Multer → buffer в памяти
|
||||||
|
↓
|
||||||
|
MinIO middleware загружает в S3
|
||||||
|
↓
|
||||||
|
URL: http://minio:9000/nakama-media/posts/12345.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fallback (если MinIO недоступен):
|
||||||
|
```
|
||||||
|
Пользователь загружает фото
|
||||||
|
↓
|
||||||
|
Multer → buffer в памяти
|
||||||
|
↓
|
||||||
|
MinIO недоступен → fallback
|
||||||
|
↓
|
||||||
|
Сохранение в backend/uploads/
|
||||||
|
↓
|
||||||
|
URL: /uploads/posts/12345.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛡️ Безопасность
|
||||||
|
|
||||||
|
### Важно изменить для продакшена:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# ❌ НЕ используйте в продакшене:
|
||||||
|
MINIO_ACCESS_KEY=minioadmin
|
||||||
|
MINIO_SECRET_KEY=minioadmin
|
||||||
|
|
||||||
|
# ✅ Используйте:
|
||||||
|
MINIO_ACCESS_KEY=nakama_app_$(openssl rand -hex 8)
|
||||||
|
MINIO_SECRET_KEY=$(openssl rand -hex 32)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Настройка HTTPS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# На сервере MinIO:
|
||||||
|
mkdir -p ~/.minio/certs
|
||||||
|
cp cert.pem ~/.minio/certs/public.crt
|
||||||
|
cp key.pem ~/.minio/certs/private.key
|
||||||
|
systemctl restart minio
|
||||||
|
```
|
||||||
|
|
||||||
|
```env
|
||||||
|
# В .env:
|
||||||
|
MINIO_USE_SSL=true
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Мониторинг
|
||||||
|
|
||||||
|
### Проверить подключение:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# В логах backend:
|
||||||
|
docker-compose logs backend | grep -i minio
|
||||||
|
|
||||||
|
# Должны увидеть:
|
||||||
|
# ✅ MinIO подключен: minio:9000
|
||||||
|
# Bucket: nakama-media
|
||||||
|
```
|
||||||
|
|
||||||
|
### Веб-консоль MinIO:
|
||||||
|
|
||||||
|
1. Откройте: http://localhost:9001
|
||||||
|
2. Мониторинг → Metrics
|
||||||
|
3. Просмотр файлов: Object Browser → nakama-media
|
||||||
|
|
||||||
|
### Статистика через API:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// В коде backend:
|
||||||
|
const { getBucketStats } = require('./utils/minio');
|
||||||
|
|
||||||
|
const stats = await getBucketStats();
|
||||||
|
console.log(stats);
|
||||||
|
// {
|
||||||
|
// totalFiles: 1234,
|
||||||
|
// totalSize: 52428800,
|
||||||
|
// totalSizeMB: "50.00",
|
||||||
|
// bucket: "nakama-media"
|
||||||
|
// }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Устранение проблем
|
||||||
|
|
||||||
|
### Проблема: "MinIO недоступен"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверьте статус контейнера
|
||||||
|
docker-compose ps minio
|
||||||
|
|
||||||
|
# Проверьте логи
|
||||||
|
docker-compose logs minio
|
||||||
|
|
||||||
|
# Перезапустите
|
||||||
|
docker-compose restart minio
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблема: "Bucket не найден"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Войдите в MinIO Console
|
||||||
|
http://localhost:9001
|
||||||
|
|
||||||
|
# Object Browser → Create Bucket
|
||||||
|
# Имя: nakama-media
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблема: "Access Denied"
|
||||||
|
|
||||||
|
Проверьте credentials в .env:
|
||||||
|
```bash
|
||||||
|
docker-compose logs backend | grep MINIO
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Структура файлов в MinIO
|
||||||
|
|
||||||
|
```
|
||||||
|
nakama-media/ ← Bucket
|
||||||
|
├── posts/ ← Посты пользователей
|
||||||
|
│ ├── 1700000000-123.jpg
|
||||||
|
│ ├── 1700000001-456.png
|
||||||
|
│ └── ...
|
||||||
|
├── avatars/ ← Аватары (будущее)
|
||||||
|
│ └── ...
|
||||||
|
└── channel/ ← Публикации в канал
|
||||||
|
├── 1700000002-789.jpg
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 Готово!
|
||||||
|
|
||||||
|
Теперь все медиа файлы автоматически сохраняются в MinIO!
|
||||||
|
|
||||||
|
**Что дальше:**
|
||||||
|
1. Прочитайте `MINIO_SETUP.md` для детальной настройки
|
||||||
|
2. Измените стандартные credentials
|
||||||
|
3. Настройте HTTPS для продакшена
|
||||||
|
4. Настройте резервное копирование
|
||||||
|
5. Рассмотрите использование CDN
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Полезные ссылки
|
||||||
|
|
||||||
|
- **MinIO Documentation:** https://min.io/docs/minio/linux/index.html
|
||||||
|
- **MinIO Client (mc):** https://min.io/docs/minio/linux/reference/minio-mc.html
|
||||||
|
- **S3 API Reference:** https://docs.aws.amazon.com/s3/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Вопросы?** Смотрите `MINIO_SETUP.md` для подробной документации!
|
||||||
|
|
||||||
|
|
@ -0,0 +1,500 @@
|
||||||
|
# 🗄️ Настройка MinIO для Nakama
|
||||||
|
|
||||||
|
## Что такое MinIO?
|
||||||
|
|
||||||
|
MinIO - это высокопроизводительное объектное хранилище, совместимое с Amazon S3 API. Оно идеально подходит для хранения медиа файлов в распределенных системах.
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- ✅ S3-совместимый API
|
||||||
|
- ✅ Высокая производительность
|
||||||
|
- ✅ Встроенное резервное копирование
|
||||||
|
- ✅ Веб-консоль для управления
|
||||||
|
- ✅ Масштабируемость
|
||||||
|
- ✅ Open Source
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### Вариант 1: С Docker Compose (рекомендуется)
|
||||||
|
|
||||||
|
MinIO уже включен в `docker-compose.yml`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Обновите .env файл
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавьте MinIO настройки:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# MinIO Configuration
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=minio # В Docker используется имя сервиса
|
||||||
|
MINIO_PORT=9000
|
||||||
|
MINIO_USE_SSL=false
|
||||||
|
MINIO_ACCESS_KEY=minioadmin # Измените на свой
|
||||||
|
MINIO_SECRET_KEY=minioadmin_secure_pwd # Измените на свой
|
||||||
|
MINIO_BUCKET=nakama-media
|
||||||
|
MINIO_PUBLIC_URL= # Оставьте пустым или укажите CDN URL
|
||||||
|
```
|
||||||
|
|
||||||
|
Запустите:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверьте:
|
||||||
|
- MinIO API: http://localhost:9000
|
||||||
|
- MinIO Console: http://localhost:9001
|
||||||
|
- Логин: minioadmin / minioadmin_secure_pwd
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Вариант 2: Отдельный сервер MinIO (103.80.87.247)
|
||||||
|
|
||||||
|
#### Установка на удаленном сервере:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Подключитесь к серверу
|
||||||
|
ssh root@103.80.87.247
|
||||||
|
|
||||||
|
# Скачайте MinIO
|
||||||
|
wget https://dl.min.io/server/minio/release/linux-amd64/minio
|
||||||
|
chmod +x minio
|
||||||
|
mv minio /usr/local/bin/
|
||||||
|
|
||||||
|
# Создайте директорию для данных
|
||||||
|
mkdir -p /var/minio/data
|
||||||
|
|
||||||
|
# Создайте systemd сервис
|
||||||
|
nano /etc/systemd/system/minio.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавьте в файл:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=MinIO
|
||||||
|
Documentation=https://min.io/docs/minio/linux/index.html
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
AssertFileIsExecutable=/usr/local/bin/minio
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/usr/local/
|
||||||
|
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
ProtectProc=invisible
|
||||||
|
|
||||||
|
EnvironmentFile=-/etc/default/minio
|
||||||
|
ExecStartPre=/bin/bash -c "if [ -z \"${MINIO_VOLUMES}\" ]; then echo \"Variable MINIO_VOLUMES not set in /etc/default/minio\"; exit 1; fi"
|
||||||
|
ExecStart=/usr/local/bin/minio server $MINIO_OPTS $MINIO_VOLUMES
|
||||||
|
|
||||||
|
# MinIO RELEASE.2023-05-04T21-44-30Z adds support for Type=notify (https://www.freedesktop.org/software/systemd/man/systemd.service.html#Type=)
|
||||||
|
# This may improve systemctl setups where other services use `After=minio.server`
|
||||||
|
# Uncomment the line to enable the functionality
|
||||||
|
# Type=notify
|
||||||
|
|
||||||
|
# Let systemd restart this service always
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
# Specifies the maximum file descriptor number that can be opened by this process
|
||||||
|
LimitNOFILE=65536
|
||||||
|
|
||||||
|
# Specifies the maximum number of threads this process can create
|
||||||
|
TasksMax=infinity
|
||||||
|
|
||||||
|
# Disable timeout logic and wait until process is stopped
|
||||||
|
TimeoutStopSec=infinity
|
||||||
|
SendSIGKILL=no
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Создайте файл конфигурации:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano /etc/default/minio
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавьте:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# MinIO local volumes configuration
|
||||||
|
MINIO_VOLUMES="/var/minio/data"
|
||||||
|
|
||||||
|
# MinIO root credentials
|
||||||
|
MINIO_ROOT_USER=minioadmin
|
||||||
|
MINIO_ROOT_PASSWORD=your_secure_password_here
|
||||||
|
|
||||||
|
# MinIO options
|
||||||
|
MINIO_OPTS="--console-address :9001"
|
||||||
|
```
|
||||||
|
|
||||||
|
Запустите MinIO:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl enable minio
|
||||||
|
systemctl start minio
|
||||||
|
systemctl status minio
|
||||||
|
```
|
||||||
|
|
||||||
|
Откройте порты:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ufw allow 9000/tcp # API
|
||||||
|
ufw allow 9001/tcp # Console
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Обновите .env на сервере приложения:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=103.80.87.247
|
||||||
|
MINIO_PORT=9000
|
||||||
|
MINIO_USE_SSL=false
|
||||||
|
MINIO_ACCESS_KEY=minioadmin
|
||||||
|
MINIO_SECRET_KEY=your_secure_password_here
|
||||||
|
MINIO_BUCKET=nakama-media
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Настройка через веб-консоль
|
||||||
|
|
||||||
|
1. Откройте: http://localhost:9001 (или http://103.80.87.247:9001)
|
||||||
|
2. Войдите с учетными данными (minioadmin / your_password)
|
||||||
|
3. Создайте bucket:
|
||||||
|
- Object Browser → Create Bucket
|
||||||
|
- Имя: `nakama-media`
|
||||||
|
- Создайте
|
||||||
|
|
||||||
|
4. Настройте публичный доступ (опционально):
|
||||||
|
- Выберите bucket → Access → Add Access Rule
|
||||||
|
- Prefix: `*`
|
||||||
|
- Access: `readonly`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Создание отдельного пользователя (рекомендуется)
|
||||||
|
|
||||||
|
В MinIO Console:
|
||||||
|
|
||||||
|
1. **Identity → Users → Create User**
|
||||||
|
- Access Key: `nakama_app`
|
||||||
|
- Secret Key: `secure_secret_key_here`
|
||||||
|
|
||||||
|
2. **Identity → Policies → Create Policy**
|
||||||
|
|
||||||
|
Имя: `nakama-media-policy`
|
||||||
|
|
||||||
|
Policy JSON:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"s3:GetObject",
|
||||||
|
"s3:PutObject",
|
||||||
|
"s3:DeleteObject",
|
||||||
|
"s3:ListBucket"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::nakama-media",
|
||||||
|
"arn:aws:s3:::nakama-media/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Назначьте policy пользователю**
|
||||||
|
- Identity → Users → nakama_app
|
||||||
|
- Policies → Assign Policy → nakama-media-policy
|
||||||
|
|
||||||
|
4. **Обновите .env:**
|
||||||
|
```env
|
||||||
|
MINIO_ACCESS_KEY=nakama_app
|
||||||
|
MINIO_SECRET_KEY=secure_secret_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Проверка работы
|
||||||
|
|
||||||
|
### Тест 1: Создание поста с изображением
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# В приложении создайте пост с изображением
|
||||||
|
# Проверьте в MinIO Console: Object Browser → nakama-media → posts/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Тест 2: Через MinIO Client (mc)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установите mc
|
||||||
|
wget https://dl.min.io/client/mc/release/linux-amd64/mc
|
||||||
|
chmod +x mc
|
||||||
|
mv mc /usr/local/bin/
|
||||||
|
|
||||||
|
# Настройте alias
|
||||||
|
mc alias set nakama http://103.80.87.247:9000 minioadmin your_password
|
||||||
|
|
||||||
|
# Проверьте bucket
|
||||||
|
mc ls nakama/nakama-media
|
||||||
|
|
||||||
|
# Загрузите тестовый файл
|
||||||
|
mc cp test.jpg nakama/nakama-media/test/
|
||||||
|
|
||||||
|
# Удалите файл
|
||||||
|
mc rm nakama/nakama-media/test/test.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Тест 3: Через API (curl)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Получить список объектов
|
||||||
|
curl -X GET \
|
||||||
|
http://localhost:9000/nakama-media/ \
|
||||||
|
--user minioadmin:your_password
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Миграция существующих файлов в MinIO
|
||||||
|
|
||||||
|
Если у вас уже есть файлы в `backend/uploads/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# На сервере с файлами
|
||||||
|
cd /path/to/nakama
|
||||||
|
|
||||||
|
# Установите mc
|
||||||
|
wget https://dl.min.io/client/mc/release/linux-amd64/mc
|
||||||
|
chmod +x mc
|
||||||
|
|
||||||
|
# Настройте подключение
|
||||||
|
./mc alias set nakama http://103.80.87.247:9000 minioadmin your_password
|
||||||
|
|
||||||
|
# Синхронизируйте файлы
|
||||||
|
./mc mirror backend/uploads/posts nakama/nakama-media/posts/
|
||||||
|
./mc mirror backend/uploads/avatars nakama/nakama-media/avatars/
|
||||||
|
|
||||||
|
# Проверьте
|
||||||
|
./mc ls nakama/nakama-media/posts/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Безопасность
|
||||||
|
|
||||||
|
### 1. Измените стандартные учетные данные
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# В /etc/default/minio:
|
||||||
|
MINIO_ROOT_USER=your_admin_username
|
||||||
|
MINIO_ROOT_PASSWORD=very_secure_password_123
|
||||||
|
|
||||||
|
# Перезапустите
|
||||||
|
systemctl restart minio
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Настройте HTTPS (рекомендуется для продакшена)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создайте директорию для сертификатов
|
||||||
|
mkdir -p /root/.minio/certs
|
||||||
|
|
||||||
|
# Скопируйте SSL сертификаты
|
||||||
|
cp cert.pem /root/.minio/certs/public.crt
|
||||||
|
cp key.pem /root/.minio/certs/private.key
|
||||||
|
|
||||||
|
# Перезапустите MinIO
|
||||||
|
systemctl restart minio
|
||||||
|
```
|
||||||
|
|
||||||
|
Обновите .env:
|
||||||
|
```env
|
||||||
|
MINIO_USE_SSL=true
|
||||||
|
MINIO_PUBLIC_URL=https://minio.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Firewall
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Разрешить только с IP приложения
|
||||||
|
ufw allow from YOUR_APP_SERVER_IP to any port 9000
|
||||||
|
|
||||||
|
# Или ограничить консоль
|
||||||
|
ufw allow from YOUR_IP to any port 9001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 Мониторинг
|
||||||
|
|
||||||
|
### Prometheus метрики
|
||||||
|
|
||||||
|
MinIO поддерживает Prometheus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Метрики доступны на:
|
||||||
|
curl http://localhost:9000/minio/v2/metrics/cluster
|
||||||
|
```
|
||||||
|
|
||||||
|
### Веб-консоль
|
||||||
|
|
||||||
|
Мониторинг в реальном времени:
|
||||||
|
- Monitoring → Metrics
|
||||||
|
- Bandwidth
|
||||||
|
- Storage Usage
|
||||||
|
- API Calls
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Устранение проблем
|
||||||
|
|
||||||
|
### Проблема: "MinIO недоступен"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверьте статус
|
||||||
|
systemctl status minio
|
||||||
|
|
||||||
|
# Проверьте логи
|
||||||
|
journalctl -u minio -f
|
||||||
|
|
||||||
|
# Проверьте подключение
|
||||||
|
telnet 103.80.87.247 9000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблема: "Bucket does not exist"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создайте через mc
|
||||||
|
mc mb nakama/nakama-media
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблема: "Access Denied"
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверьте credentials
|
||||||
|
mc admin user list nakama
|
||||||
|
|
||||||
|
# Проверьте policy
|
||||||
|
mc admin policy info nakama nakama-media-policy
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Резервное копирование MinIO
|
||||||
|
|
||||||
|
### Автоматический бекап с mc
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Создайте скрипт
|
||||||
|
nano /usr/local/bin/backup-minio.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
BACKUP_DIR="/var/backups/minio"
|
||||||
|
DATE=$(date +"%Y-%m-%d_%H-%M-%S")
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
# Синхронизировать все файлы
|
||||||
|
/usr/local/bin/mc mirror nakama/nakama-media "$BACKUP_DIR/$DATE/"
|
||||||
|
|
||||||
|
# Удалить старые бекапы (> 30 дней)
|
||||||
|
find "$BACKUP_DIR" -type d -mtime +30 -exec rm -rf {} \;
|
||||||
|
|
||||||
|
echo "Backup completed: $DATE"
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x /usr/local/bin/backup-minio.sh
|
||||||
|
|
||||||
|
# Добавьте в cron (еженедельно)
|
||||||
|
crontab -e
|
||||||
|
# Добавьте: 0 3 * * 0 /usr/local/bin/backup-minio.sh >> /var/log/minio-backup.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Конфигурация для разных сценариев
|
||||||
|
|
||||||
|
### Локальная разработка:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=localhost
|
||||||
|
MINIO_PORT=9000
|
||||||
|
MINIO_USE_SSL=false
|
||||||
|
MINIO_ACCESS_KEY=minioadmin
|
||||||
|
MINIO_SECRET_KEY=minioadmin
|
||||||
|
MINIO_BUCKET=nakama-media-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Продакшен с одним сервером:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=103.80.87.247
|
||||||
|
MINIO_PORT=9000
|
||||||
|
MINIO_USE_SSL=false
|
||||||
|
MINIO_ACCESS_KEY=nakama_app
|
||||||
|
MINIO_SECRET_KEY=secure_key_here
|
||||||
|
MINIO_BUCKET=nakama-media
|
||||||
|
```
|
||||||
|
|
||||||
|
### Продакшен с CDN:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=103.80.87.247
|
||||||
|
MINIO_PORT=9000
|
||||||
|
MINIO_USE_SSL=false
|
||||||
|
MINIO_ACCESS_KEY=nakama_app
|
||||||
|
MINIO_SECRET_KEY=secure_key_here
|
||||||
|
MINIO_BUCKET=nakama-media
|
||||||
|
MINIO_PUBLIC_URL=https://cdn.yourdomain.com # Cloudflare/другой CDN
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Рекомендации
|
||||||
|
|
||||||
|
1. **Безопасность:**
|
||||||
|
- Измените стандартные credentials
|
||||||
|
- Используйте HTTPS в продакшене
|
||||||
|
- Настройте firewall
|
||||||
|
- Создайте отдельного пользователя для приложения
|
||||||
|
|
||||||
|
2. **Производительность:**
|
||||||
|
- Используйте CDN для раздачи файлов
|
||||||
|
- Настройте кэширование
|
||||||
|
- Включите compression
|
||||||
|
|
||||||
|
3. **Надежность:**
|
||||||
|
- Настройте резервное копирование
|
||||||
|
- Мониторьте место на диске
|
||||||
|
- Регулярно проверяйте integrity
|
||||||
|
|
||||||
|
4. **Масштабирование:**
|
||||||
|
- Рассмотрите distributed mode для больших нагрузок
|
||||||
|
- Используйте lifecycle policies для старых файлов
|
||||||
|
- Настройте репликацию между серверами
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**MinIO готов к использованию!** 🚀
|
||||||
|
|
||||||
|
Файлы автоматически будут загружаться в MinIO при создании постов и публикациях в канал.
|
||||||
|
|
||||||
|
|
@ -0,0 +1,438 @@
|
||||||
|
# 🔌 Подключение к существующему MinIO через S3 SDK
|
||||||
|
|
||||||
|
## ✅ Ваша ситуация
|
||||||
|
|
||||||
|
У вас уже запущен MinIO на сервере **103.80.87.247**:
|
||||||
|
- **Console (Web UI):** http://103.80.87.247:9901/
|
||||||
|
- **API (S3):** http://103.80.87.247:9000/ (обычно)
|
||||||
|
|
||||||
|
Мы используем **AWS S3 SDK** для подключения к MinIO (MinIO полностью совместим с S3 API).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Быстрая настройка
|
||||||
|
|
||||||
|
### Шаг 1: Установите зависимости
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/glpshchn/Desktop/nakama
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Будут установлены:
|
||||||
|
- `@aws-sdk/client-s3` - S3 клиент
|
||||||
|
- `@aws-sdk/lib-storage` - Загрузка больших файлов
|
||||||
|
- `@aws-sdk/s3-request-presigner` - Presigned URLs
|
||||||
|
|
||||||
|
### Шаг 2: Получите Access Key и Secret Key
|
||||||
|
|
||||||
|
1. Откройте MinIO Console: http://103.80.87.247:9901/
|
||||||
|
2. Войдите с учетными данными
|
||||||
|
3. Перейдите: **Identity → Service Accounts** (или **Users**)
|
||||||
|
4. Создайте новый Service Account для приложения:
|
||||||
|
- Name: `nakama-app`
|
||||||
|
- Policy: `readwrite`
|
||||||
|
5. **Скопируйте Access Key и Secret Key** (покажутся только один раз!)
|
||||||
|
|
||||||
|
### Шаг 3: Обновите .env файл
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nano /Users/glpshchn/Desktop/nakama/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавьте/обновите:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# MinIO Configuration
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=103.80.87.247
|
||||||
|
MINIO_PORT=9000 # API порт (НЕ 9901!)
|
||||||
|
MINIO_USE_SSL=false
|
||||||
|
MINIO_ACCESS_KEY=YOUR_ACCESS_KEY_HERE # Из MinIO Console
|
||||||
|
MINIO_SECRET_KEY=YOUR_SECRET_KEY_HERE # Из MinIO Console
|
||||||
|
MINIO_BUCKET=nakama-media
|
||||||
|
MINIO_REGION=us-east-1
|
||||||
|
MINIO_PUBLIC_URL=http://103.80.87.247:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 4: Создайте bucket в MinIO
|
||||||
|
|
||||||
|
В MinIO Console:
|
||||||
|
1. **Object Browser** → **Create Bucket**
|
||||||
|
2. Имя: `nakama-media`
|
||||||
|
3. Нажмите **Create Bucket**
|
||||||
|
|
||||||
|
Или через API:
|
||||||
|
```bash
|
||||||
|
curl -X PUT http://103.80.87.247:9000/nakama-media \
|
||||||
|
-H "Authorization: AWS4-HMAC-SHA256 ..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 5: Запустите приложение
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose down
|
||||||
|
docker-compose build
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверьте логи:
|
||||||
|
```bash
|
||||||
|
docker-compose logs backend | grep -i minio
|
||||||
|
|
||||||
|
# Должны увидеть:
|
||||||
|
# ✅ S3 клиент для MinIO инициализирован
|
||||||
|
# Bucket: nakama-media
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Проверка подключения
|
||||||
|
|
||||||
|
### Тест 1: Через API endpoint
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Проверьте статус MinIO (нужен токен модератора)
|
||||||
|
curl -X GET http://localhost:3000/api/minio/status \
|
||||||
|
-H "Authorization: Bearer YOUR_MODERATOR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Тест 2: Создайте пост с изображением
|
||||||
|
|
||||||
|
1. Откройте приложение
|
||||||
|
2. Создайте пост с изображением
|
||||||
|
3. Проверьте в MinIO Console: **Object Browser → nakama-media → posts/**
|
||||||
|
|
||||||
|
### Тест 3: Через AWS CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установите AWS CLI
|
||||||
|
# macOS:
|
||||||
|
brew install awscli
|
||||||
|
|
||||||
|
# Ubuntu:
|
||||||
|
sudo apt install awscli
|
||||||
|
|
||||||
|
# Настройте profile для MinIO
|
||||||
|
aws configure --profile minio
|
||||||
|
# AWS Access Key ID: ваш_access_key
|
||||||
|
# AWS Secret Access Key: ваш_secret_key
|
||||||
|
# Default region name: us-east-1
|
||||||
|
# Default output format: json
|
||||||
|
|
||||||
|
# Проверьте подключение
|
||||||
|
aws s3 ls s3://nakama-media \
|
||||||
|
--endpoint-url http://103.80.87.247:9000 \
|
||||||
|
--profile minio
|
||||||
|
|
||||||
|
# Загрузите тестовый файл
|
||||||
|
aws s3 cp test.jpg s3://nakama-media/test/ \
|
||||||
|
--endpoint-url http://103.80.87.247:9000 \
|
||||||
|
--profile minio
|
||||||
|
|
||||||
|
# Список файлов
|
||||||
|
aws s3 ls s3://nakama-media/posts/ \
|
||||||
|
--endpoint-url http://103.80.87.247:9000 \
|
||||||
|
--profile minio
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Конфигурация для разных сценариев
|
||||||
|
|
||||||
|
### Вариант 1: HTTP (без SSL)
|
||||||
|
|
||||||
|
```env
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=103.80.87.247
|
||||||
|
MINIO_PORT=9000
|
||||||
|
MINIO_USE_SSL=false
|
||||||
|
MINIO_ACCESS_KEY=your_access_key
|
||||||
|
MINIO_SECRET_KEY=your_secret_key
|
||||||
|
MINIO_BUCKET=nakama-media
|
||||||
|
MINIO_PUBLIC_URL=http://103.80.87.247:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант 2: HTTPS (с SSL)
|
||||||
|
|
||||||
|
```env
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=103.80.87.247
|
||||||
|
MINIO_PORT=9000
|
||||||
|
MINIO_USE_SSL=true
|
||||||
|
MINIO_ACCESS_KEY=your_access_key
|
||||||
|
MINIO_SECRET_KEY=your_secret_key
|
||||||
|
MINIO_BUCKET=nakama-media
|
||||||
|
MINIO_PUBLIC_URL=https://103.80.87.247:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант 3: Через домен + CDN
|
||||||
|
|
||||||
|
```env
|
||||||
|
MINIO_ENABLED=true
|
||||||
|
MINIO_ENDPOINT=minio.yourdomain.com
|
||||||
|
MINIO_PORT=443
|
||||||
|
MINIO_USE_SSL=true
|
||||||
|
MINIO_ACCESS_KEY=your_access_key
|
||||||
|
MINIO_SECRET_KEY=your_secret_key
|
||||||
|
MINIO_BUCKET=nakama-media
|
||||||
|
MINIO_PUBLIC_URL=https://cdn.yourdomain.com
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Безопасность
|
||||||
|
|
||||||
|
### 1. Создайте отдельного пользователя для приложения
|
||||||
|
|
||||||
|
В MinIO Console:
|
||||||
|
|
||||||
|
**Identity → Users → Create User:**
|
||||||
|
- Username: `nakama-app`
|
||||||
|
- Password: `secure_password_123`
|
||||||
|
|
||||||
|
**Создайте Policy:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"s3:GetObject",
|
||||||
|
"s3:PutObject",
|
||||||
|
"s3:DeleteObject"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::nakama-media/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"s3:ListBucket"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::nakama-media"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Назначьте Policy пользователю.**
|
||||||
|
|
||||||
|
**Создайте Service Account для пользователя** и используйте его credentials в .env.
|
||||||
|
|
||||||
|
### 2. Ограничьте доступ к API порту
|
||||||
|
|
||||||
|
На сервере MinIO:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Разрешить доступ только с IP приложения
|
||||||
|
ufw allow from YOUR_APP_SERVER_IP to any port 9000
|
||||||
|
|
||||||
|
# Консоль можно ограничить вашим IP
|
||||||
|
ufw allow from YOUR_IP to any port 9901
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Настройте HTTPS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# На сервере MinIO:
|
||||||
|
mkdir -p ~/.minio/certs
|
||||||
|
|
||||||
|
# Скопируйте SSL сертификаты
|
||||||
|
cp /path/to/cert.pem ~/.minio/certs/public.crt
|
||||||
|
cp /path/to/key.pem ~/.minio/certs/private.key
|
||||||
|
|
||||||
|
# Перезапустите MinIO
|
||||||
|
systemctl restart minio
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Отличия S3 SDK от MinIO SDK
|
||||||
|
|
||||||
|
### MinIO SDK (старый):
|
||||||
|
```javascript
|
||||||
|
const Minio = require('minio');
|
||||||
|
const client = new Minio.Client({
|
||||||
|
endPoint: '103.80.87.247',
|
||||||
|
port: 9000,
|
||||||
|
useSSL: false,
|
||||||
|
accessKey: 'key',
|
||||||
|
secretKey: 'secret'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### AWS S3 SDK (новый, используем):
|
||||||
|
```javascript
|
||||||
|
const { S3Client } = require('@aws-sdk/client-s3');
|
||||||
|
const client = new S3Client({
|
||||||
|
endpoint: 'http://103.80.87.247:9000',
|
||||||
|
region: 'us-east-1',
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: 'key',
|
||||||
|
secretAccessKey: 'secret'
|
||||||
|
},
|
||||||
|
forcePathStyle: true // Важно для MinIO!
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Преимущества S3 SDK:**
|
||||||
|
- ✅ Официальный AWS SDK (лучше поддержка)
|
||||||
|
- ✅ Работает с любым S3-совместимым хранилищем
|
||||||
|
- ✅ Больше функций и опций
|
||||||
|
- ✅ Лучшая типизация для TypeScript
|
||||||
|
- ✅ Модульная структура (меньше размер bundle)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Структура хранения
|
||||||
|
|
||||||
|
```
|
||||||
|
MinIO Server (103.80.87.247:9000)
|
||||||
|
│
|
||||||
|
└── nakama-media/ ← Bucket
|
||||||
|
├── posts/ ← Посты пользователей
|
||||||
|
│ ├── 1700000000-123.jpg
|
||||||
|
│ ├── 1700000001-456.png
|
||||||
|
│ └── ...
|
||||||
|
├── avatars/ ← Аватары
|
||||||
|
│ └── ...
|
||||||
|
└── channel/ ← Публикации в канал
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 Решение проблем
|
||||||
|
|
||||||
|
### Проблема: "Connection refused" на порту 9000
|
||||||
|
|
||||||
|
**Причина:** MinIO API не слушает на порту 9000
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
```bash
|
||||||
|
# На сервере MinIO проверьте:
|
||||||
|
netstat -tulpn | grep 9000
|
||||||
|
|
||||||
|
# Если пусто, проверьте конфигурацию MinIO
|
||||||
|
systemctl status minio
|
||||||
|
|
||||||
|
# Проверьте переменные окружения
|
||||||
|
cat /etc/default/minio
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблема: "Access Denied"
|
||||||
|
|
||||||
|
**Причина:** Неверные credentials или недостаточно прав
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
1. Проверьте Access Key и Secret Key в .env
|
||||||
|
2. Проверьте policy пользователя в MinIO Console
|
||||||
|
3. Убедитесь что bucket существует
|
||||||
|
|
||||||
|
### Проблема: "Bucket does not exist"
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
```bash
|
||||||
|
# Создайте bucket через AWS CLI:
|
||||||
|
aws s3 mb s3://nakama-media \
|
||||||
|
--endpoint-url http://103.80.87.247:9000 \
|
||||||
|
--profile minio
|
||||||
|
|
||||||
|
# Или в MinIO Console:
|
||||||
|
# Object Browser → Create Bucket → nakama-media
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблема: "forcePathStyle" не работает
|
||||||
|
|
||||||
|
**Причина:** Старая версия MinIO или неправильный endpoint
|
||||||
|
|
||||||
|
**Решение:**
|
||||||
|
```env
|
||||||
|
# Убедитесь что endpoint БЕЗ протокола в config:
|
||||||
|
MINIO_ENDPOINT=103.80.87.247 # ✅ Правильно
|
||||||
|
MINIO_ENDPOINT=http://103.80.87.247 # ❌ Неправильно
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблема: CORS ошибки при доступе к файлам
|
||||||
|
|
||||||
|
**Решение:** Настройте CORS в MinIO Console
|
||||||
|
```bash
|
||||||
|
# Через mc (MinIO Client):
|
||||||
|
mc admin config set myminio api cors_allow_origin="*"
|
||||||
|
mc admin service restart myminio
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Пример использования в коде
|
||||||
|
|
||||||
|
### Загрузка файла:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { uploadFile } = require('./utils/minio');
|
||||||
|
|
||||||
|
// В route handler:
|
||||||
|
const fileUrl = await uploadFile(
|
||||||
|
req.file.buffer, // Buffer из multer
|
||||||
|
req.file.originalname,
|
||||||
|
req.file.mimetype,
|
||||||
|
'posts' // Папка
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('File URL:', fileUrl);
|
||||||
|
// http://103.80.87.247:9000/nakama-media/posts/1700000000-123.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Удаление файла:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { deleteFile } = require('./utils/minio');
|
||||||
|
|
||||||
|
await deleteFile('http://103.80.87.247:9000/nakama-media/posts/1700000000-123.jpg');
|
||||||
|
```
|
||||||
|
|
||||||
|
### Получение presigned URL:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { getPresignedUrl } = require('./utils/minio');
|
||||||
|
|
||||||
|
const url = await getPresignedUrl('posts/1700000000-123.jpg', 3600); // 1 час
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist настройки
|
||||||
|
|
||||||
|
- [ ] MinIO работает на 103.80.87.247
|
||||||
|
- [ ] Console доступен на :9901
|
||||||
|
- [ ] API доступен на :9000
|
||||||
|
- [ ] Создан bucket `nakama-media`
|
||||||
|
- [ ] Созданы Access Key и Secret Key
|
||||||
|
- [ ] Обновлен .env с правильными credentials
|
||||||
|
- [ ] Установлены npm пакеты (`npm install`)
|
||||||
|
- [ ] Перезапущен Docker (`docker-compose up -d`)
|
||||||
|
- [ ] Проверены логи (`docker-compose logs backend`)
|
||||||
|
- [ ] Создан тестовый пост с изображением
|
||||||
|
- [ ] Файл появился в MinIO Console
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Следующие шаги
|
||||||
|
|
||||||
|
1. ✅ **Проверьте подключение:** создайте пост с изображением
|
||||||
|
2. 🔒 **Настройте безопасность:** создайте отдельного пользователя
|
||||||
|
3. 🌐 **Настройте домен:** вместо IP используйте домен
|
||||||
|
4. 🔐 **Включите HTTPS:** для продакшена
|
||||||
|
5. 📊 **Настройте мониторинг:** следите за использованием
|
||||||
|
6. 💾 **Настройте бекапы:** регулярное резервное копирование
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Готово!** Теперь все файлы загружаются в ваш MinIO через S3 SDK! 🚀
|
||||||
|
|
||||||
|
|
@ -69,6 +69,20 @@ module.exports = {
|
||||||
search: parseInt(process.env.CACHE_TTL_SEARCH || '180') // 3 мин
|
search: parseInt(process.env.CACHE_TTL_SEARCH || '180') // 3 мин
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// MinIO Configuration
|
||||||
|
minio: {
|
||||||
|
enabled: process.env.MINIO_ENABLED === 'true',
|
||||||
|
endpoint: process.env.MINIO_ENDPOINT || '103.80.87.247',
|
||||||
|
port: parseInt(process.env.MINIO_PORT || '9000', 10),
|
||||||
|
useSSL: process.env.MINIO_USE_SSL === 'true',
|
||||||
|
accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
|
||||||
|
secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin',
|
||||||
|
bucket: process.env.MINIO_BUCKET || 'nakama-media',
|
||||||
|
region: process.env.MINIO_REGION || 'us-east-1',
|
||||||
|
publicUrl: process.env.MINIO_PUBLIC_URL || '', // Кастомный URL (CDN)
|
||||||
|
publicBucket: process.env.MINIO_PUBLIC_BUCKET === 'true'
|
||||||
|
},
|
||||||
|
|
||||||
// Проверки
|
// Проверки
|
||||||
isDevelopment: () => process.env.NODE_ENV === 'development',
|
isDevelopment: () => process.env.NODE_ENV === 'development',
|
||||||
isProduction: () => process.env.NODE_ENV === 'production',
|
isProduction: () => process.env.NODE_ENV === 'production',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
const multer = require('multer');
|
||||||
|
const path = require('path');
|
||||||
|
const { uploadFile, isEnabled: isMinioEnabled } = require('../utils/minio');
|
||||||
|
const { log } = require('./logger');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
// Временное хранилище для файлов
|
||||||
|
const tempStorage = multer.memoryStorage();
|
||||||
|
|
||||||
|
// Конфигурация multer
|
||||||
|
const multerConfig = {
|
||||||
|
storage: tempStorage,
|
||||||
|
limits: {
|
||||||
|
fileSize: 10 * 1024 * 1024, // 10MB
|
||||||
|
files: 10
|
||||||
|
},
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
// Запрещенные расширения (исполняемые файлы)
|
||||||
|
const forbiddenExts = [
|
||||||
|
'.exe', '.bat', '.cmd', '.sh', '.ps1', '.js', '.jar',
|
||||||
|
'.app', '.dmg', '.deb', '.rpm', '.msi', '.scr',
|
||||||
|
'.vbs', '.com', '.pif', '.cpl'
|
||||||
|
];
|
||||||
|
const ext = path.extname(file.originalname).toLowerCase();
|
||||||
|
|
||||||
|
// Проверить на запрещенные расширения
|
||||||
|
if (forbiddenExts.includes(ext)) {
|
||||||
|
return cb(new Error('Запрещенный тип файла'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Разрешенные типы изображений и видео
|
||||||
|
const allowedMimes = [
|
||||||
|
'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp',
|
||||||
|
'video/mp4', 'video/quicktime', 'video/x-msvideo'
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!allowedMimes.includes(file.mimetype)) {
|
||||||
|
return cb(new Error('Только изображения и видео разрешены'));
|
||||||
|
}
|
||||||
|
|
||||||
|
cb(null, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware для загрузки файлов
|
||||||
|
* Автоматически загружает в MinIO если включен, иначе локально
|
||||||
|
*/
|
||||||
|
function createUploadMiddleware(fieldName, maxCount = 5, folder = 'posts') {
|
||||||
|
const upload = multer(multerConfig);
|
||||||
|
const multerMiddleware = maxCount === 1
|
||||||
|
? upload.single(fieldName)
|
||||||
|
: upload.array(fieldName, maxCount);
|
||||||
|
|
||||||
|
return async (req, res, next) => {
|
||||||
|
multerMiddleware(req, res, async (err) => {
|
||||||
|
if (err) {
|
||||||
|
log('error', 'Ошибка multer', { error: err.message });
|
||||||
|
return res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверить наличие файлов
|
||||||
|
const files = req.files || (req.file ? [req.file] : []);
|
||||||
|
|
||||||
|
if (!files.length) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если MinIO включен, загрузить туда
|
||||||
|
if (isMinioEnabled()) {
|
||||||
|
const uploadedUrls = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const fileUrl = await uploadFile(
|
||||||
|
file.buffer,
|
||||||
|
file.originalname,
|
||||||
|
file.mimetype,
|
||||||
|
folder
|
||||||
|
);
|
||||||
|
uploadedUrls.push(fileUrl);
|
||||||
|
} catch (uploadError) {
|
||||||
|
log('error', 'Ошибка загрузки в MinIO', {
|
||||||
|
error: uploadError.message,
|
||||||
|
filename: file.originalname
|
||||||
|
});
|
||||||
|
throw uploadError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохранить URLs в req для дальнейшей обработки
|
||||||
|
req.uploadedFiles = uploadedUrls;
|
||||||
|
req.uploadMethod = 'minio';
|
||||||
|
|
||||||
|
log('info', 'Файлы загружены в MinIO', {
|
||||||
|
count: uploadedUrls.length,
|
||||||
|
folder
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Локальное хранилище (fallback)
|
||||||
|
const uploadDir = path.join(__dirname, '../uploads', folder);
|
||||||
|
|
||||||
|
// Создать директорию если не существует
|
||||||
|
if (!fs.existsSync(uploadDir)) {
|
||||||
|
fs.mkdirSync(uploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadedPaths = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const random = Math.round(Math.random() * 1E9);
|
||||||
|
const ext = path.extname(file.originalname);
|
||||||
|
const filename = `${timestamp}-${random}${ext}`;
|
||||||
|
const filepath = path.join(uploadDir, filename);
|
||||||
|
|
||||||
|
// Сохранить файл
|
||||||
|
fs.writeFileSync(filepath, file.buffer);
|
||||||
|
|
||||||
|
// Относительный путь для URL
|
||||||
|
const relativePath = `/uploads/${folder}/${filename}`;
|
||||||
|
uploadedPaths.push(relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
req.uploadedFiles = uploadedPaths;
|
||||||
|
req.uploadMethod = 'local';
|
||||||
|
|
||||||
|
log('info', 'Файлы загружены локально', {
|
||||||
|
count: uploadedPaths.length,
|
||||||
|
folder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
log('error', 'Ошибка обработки загруженных файлов', { error: error.message });
|
||||||
|
return res.status(500).json({ error: 'Ошибка загрузки файлов' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware для удаления файлов из MinIO при ошибке
|
||||||
|
*/
|
||||||
|
function cleanupOnError() {
|
||||||
|
return (err, req, res, next) => {
|
||||||
|
if (req.uploadedFiles && req.uploadMethod === 'minio') {
|
||||||
|
const { deleteFiles } = require('../utils/minio');
|
||||||
|
deleteFiles(req.uploadedFiles).catch(cleanupErr => {
|
||||||
|
log('error', 'Ошибка очистки файлов MinIO', { error: cleanupErr.message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
next(err);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createUploadMiddleware,
|
||||||
|
cleanupOnError,
|
||||||
|
|
||||||
|
// Готовые middleware для разных случаев
|
||||||
|
uploadPostImages: createUploadMiddleware('images', 5, 'posts'),
|
||||||
|
uploadAvatar: createUploadMiddleware('avatar', 1, 'avatars'),
|
||||||
|
uploadChannelMedia: createUploadMiddleware('images', 10, 'channel')
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const { checkConnection, getBucketStats } = require('../utils/minio');
|
||||||
|
const { authenticate, requireModerator } = require('../middleware/auth');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить подключение к MinIO (только для модераторов)
|
||||||
|
*/
|
||||||
|
router.get('/status', authenticate, requireModerator, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const isConnected = await checkConnection();
|
||||||
|
|
||||||
|
if (!isConnected) {
|
||||||
|
return res.status(503).json({
|
||||||
|
connected: false,
|
||||||
|
message: 'MinIO недоступен'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = await getBucketStats();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
connected: true,
|
||||||
|
stats,
|
||||||
|
message: 'MinIO работает корректно'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({
|
||||||
|
connected: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
|
|
||||||
|
|
@ -2,10 +2,11 @@ const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const multer = require('multer');
|
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { authenticateModeration } = require('../middleware/auth');
|
const { authenticateModeration } = require('../middleware/auth');
|
||||||
const { logSecurityEvent } = require('../middleware/logger');
|
const { logSecurityEvent } = require('../middleware/logger');
|
||||||
|
const { uploadChannelMedia, cleanupOnError } = require('../middleware/upload');
|
||||||
|
const { deleteFile } = require('../utils/minio');
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
const Post = require('../models/Post');
|
const Post = require('../models/Post');
|
||||||
const Report = require('../models/Report');
|
const Report = require('../models/Report');
|
||||||
|
|
@ -15,26 +16,6 @@ const { listAdmins, isModerationAdmin, normalizeUsername } = require('../service
|
||||||
const { sendChannelMediaGroup, sendMessageToUser } = require('../bots/serverMonitor');
|
const { sendChannelMediaGroup, sendMessageToUser } = require('../bots/serverMonitor');
|
||||||
const config = require('../config');
|
const config = require('../config');
|
||||||
|
|
||||||
const TEMP_DIR = path.join(__dirname, '../uploads/mod-channel');
|
|
||||||
if (!fs.existsSync(TEMP_DIR)) {
|
|
||||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const upload = multer({
|
|
||||||
storage: multer.diskStorage({
|
|
||||||
destination: TEMP_DIR,
|
|
||||||
filename: (_req, file, cb) => {
|
|
||||||
const unique = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
|
|
||||||
const ext = path.extname(file.originalname || '');
|
|
||||||
cb(null, `${unique}${ext || '.jpg'}`);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
limits: {
|
|
||||||
files: 10,
|
|
||||||
fileSize: 15 * 1024 * 1024 // 15MB
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const OWNER_USERNAMES = new Set(config.moderationOwnerUsernames || []);
|
const OWNER_USERNAMES = new Set(config.moderationOwnerUsernames || []);
|
||||||
|
|
||||||
const requireModerationAccess = async (req, res, next) => {
|
const requireModerationAccess = async (req, res, next) => {
|
||||||
|
|
@ -672,7 +653,7 @@ router.post(
|
||||||
'/channel/publish',
|
'/channel/publish',
|
||||||
authenticateModeration,
|
authenticateModeration,
|
||||||
requireModerationAccess,
|
requireModerationAccess,
|
||||||
upload.array('images', 10),
|
uploadChannelMedia,
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const { description = '', tags } = req.body;
|
const { description = '', tags } = req.body;
|
||||||
const files = req.files || [];
|
const files = req.files || [];
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,17 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const multer = require('multer');
|
|
||||||
const path = require('path');
|
|
||||||
const fs = require('fs');
|
|
||||||
const { authenticate } = require('../middleware/auth');
|
const { authenticate } = require('../middleware/auth');
|
||||||
const { postCreationLimiter, interactionLimiter } = require('../middleware/rateLimiter');
|
const { postCreationLimiter, interactionLimiter } = require('../middleware/rateLimiter');
|
||||||
const { searchLimiter } = require('../middleware/rateLimiter');
|
const { searchLimiter } = require('../middleware/rateLimiter');
|
||||||
const { validatePostContent, validateTags, validateImageUrl } = require('../middleware/validator');
|
const { validatePostContent, validateTags, validateImageUrl } = require('../middleware/validator');
|
||||||
const { logSecurityEvent } = require('../middleware/logger');
|
const { logSecurityEvent } = require('../middleware/logger');
|
||||||
const { strictPostLimiter, fileUploadLimiter } = require('../middleware/security');
|
const { strictPostLimiter, fileUploadLimiter } = require('../middleware/security');
|
||||||
|
const { uploadPostImages, cleanupOnError } = require('../middleware/upload');
|
||||||
|
const { deleteFiles } = require('../utils/minio');
|
||||||
const Post = require('../models/Post');
|
const Post = require('../models/Post');
|
||||||
const Notification = require('../models/Notification');
|
const Notification = require('../models/Notification');
|
||||||
const { extractHashtags } = require('../utils/hashtags');
|
const { extractHashtags } = require('../utils/hashtags');
|
||||||
|
|
||||||
// Настройка multer для загрузки изображений
|
|
||||||
const storage = multer.diskStorage({
|
|
||||||
destination: (req, file, cb) => {
|
|
||||||
const dir = path.join(__dirname, '../uploads/posts');
|
|
||||||
if (!fs.existsSync(dir)) {
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
cb(null, dir);
|
|
||||||
},
|
|
||||||
filename: (req, file, cb) => {
|
|
||||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
||||||
cb(null, uniqueSuffix + path.extname(file.originalname));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const upload = multer({
|
|
||||||
storage: storage,
|
|
||||||
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
|
||||||
fileFilter: (req, file, cb) => {
|
|
||||||
// Запрещенные расширения (исполняемые файлы)
|
|
||||||
const forbiddenExts = ['.exe', '.bat', '.cmd', '.sh', '.ps1', '.js', '.jar', '.app', '.dmg', '.deb', '.rpm', '.msi', '.scr', '.vbs', '.com', '.pif', '.cpl'];
|
|
||||||
const ext = path.extname(file.originalname).toLowerCase();
|
|
||||||
|
|
||||||
// Проверить на запрещенные расширения
|
|
||||||
if (forbiddenExts.includes(ext)) {
|
|
||||||
return cb(new Error('Запрещенный тип файла'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Разрешенные типы изображений
|
|
||||||
const allowedTypes = /jpeg|jpg|png|gif|webp/;
|
|
||||||
const extname = allowedTypes.test(ext);
|
|
||||||
const mimetype = allowedTypes.test(file.mimetype);
|
|
||||||
|
|
||||||
// Дополнительная проверка MIME типа
|
|
||||||
const allowedMimes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
|
||||||
if (!allowedMimes.includes(file.mimetype)) {
|
|
||||||
return cb(new Error('Только изображения разрешены'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mimetype && extname) {
|
|
||||||
return cb(null, true);
|
|
||||||
} else {
|
|
||||||
cb(new Error('Только изображения разрешены'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Поддержка до 5 изображений в одном посте
|
|
||||||
const uploadMultiple = upload.array('images', 5);
|
|
||||||
|
|
||||||
// Получить ленту постов
|
// Получить ленту постов
|
||||||
router.get('/', authenticate, async (req, res) => {
|
router.get('/', authenticate, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -113,7 +62,7 @@ router.get('/', authenticate, async (req, res) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Создать пост
|
// Создать пост
|
||||||
router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploadLimiter, uploadMultiple, async (req, res) => {
|
router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploadLimiter, uploadPostImages, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { content, tags, mentionedUsers, isNSFW, externalImages } = req.body;
|
const { content, tags, mentionedUsers, isNSFW, externalImages } = req.body;
|
||||||
|
|
||||||
|
|
@ -146,9 +95,9 @@ router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploa
|
||||||
// Обработка изображений
|
// Обработка изображений
|
||||||
let images = [];
|
let images = [];
|
||||||
|
|
||||||
// Загруженные файлы
|
// Загруженные файлы (через middleware)
|
||||||
if (req.files && req.files.length > 0) {
|
if (req.uploadedFiles && req.uploadedFiles.length > 0) {
|
||||||
images = req.files.map(file => `/uploads/posts/${file.filename}`);
|
images = req.uploadedFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Внешние изображения (из поиска)
|
// Внешние изображения (из поиска)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ dotenv.config({ path: path.join(__dirname, '.env') });
|
||||||
const { generalLimiter } = require('./middleware/rateLimiter');
|
const { generalLimiter } = require('./middleware/rateLimiter');
|
||||||
const { initRedis } = require('./utils/redis');
|
const { initRedis } = require('./utils/redis');
|
||||||
const { initWebSocket } = require('./websocket');
|
const { initWebSocket } = require('./websocket');
|
||||||
|
const { initMinioClient, checkConnection: checkMinioConnection } = require('./utils/minio');
|
||||||
const config = require('./config');
|
const config = require('./config');
|
||||||
|
|
||||||
// Security middleware
|
// Security middleware
|
||||||
|
|
@ -150,14 +151,34 @@ app.get('/health', (req, res) => {
|
||||||
|
|
||||||
// MongoDB подключение
|
// MongoDB подключение
|
||||||
mongoose.connect(config.mongoUri)
|
mongoose.connect(config.mongoUri)
|
||||||
.then(() => {
|
.then(async () => {
|
||||||
console.log(`✅ MongoDB подключена: ${config.mongoUri.replace(/\/\/.*@/, '//***@')}`);
|
console.log(`✅ MongoDB подключена: ${config.mongoUri.replace(/\/\/.*@/, '//***@')}`);
|
||||||
|
|
||||||
// Инициализировать Redis (опционально)
|
// Инициализировать Redis (опционально)
|
||||||
if (config.redisUrl) {
|
if (config.redisUrl) {
|
||||||
initRedis().catch(err => console.log('⚠️ Redis недоступен, работаем без кэша'));
|
initRedis().catch(err => console.log('⚠️ Redis недоступен, работаем без кэша'));
|
||||||
} else {
|
} else {
|
||||||
console.log('ℹ️ Redis не настроен, кэширование отключено');
|
console.log('ℹ️ Redis не настроен, кэширование отключено');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Инициализировать MinIO (опционально)
|
||||||
|
if (config.minio.enabled) {
|
||||||
|
try {
|
||||||
|
initMinioClient();
|
||||||
|
const minioOk = await checkMinioConnection();
|
||||||
|
if (minioOk) {
|
||||||
|
console.log(`✅ MinIO подключен: ${config.minio.endpoint}:${config.minio.port}`);
|
||||||
|
console.log(` Bucket: ${config.minio.bucket}`);
|
||||||
|
} else {
|
||||||
|
console.log('⚠️ MinIO недоступен, используется локальное хранилище');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log('⚠️ MinIO ошибка инициализации:', err.message);
|
||||||
|
console.log(' Используется локальное хранилище');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('ℹ️ MinIO отключен, используется локальное хранилище');
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(err => console.error('❌ Ошибка MongoDB:', err));
|
.catch(err => console.error('❌ Ошибка MongoDB:', err));
|
||||||
|
|
||||||
|
|
@ -172,6 +193,7 @@ app.use('/api/moderation', require('./routes/moderation'));
|
||||||
app.use('/api/statistics', require('./routes/statistics'));
|
app.use('/api/statistics', require('./routes/statistics'));
|
||||||
app.use('/api/bot', require('./routes/bot'));
|
app.use('/api/bot', require('./routes/bot'));
|
||||||
app.use('/api/mod-app', require('./routes/modApp'));
|
app.use('/api/mod-app', require('./routes/modApp'));
|
||||||
|
app.use('/api/minio', require('./routes/minio-test'));
|
||||||
|
|
||||||
// Базовый роут
|
// Базовый роут
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,358 @@
|
||||||
|
const { S3Client, PutObjectCommand, DeleteObjectCommand, HeadBucketCommand, CreateBucketCommand, ListObjectsV2Command, PutBucketPolicyCommand } = require('@aws-sdk/client-s3');
|
||||||
|
const { Upload } = require('@aws-sdk/lib-storage');
|
||||||
|
const config = require('../config');
|
||||||
|
const { log } = require('../middleware/logger');
|
||||||
|
|
||||||
|
let s3Client = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализация S3 клиента для MinIO
|
||||||
|
*/
|
||||||
|
function initMinioClient() {
|
||||||
|
if (!config.minio.enabled) {
|
||||||
|
log('info', 'MinIO отключен, используется локальное хранилище');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const endpoint = config.minio.useSSL
|
||||||
|
? `https://${config.minio.endpoint}:${config.minio.port}`
|
||||||
|
: `http://${config.minio.endpoint}:${config.minio.port}`;
|
||||||
|
|
||||||
|
s3Client = new S3Client({
|
||||||
|
endpoint: endpoint,
|
||||||
|
region: config.minio.region || 'us-east-1',
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: config.minio.accessKey,
|
||||||
|
secretAccessKey: config.minio.secretKey
|
||||||
|
},
|
||||||
|
forcePathStyle: true, // Важно для MinIO!
|
||||||
|
tls: config.minio.useSSL
|
||||||
|
});
|
||||||
|
|
||||||
|
log('info', 'S3 клиент для MinIO инициализирован', {
|
||||||
|
endpoint: endpoint,
|
||||||
|
bucket: config.minio.bucket,
|
||||||
|
region: config.minio.region
|
||||||
|
});
|
||||||
|
|
||||||
|
// Создать bucket если не существует
|
||||||
|
ensureBucket();
|
||||||
|
|
||||||
|
return s3Client;
|
||||||
|
} catch (error) {
|
||||||
|
log('error', 'Ошибка инициализации S3 клиента', { error: error.message });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Убедиться что bucket существует
|
||||||
|
*/
|
||||||
|
async function ensureBucket() {
|
||||||
|
if (!s3Client) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверить существование bucket
|
||||||
|
try {
|
||||||
|
await s3Client.send(new HeadBucketCommand({
|
||||||
|
Bucket: config.minio.bucket
|
||||||
|
}));
|
||||||
|
log('info', `Bucket ${config.minio.bucket} существует`);
|
||||||
|
} catch (headError) {
|
||||||
|
// Bucket не существует, создаем
|
||||||
|
if (headError.name === 'NotFound' || headError.$metadata?.httpStatusCode === 404) {
|
||||||
|
await s3Client.send(new CreateBucketCommand({
|
||||||
|
Bucket: config.minio.bucket
|
||||||
|
}));
|
||||||
|
log('info', `Bucket ${config.minio.bucket} создан`);
|
||||||
|
|
||||||
|
// Установить публичную политику для bucket (опционально)
|
||||||
|
if (config.minio.publicBucket) {
|
||||||
|
const policy = {
|
||||||
|
Version: '2012-10-17',
|
||||||
|
Statement: [{
|
||||||
|
Effect: 'Allow',
|
||||||
|
Principal: { AWS: ['*'] },
|
||||||
|
Action: ['s3:GetObject'],
|
||||||
|
Resource: [`arn:aws:s3:::${config.minio.bucket}/*`]
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
await s3Client.send(new PutBucketPolicyCommand({
|
||||||
|
Bucket: config.minio.bucket,
|
||||||
|
Policy: JSON.stringify(policy)
|
||||||
|
}));
|
||||||
|
log('info', `Bucket ${config.minio.bucket} установлен как публичный`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw headError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log('error', 'Ошибка проверки/создания bucket', { error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить файл в MinIO через S3 SDK
|
||||||
|
* @param {Buffer} buffer - Буфер файла
|
||||||
|
* @param {string} filename - Имя файла
|
||||||
|
* @param {string} contentType - MIME тип
|
||||||
|
* @param {string} folder - Папка в bucket (например, 'posts', 'avatars')
|
||||||
|
* @returns {Promise<string>} - URL файла
|
||||||
|
*/
|
||||||
|
async function uploadFile(buffer, filename, contentType, folder = 'posts') {
|
||||||
|
if (!s3Client) {
|
||||||
|
throw new Error('S3 клиент не инициализирован');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Генерировать уникальное имя файла
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const random = Math.round(Math.random() * 1E9);
|
||||||
|
const ext = filename.split('.').pop();
|
||||||
|
const objectName = `${folder}/${timestamp}-${random}.${ext}`;
|
||||||
|
|
||||||
|
// Загрузить файл через S3 SDK
|
||||||
|
const upload = new Upload({
|
||||||
|
client: s3Client,
|
||||||
|
params: {
|
||||||
|
Bucket: config.minio.bucket,
|
||||||
|
Key: objectName,
|
||||||
|
Body: buffer,
|
||||||
|
ContentType: contentType,
|
||||||
|
CacheControl: 'public, max-age=31536000', // 1 год
|
||||||
|
Metadata: {
|
||||||
|
originalname: filename,
|
||||||
|
uploadedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await upload.done();
|
||||||
|
|
||||||
|
// Вернуть URL файла
|
||||||
|
const fileUrl = getFileUrl(objectName);
|
||||||
|
|
||||||
|
log('info', 'Файл загружен в MinIO через S3', {
|
||||||
|
objectName,
|
||||||
|
size: buffer.length,
|
||||||
|
url: fileUrl
|
||||||
|
});
|
||||||
|
|
||||||
|
return fileUrl;
|
||||||
|
} catch (error) {
|
||||||
|
log('error', 'Ошибка загрузки файла в MinIO', { error: error.message });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удалить файл из MinIO через S3 SDK
|
||||||
|
* @param {string} fileUrl - URL файла или путь к объекту
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async function deleteFile(fileUrl) {
|
||||||
|
if (!s3Client) {
|
||||||
|
throw new Error('S3 клиент не инициализирован');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Извлечь путь к объекту из URL
|
||||||
|
const objectName = extractObjectName(fileUrl);
|
||||||
|
|
||||||
|
if (!objectName) {
|
||||||
|
log('warn', 'Не удалось извлечь имя объекта из URL', { fileUrl });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await s3Client.send(new DeleteObjectCommand({
|
||||||
|
Bucket: config.minio.bucket,
|
||||||
|
Key: objectName
|
||||||
|
}));
|
||||||
|
|
||||||
|
log('info', 'Файл удален из MinIO через S3', { objectName });
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
log('error', 'Ошибка удаления файла из MinIO', { error: error.message });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удалить несколько файлов
|
||||||
|
* @param {string[]} fileUrls - Массив URL файлов
|
||||||
|
* @returns {Promise<number>} - Количество удаленных файлов
|
||||||
|
*/
|
||||||
|
async function deleteFiles(fileUrls) {
|
||||||
|
if (!minioClient || !fileUrls || !fileUrls.length) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deleted = 0;
|
||||||
|
|
||||||
|
for (const fileUrl of fileUrls) {
|
||||||
|
try {
|
||||||
|
const success = await deleteFile(fileUrl);
|
||||||
|
if (success) deleted++;
|
||||||
|
} catch (error) {
|
||||||
|
log('error', 'Ошибка при удалении файла', { fileUrl, error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить временный URL для доступа к файлу (presigned URL)
|
||||||
|
* @param {string} objectName - Имя объекта
|
||||||
|
* @param {number} expirySeconds - Время жизни URL в секундах (по умолчанию 7 дней)
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async function getPresignedUrl(objectName, expirySeconds = 7 * 24 * 60 * 60) {
|
||||||
|
if (!s3Client) {
|
||||||
|
throw new Error('S3 клиент не инициализирован');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');
|
||||||
|
const { GetObjectCommand } = require('@aws-sdk/client-s3');
|
||||||
|
|
||||||
|
const command = new GetObjectCommand({
|
||||||
|
Bucket: config.minio.bucket,
|
||||||
|
Key: objectName
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = await getSignedUrl(s3Client, command, { expiresIn: expirySeconds });
|
||||||
|
return url;
|
||||||
|
} catch (error) {
|
||||||
|
log('error', 'Ошибка получения presigned URL', { error: error.message });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить публичный URL файла
|
||||||
|
* @param {string} objectName - Имя объекта
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function getFileUrl(objectName) {
|
||||||
|
if (config.minio.publicUrl) {
|
||||||
|
// Использовать кастомный публичный URL (например, через CDN)
|
||||||
|
return `${config.minio.publicUrl}/${config.minio.bucket}/${objectName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Использовать прямой URL MinIO
|
||||||
|
const protocol = config.minio.useSSL ? 'https' : 'http';
|
||||||
|
const port = config.minio.port === 80 || config.minio.port === 443 ? '' : `:${config.minio.port}`;
|
||||||
|
return `${protocol}://${config.minio.endpoint}${port}/${config.minio.bucket}/${objectName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Извлечь имя объекта из URL
|
||||||
|
* @param {string} fileUrl - URL файла
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
function extractObjectName(fileUrl) {
|
||||||
|
if (!fileUrl) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Если это уже имя объекта (путь)
|
||||||
|
if (!fileUrl.startsWith('http')) {
|
||||||
|
return fileUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлечь из URL
|
||||||
|
const url = new URL(fileUrl);
|
||||||
|
const pathParts = url.pathname.split('/');
|
||||||
|
|
||||||
|
// Убрать bucket из пути
|
||||||
|
const bucketIndex = pathParts.indexOf(config.minio.bucket);
|
||||||
|
if (bucketIndex !== -1) {
|
||||||
|
return pathParts.slice(bucketIndex + 1).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Попробовать альтернативный формат
|
||||||
|
return pathParts.slice(1).join('/');
|
||||||
|
} catch (error) {
|
||||||
|
log('error', 'Ошибка парсинга URL', { fileUrl, error: error.message });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить доступность MinIO через S3 SDK
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
async function checkConnection() {
|
||||||
|
if (!s3Client) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await s3Client.send(new HeadBucketCommand({
|
||||||
|
Bucket: config.minio.bucket
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
log('error', 'MinIO недоступен', { error: error.message });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить статистику bucket через S3 SDK
|
||||||
|
* @returns {Promise<object>}
|
||||||
|
*/
|
||||||
|
async function getBucketStats() {
|
||||||
|
if (!s3Client) {
|
||||||
|
throw new Error('S3 клиент не инициализирован');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let totalSize = 0;
|
||||||
|
let totalFiles = 0;
|
||||||
|
let continuationToken = undefined;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const response = await s3Client.send(new ListObjectsV2Command({
|
||||||
|
Bucket: config.minio.bucket,
|
||||||
|
ContinuationToken: continuationToken
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (response.Contents) {
|
||||||
|
for (const obj of response.Contents) {
|
||||||
|
totalSize += obj.Size || 0;
|
||||||
|
totalFiles++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continuationToken = response.NextContinuationToken;
|
||||||
|
} while (continuationToken);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalFiles,
|
||||||
|
totalSize,
|
||||||
|
totalSizeMB: (totalSize / (1024 * 1024)).toFixed(2),
|
||||||
|
totalSizeGB: (totalSize / (1024 * 1024 * 1024)).toFixed(2),
|
||||||
|
bucket: config.minio.bucket
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log('error', 'Ошибка получения статистики bucket', { error: error.message });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
initMinioClient,
|
||||||
|
uploadFile,
|
||||||
|
deleteFile,
|
||||||
|
deleteFiles,
|
||||||
|
getPresignedUrl,
|
||||||
|
getFileUrl,
|
||||||
|
checkConnection,
|
||||||
|
getBucketStats,
|
||||||
|
isEnabled: () => config.minio.enabled
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -25,9 +25,19 @@ services:
|
||||||
- FRONTEND_URL=${FRONTEND_URL:-http://localhost:5173}
|
- FRONTEND_URL=${FRONTEND_URL:-http://localhost:5173}
|
||||||
- CORS_ORIGIN=${CORS_ORIGIN:-*}
|
- CORS_ORIGIN=${CORS_ORIGIN:-*}
|
||||||
- REDIS_URL=${REDIS_URL}
|
- REDIS_URL=${REDIS_URL}
|
||||||
|
# MinIO Configuration (подключение к внешнему серверу)
|
||||||
|
- MINIO_ENABLED=${MINIO_ENABLED:-true}
|
||||||
|
- MINIO_ENDPOINT=${MINIO_ENDPOINT:-103.80.87.247}
|
||||||
|
- MINIO_PORT=${MINIO_PORT:-9000}
|
||||||
|
- MINIO_USE_SSL=${MINIO_USE_SSL:-false}
|
||||||
|
- MINIO_ACCESS_KEY=${MINIO_ACCESS_KEY}
|
||||||
|
- MINIO_SECRET_KEY=${MINIO_SECRET_KEY}
|
||||||
|
- MINIO_BUCKET=${MINIO_BUCKET:-nakama-media}
|
||||||
|
- MINIO_REGION=${MINIO_REGION:-us-east-1}
|
||||||
|
- MINIO_PUBLIC_URL=${MINIO_PUBLIC_URL}
|
||||||
volumes:
|
volumes:
|
||||||
# Медиа хранится на удаленном сервере, монтируем через NFS или SSH
|
# Fallback локальное хранилище (если MinIO недоступен)
|
||||||
- /mnt/nakama-media:/app/backend/uploads
|
- nakama-media:/app/backend/uploads
|
||||||
networks:
|
networks:
|
||||||
- nakama-network
|
- nakama-network
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
@ -100,6 +110,9 @@ services:
|
||||||
entrypoint: /bin/bash
|
entrypoint: /bin/bash
|
||||||
command: -c "echo 'Backup container ready. Run manual backups or set up cron.'"
|
command: -c "echo 'Backup container ready. Run manual backups or set up cron.'"
|
||||||
|
|
||||||
|
# MinIO запущен на отдельном сервере 103.80.87.247
|
||||||
|
# Локальный MinIO не нужен
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
nakama-network:
|
nakama-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,10 @@
|
||||||
"xss-clean": "^0.1.4",
|
"xss-clean": "^0.1.4",
|
||||||
"hpp": "^0.2.3",
|
"hpp": "^0.2.3",
|
||||||
"validator": "^13.11.0",
|
"validator": "^13.11.0",
|
||||||
"@telegram-apps/init-data-node": "^1.0.4"
|
"@telegram-apps/init-data-node": "^1.0.4",
|
||||||
|
"@aws-sdk/client-s3": "^3.451.0",
|
||||||
|
"@aws-sdk/lib-storage": "^3.451.0",
|
||||||
|
"@aws-sdk/s3-request-presigner": "^3.451.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue