Update files

This commit is contained in:
glpshchn 2025-11-21 01:07:37 +03:00
parent 1cdfd57cdf
commit 678783d3be
13 changed files with 1973 additions and 83 deletions

60
ENV_EXAMPLE.txt Normal file
View File

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

347
MINIO_MIGRATION_SUMMARY.md Normal file
View File

@ -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` для подробной документации!

500
MINIO_SETUP.md Normal file
View File

@ -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 при создании постов и публикациях в канал.

438
S3_MINIO_SETUP.md Normal file
View File

@ -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! 🚀

View File

@ -69,6 +69,20 @@ module.exports = {
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',
isProduction: () => process.env.NODE_ENV === 'production',

View File

@ -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')
};

View File

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

View File

@ -2,10 +2,11 @@ const express = require('express');
const router = express.Router();
const fs = require('fs');
const path = require('path');
const multer = require('multer');
const crypto = require('crypto');
const { authenticateModeration } = require('../middleware/auth');
const { logSecurityEvent } = require('../middleware/logger');
const { uploadChannelMedia, cleanupOnError } = require('../middleware/upload');
const { deleteFile } = require('../utils/minio');
const User = require('../models/User');
const Post = require('../models/Post');
const Report = require('../models/Report');
@ -15,26 +16,6 @@ const { listAdmins, isModerationAdmin, normalizeUsername } = require('../service
const { sendChannelMediaGroup, sendMessageToUser } = require('../bots/serverMonitor');
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 requireModerationAccess = async (req, res, next) => {
@ -672,7 +653,7 @@ router.post(
'/channel/publish',
authenticateModeration,
requireModerationAccess,
upload.array('images', 10),
uploadChannelMedia,
async (req, res) => {
const { description = '', tags } = req.body;
const files = req.files || [];

View File

@ -1,68 +1,17 @@
const express = require('express');
const router = express.Router();
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { authenticate } = require('../middleware/auth');
const { postCreationLimiter, interactionLimiter } = require('../middleware/rateLimiter');
const { searchLimiter } = require('../middleware/rateLimiter');
const { validatePostContent, validateTags, validateImageUrl } = require('../middleware/validator');
const { logSecurityEvent } = require('../middleware/logger');
const { strictPostLimiter, fileUploadLimiter } = require('../middleware/security');
const { uploadPostImages, cleanupOnError } = require('../middleware/upload');
const { deleteFiles } = require('../utils/minio');
const Post = require('../models/Post');
const Notification = require('../models/Notification');
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) => {
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 {
const { content, tags, mentionedUsers, isNSFW, externalImages } = req.body;
@ -146,9 +95,9 @@ router.post('/', authenticate, strictPostLimiter, postCreationLimiter, fileUploa
// Обработка изображений
let images = [];
// Загруженные файлы
if (req.files && req.files.length > 0) {
images = req.files.map(file => `/uploads/posts/${file.filename}`);
// Загруженные файлы (через middleware)
if (req.uploadedFiles && req.uploadedFiles.length > 0) {
images = req.uploadedFiles;
}
// Внешние изображения (из поиска)

View File

@ -11,6 +11,7 @@ dotenv.config({ path: path.join(__dirname, '.env') });
const { generalLimiter } = require('./middleware/rateLimiter');
const { initRedis } = require('./utils/redis');
const { initWebSocket } = require('./websocket');
const { initMinioClient, checkConnection: checkMinioConnection } = require('./utils/minio');
const config = require('./config');
// Security middleware
@ -150,14 +151,34 @@ app.get('/health', (req, res) => {
// MongoDB подключение
mongoose.connect(config.mongoUri)
.then(() => {
.then(async () => {
console.log(`✅ MongoDB подключена: ${config.mongoUri.replace(/\/\/.*@/, '//***@')}`);
// Инициализировать Redis (опционально)
if (config.redisUrl) {
initRedis().catch(err => console.log('⚠️ Redis недоступен, работаем без кэша'));
} else {
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));
@ -172,6 +193,7 @@ app.use('/api/moderation', require('./routes/moderation'));
app.use('/api/statistics', require('./routes/statistics'));
app.use('/api/bot', require('./routes/bot'));
app.use('/api/mod-app', require('./routes/modApp'));
app.use('/api/minio', require('./routes/minio-test'));
// Базовый роут
app.get('/', (req, res) => {

358
backend/utils/minio.js Normal file
View File

@ -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
};

View File

@ -25,9 +25,19 @@ services:
- FRONTEND_URL=${FRONTEND_URL:-http://localhost:5173}
- CORS_ORIGIN=${CORS_ORIGIN:-*}
- 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:
# Медиа хранится на удаленном сервере, монтируем через NFS или SSH
- /mnt/nakama-media:/app/backend/uploads
# Fallback локальное хранилище (если MinIO недоступен)
- nakama-media:/app/backend/uploads
networks:
- nakama-network
depends_on:
@ -100,6 +110,9 @@ services:
entrypoint: /bin/bash
command: -c "echo 'Backup container ready. Run manual backups or set up cron.'"
# MinIO запущен на отдельном сервере 103.80.87.247
# Локальный MinIO не нужен
networks:
nakama-network:
driver: bridge

View File

@ -36,7 +36,10 @@
"xss-clean": "^0.1.4",
"hpp": "^0.2.3",
"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": {
"nodemon": "^3.0.1",