2025-09-02 12:32:15 +03:00
|
|
|
|
# 🔀 Hybrid Architecture: Vercel Edge + Quoter
|
|
|
|
|
|
|
|
|
|
|
|
## 📋 Архитектурное решение
|
|
|
|
|
|
|
|
|
|
|
|
Ваша спецификация описывает **идеальную гибридную архитектуру**:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
📤 Upload: Quoter (контроль + квоты)
|
|
|
|
|
|
📥 Download: Vercel Edge API (производительность)
|
2025-09-02 14:00:54 +03:00
|
|
|
|
🎨 Thumbnails: Vercel /api/thumb/[width]/[...path] (динамическая генерация)
|
2025-09-02 12:32:15 +03:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## ✅ Преимущества гибридного подхода
|
|
|
|
|
|
|
|
|
|
|
|
### 🎯 **Лучшее из двух миров**
|
|
|
|
|
|
|
|
|
|
|
|
| Компонент | Сервис | Почему именно он |
|
|
|
|
|
|
|-----------|---------|------------------|
|
|
|
|
|
|
| **Upload** | Quoter | Контроль квот, кастомная логика, безопасность |
|
|
|
|
|
|
| **Download** | Vercel | Автоматический WebP/AVIF, глобальный edge |
|
|
|
|
|
|
| **Resize** | Vercel | Ленивая генерация, auto-optimization |
|
|
|
|
|
|
| **OG** | Vercel | Динамическая генерация, кэширование |
|
|
|
|
|
|
|
|
|
|
|
|
### 💰 **Экономическая эффективность**
|
|
|
|
|
|
- **Upload costs**: Только VPS + Storj (контролируемые)
|
|
|
|
|
|
- **Download costs**: Vercel edge (pay-per-use, но дешевле CDN)
|
|
|
|
|
|
- **Storage costs**: Storj (~$4/TB против $20+/TB у Vercel)
|
|
|
|
|
|
|
|
|
|
|
|
### 🚀 **Производительность**
|
|
|
|
|
|
- **Upload**: Direct to S3, без proxy overhead
|
|
|
|
|
|
- **Download**: Vercel Edge (~50ms globally)
|
|
|
|
|
|
- **Caching**: Двухуровневое (Vercel + S3)
|
|
|
|
|
|
|
|
|
|
|
|
## 🔧 Интеграция с текущим Quoter
|
|
|
|
|
|
|
|
|
|
|
|
### 1. **Обновление CORS для Vercel**
|
|
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
|
// src/main.rs - добавить Vercel в allowed origins
|
|
|
|
|
|
let cors = Cors::default()
|
|
|
|
|
|
.allowed_origin("https://discours.io")
|
|
|
|
|
|
.allowed_origin("https://new.discours.io")
|
|
|
|
|
|
.allowed_origin("https://vercel.app") // для Vercel edge functions
|
|
|
|
|
|
.allowed_origin("http://localhost:3000") // для разработки
|
|
|
|
|
|
.allowed_methods(vec!["GET", "POST", "OPTIONS"])
|
|
|
|
|
|
// ...
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. **Добавление заголовков для Vercel Image API**
|
|
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
|
// src/handlers/common.rs - добавить заголовки для Vercel
|
|
|
|
|
|
pub fn create_vercel_compatible_response(content_type: &str, data: Vec<u8>, etag: &str) -> HttpResponse {
|
|
|
|
|
|
HttpResponse::Ok()
|
|
|
|
|
|
.content_type(content_type)
|
|
|
|
|
|
.insert_header(("etag", etag))
|
|
|
|
|
|
.insert_header(("cache-control", CACHE_CONTROL_IMMUTABLE))
|
|
|
|
|
|
.insert_header(("access-control-allow-origin", "*"))
|
|
|
|
|
|
.insert_header(("x-vercel-cache", "HIT")) // для оптимизации Vercel
|
|
|
|
|
|
.body(data)
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3. **Endpoint для проверки доступности**
|
|
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
|
// src/handlers/universal.rs - добавить health check для Vercel
|
|
|
|
|
|
async fn handle_get(
|
|
|
|
|
|
req: HttpRequest,
|
|
|
|
|
|
state: web::Data<AppState>,
|
|
|
|
|
|
path: &str,
|
|
|
|
|
|
) -> Result<HttpResponse, actix_web::Error> {
|
|
|
|
|
|
match path {
|
|
|
|
|
|
"/" => crate::handlers::user::get_current_user_handler(req, state).await,
|
|
|
|
|
|
"/health" => Ok(HttpResponse::Ok().json(serde_json::json!({
|
|
|
|
|
|
"status": "ok",
|
|
|
|
|
|
"service": "quoter",
|
|
|
|
|
|
"version": env!("CARGO_PKG_VERSION")
|
|
|
|
|
|
}))),
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
// GET /{path} - получение файла через proxy
|
|
|
|
|
|
let path_without_slash = path.trim_start_matches('/');
|
|
|
|
|
|
let requested_res = web::Path::from(path_without_slash.to_string());
|
|
|
|
|
|
crate::handlers::proxy::proxy_handler(req, requested_res, state).await
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🔄 Миграционная стратегия
|
|
|
|
|
|
|
|
|
|
|
|
### Этап 1: Подготовка Quoter (текущий)
|
|
|
|
|
|
- ✅ **Готово**: Upload API с квотами и безопасностью
|
|
|
|
|
|
- ✅ **Готово**: Система миниатюр и ресайзинга
|
|
|
|
|
|
- ✅ **Готово**: Multi-cloud storage (Storj + AWS)
|
|
|
|
|
|
- 🔄 **Добавить**: CORS для Vercel edge functions
|
|
|
|
|
|
- 🔄 **Добавить**: Health check endpoint
|
|
|
|
|
|
|
|
|
|
|
|
### Этап 2: Настройка Vercel Edge
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
// vercel.json - конфигурация для оптимизации
|
|
|
|
|
|
{
|
|
|
|
|
|
"images": {
|
|
|
|
|
|
"deviceSizes": [64, 128, 256, 320, 400, 640, 800, 1200, 1600],
|
|
|
|
|
|
"imageSizes": [10, 40, 110],
|
|
|
|
|
|
"remotePatterns": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"protocol": "https",
|
|
|
|
|
|
"hostname": "files.dscrs.site",
|
|
|
|
|
|
"pathname": "/**"
|
|
|
|
|
|
}
|
|
|
|
|
|
],
|
|
|
|
|
|
"minimumCacheTTL": 86400,
|
|
|
|
|
|
"dangerouslyAllowSVG": false
|
|
|
|
|
|
},
|
|
|
|
|
|
"functions": {
|
|
|
|
|
|
"api/og.js": {
|
|
|
|
|
|
"maxDuration": 30
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Этап 3: Клиентская интеграция
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// Проверка доступности и fallback
|
|
|
|
|
|
export const getImageService = async (): Promise<'vercel' | 'quoter'> => {
|
|
|
|
|
|
// Vercel по умолчанию для большинства случаев
|
|
|
|
|
|
if (typeof window === 'undefined') return 'vercel'; // SSR
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Проверяем доступность Vercel Image API
|
|
|
|
|
|
const response = await fetch('/_next/image?url=' + encodeURIComponent('https://files.dscrs.site/test.jpg') + '&w=1&q=1');
|
|
|
|
|
|
return response.ok ? 'vercel' : 'quoter';
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return 'quoter'; // fallback
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 📊 Мониторинг гибридной системы
|
|
|
|
|
|
|
|
|
|
|
|
### Метрики Quoter (Upload)
|
|
|
|
|
|
```log
|
|
|
|
|
|
# Upload успешность
|
|
|
|
|
|
INFO Upload successful: user_123 uploaded photo.jpg (2.5MB)
|
|
|
|
|
|
INFO Quota updated: user_123 now using 45% (5.4GB/12GB)
|
|
|
|
|
|
|
|
|
|
|
|
# Rate limiting
|
|
|
|
|
|
WARN Rate limit applied: IP 192.168.1.100 blocked for upload (10/5min exceeded)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Метрики Vercel (Download)
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
// api/metrics.js - собираем метрики download
|
|
|
|
|
|
export default async function handler(req) {
|
|
|
|
|
|
const { searchParams } = new URL(req.url);
|
|
|
|
|
|
const source = searchParams.get('source'); // 'vercel' | 'quoter'
|
|
|
|
|
|
const filename = searchParams.get('filename');
|
|
|
|
|
|
|
|
|
|
|
|
// Логируем использование
|
|
|
|
|
|
console.log(`Image served: ${filename} via ${source}`);
|
|
|
|
|
|
|
|
|
|
|
|
return new Response('OK');
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 🎯 Production готовность
|
|
|
|
|
|
|
|
|
|
|
|
### Load Testing
|
|
|
|
|
|
```bash
|
|
|
|
|
|
# Test upload через Quoter
|
|
|
|
|
|
ab -n 100 -c 10 -T 'multipart/form-data; boundary=----WebKitFormBoundary' \
|
|
|
|
|
|
-H "Authorization: Bearer $TOKEN" \
|
|
|
|
|
|
https://files.dscrs.site/
|
|
|
|
|
|
|
|
|
|
|
|
# Test download через Vercel
|
|
|
|
|
|
ab -n 1000 -c 50 \
|
|
|
|
|
|
'https://discours.io/_next/image?url=https%3A//files.dscrs.site/test.jpg&w=600&q=75'
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Failover Strategy
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
export const getImageWithFailover = async (filename: string, width: number) => {
|
|
|
|
|
|
const strategies = [
|
|
|
|
|
|
() => getVercelImageUrl(`https://files.dscrs.site/${filename}`, width),
|
|
|
|
|
|
() => getQuoterWebpUrl(filename, width),
|
|
|
|
|
|
() => `https://files.dscrs.site/${filename}` // fallback to original
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
for (const strategy of strategies) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const url = strategy();
|
|
|
|
|
|
const response = await fetch(url, { method: 'HEAD' });
|
|
|
|
|
|
if (response.ok) return url;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('Image strategy failed:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
throw new Error('All image strategies failed');
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 💡 Рекомендации по оптимизации
|
|
|
|
|
|
|
|
|
|
|
|
### 1. **Кэширование**
|
|
|
|
|
|
- Vercel Edge: автоматическое кэширование
|
|
|
|
|
|
- Quoter: ETag + immutable headers
|
|
|
|
|
|
- CDN: дополнительный слой кэширования
|
|
|
|
|
|
|
|
|
|
|
|
### 2. **Мониторинг**
|
|
|
|
|
|
- Sentry для error tracking
|
|
|
|
|
|
- Vercel Analytics для performance
|
|
|
|
|
|
- Custom metrics для quota usage
|
|
|
|
|
|
|
|
|
|
|
|
### 3. **Costs optimization**
|
|
|
|
|
|
```typescript
|
|
|
|
|
|
// Умное переключение между сервисами
|
|
|
|
|
|
export const getCostOptimalImageUrl = (filename: string, width: number, useCase: ImageUseCase) => {
|
|
|
|
|
|
// Для часто используемых размеров - Vercel (лучше кэш)
|
|
|
|
|
|
if ([300, 600, 800].includes(width)) {
|
|
|
|
|
|
return getVercelImageUrl(`https://files.dscrs.site/${filename}`, width);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Для редких размеров - Quoter (избегаем Vercel billing)
|
|
|
|
|
|
return getQuoterWebpUrl(filename, width);
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## ✅ Выводы
|
|
|
|
|
|
|
|
|
|
|
|
Ваша архитектура идеальна потому что:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Upload остается в Quoter** - полный контроль безопасности и квот
|
|
|
|
|
|
2. **Download через Vercel** - глобальная производительность и auto-optimization
|
|
|
|
|
|
3. **OG через @vercel/og** - динамическая генерация без сложности
|
|
|
|
|
|
4. **Постепенная миграция** - можно внедрять поэтапно
|
|
|
|
|
|
5. **Fallback стратегия** - надежность через redundancy
|
|
|
|
|
|
|
|
|
|
|
|
💋 **Упрощение достигнуто**: убираем сложность ресайзинга из Quoter, оставляем только upload + storage, всю оптимизацию отдаем Vercel Edge.
|
|
|
|
|
|
|
|
|
|
|
|
Стоит ли добавить эти изменения в код Quoter для поддержки Vercel интеграции?
|