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 мин
|
||||
},
|
||||
|
||||
// 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',
|
||||
|
|
|
|||
|
|
@ -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 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 || [];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
// Внешние изображения (из поиска)
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue