[0.6.1] - 2025-09-02
### 🚀 Изменено - Упрощение архитектуры - **Генерация миниатюр**: Полностью удалена из Quoter, теперь управляется Vercel Edge API - **Очистка legacy кода**: Удалены все функции генерации миниатюр и сложность - **Документация**: Сокращена с 17 файлов до 7, следуя принципам KISS/DRY - **Смена фокуса**: Quoter теперь сосредоточен на upload + storage, Vercel обрабатывает миниатюры - **Логирование запросов**: Добавлена аналитика источников для оптимизации CORS whitelist - **Реализация таймаутов**: Добавлены настраиваемые таймауты для S3, Redis и внешних операций - **Упрощенная безопасность**: Удален сложный rate limiting, оставлена только необходимая защита upload - **Vercel интеграция**: Добавлена поддержка Vercel Edge API с CORS и оптимизированными заголовками - **Redis graceful fallback**: Приложение теперь работает без Redis с предупреждениями вместо паники - **Умная логика ответов**: Автоматическое определение Vercel запросов и оптимизированные заголовки - **Консолидация документации**: Объединены 4 Vercel документа в один comprehensive guide ### 📝 Обновлено - Консолидирована документация в практическую структуру: - Основной README.md с быстрым стартом - docs/SETUP.md для конфигурации и развертывания - Упрощенный features.md с фокусом на основную функциональность - docs/vercel-frontend-migration.md - единый comprehensive guide для Vercel интеграции - Добавлен акцент на Vercel по всему коду и документации - Обновлены URL patterns в документации: quoter.discours.io → files.dscrs.site ### 🗑️ Удалено - Избыточные файлы документации (api-reference, deployment, development, и т.д.) - Дублирующийся контент в нескольких документах - Излишне детальная документация для простого файлового прокси - 4 отдельных Vercel документа: vercel-thumbnails.md, vercel-integration.md, hybrid-architecture.md, vercel-og-integration.md 💋 **Упрощение**: KISS принцип применен - убрали избыточность, оставили суть.
This commit is contained in:
@@ -9,7 +9,7 @@ use std::{env, time::Duration};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub redis: MultiplexedConnection,
|
||||
pub redis: Option<MultiplexedConnection>,
|
||||
pub storj_client: S3Client,
|
||||
pub aws_client: S3Client,
|
||||
pub bucket: String,
|
||||
@@ -32,15 +32,28 @@ impl AppState {
|
||||
let redis_url = env::var("REDIS_URL").expect("REDIS_URL must be set");
|
||||
let redis_client = RedisClient::open(redis_url).expect("Invalid Redis URL");
|
||||
|
||||
// Устанавливаем таймаут для Redis операций
|
||||
let redis_connection = tokio::time::timeout(
|
||||
// Устанавливаем таймаут для Redis операций с graceful fallback
|
||||
let redis_connection = match tokio::time::timeout(
|
||||
Duration::from_secs(security_config.request_timeout_seconds),
|
||||
redis_client.get_multiplexed_async_connection(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "Redis connection timeout")
|
||||
.expect("Failed to connect to Redis within timeout")
|
||||
.expect("Redis connection failed");
|
||||
{
|
||||
Ok(Ok(conn)) => {
|
||||
log::info!("✅ Redis connection established");
|
||||
Some(conn)
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
log::warn!("⚠️ Redis connection failed: {}", e);
|
||||
log::warn!("⚠️ Running in fallback mode without Redis (quotas disabled)");
|
||||
None
|
||||
}
|
||||
Err(_) => {
|
||||
log::warn!("⚠️ Redis connection timeout");
|
||||
log::warn!("⚠️ Running in fallback mode without Redis (quotas disabled)");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Получаем конфигурацию для S3 (Storj)
|
||||
let s3_access_key = env::var("STORJ_ACCESS_KEY").expect("STORJ_ACCESS_KEY must be set");
|
||||
@@ -120,17 +133,27 @@ impl AppState {
|
||||
/// Кэширует список файлов из Storj S3 в Redis.
|
||||
pub async fn cache_filelist(&self) {
|
||||
warn!("caching AWS filelist...");
|
||||
let mut redis = self.redis.clone();
|
||||
|
||||
// Проверяем доступность Redis
|
||||
let Some(mut redis) = self.redis.clone() else {
|
||||
warn!("⚠️ Redis not available, skipping filelist caching");
|
||||
return;
|
||||
};
|
||||
|
||||
// Запрашиваем список файлов из Storj S3
|
||||
let filelist = get_s3_filelist(&self.aws_client, &self.bucket).await;
|
||||
|
||||
for [filename, filepath] in filelist.clone() {
|
||||
// Сохраняем список файлов в Redis, используя HSET для каждого файла
|
||||
let _: () = redis
|
||||
.hset(PATH_MAPPING_KEY, filename.clone(), filepath)
|
||||
.await
|
||||
.unwrap();
|
||||
if let Err(e) = tokio::time::timeout(
|
||||
self.request_timeout,
|
||||
redis.hset::<_, _, _, ()>(PATH_MAPPING_KEY, filename.clone(), filepath),
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("⚠️ Redis operation failed: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
warn!("cached {} files", filelist.len());
|
||||
@@ -138,7 +161,10 @@ impl AppState {
|
||||
|
||||
/// Получает путь из ключа (имени файла) в Redis с таймаутом.
|
||||
pub async fn get_path(&self, filename: &str) -> Result<Option<String>, actix_web::Error> {
|
||||
let mut redis = self.redis.clone();
|
||||
let Some(mut redis) = self.redis.clone() else {
|
||||
warn!("⚠️ Redis not available, returning None for path lookup");
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let new_path: Option<String> =
|
||||
tokio::time::timeout(self.request_timeout, redis.hget(PATH_MAPPING_KEY, filename))
|
||||
@@ -150,20 +176,33 @@ impl AppState {
|
||||
}
|
||||
|
||||
pub async fn set_path(&self, filename: &str, filepath: &str) {
|
||||
let mut redis = self.redis.clone();
|
||||
let Some(mut redis) = self.redis.clone() else {
|
||||
warn!(
|
||||
"⚠️ Redis not available, skipping path caching for {}",
|
||||
filename
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let _: () = tokio::time::timeout(
|
||||
if let Err(e) = tokio::time::timeout(
|
||||
self.request_timeout,
|
||||
redis.hset(PATH_MAPPING_KEY, filename, filepath),
|
||||
redis.hset::<_, _, _, ()>(PATH_MAPPING_KEY, filename, filepath),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("Redis timeout when caching file {} in Redis", filename))
|
||||
.unwrap_or_else(|_| panic!("Failed to cache file {} in Redis", filename));
|
||||
{
|
||||
warn!("⚠️ Redis operation failed for {}: {}", filename, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// создает или получает текущее значение квоты пользователя с таймаутом
|
||||
pub async fn get_or_create_quota(&self, user_id: &str) -> Result<u64, actix_web::Error> {
|
||||
let mut redis = self.redis.clone();
|
||||
let Some(mut redis) = self.redis.clone() else {
|
||||
warn!(
|
||||
"⚠️ Redis not available, returning default quota for user {}",
|
||||
user_id
|
||||
);
|
||||
return Ok(0); // Возвращаем 0 как fallback
|
||||
};
|
||||
let quota_key = format!("quota:{}", user_id);
|
||||
|
||||
// Попытка получить квоту из Redis с таймаутом
|
||||
@@ -194,7 +233,13 @@ impl AppState {
|
||||
user_id: &str,
|
||||
bytes: u64,
|
||||
) -> Result<u64, actix_web::Error> {
|
||||
let mut redis = self.redis.clone();
|
||||
let Some(mut redis) = self.redis.clone() else {
|
||||
warn!(
|
||||
"⚠️ Redis not available, skipping quota increment for user {}",
|
||||
user_id
|
||||
);
|
||||
return Ok(0); // Возвращаем 0 как fallback
|
||||
};
|
||||
let quota_key = format!("quota:{}", user_id);
|
||||
|
||||
// Проверяем, существует ли ключ в Redis с таймаутом
|
||||
|
||||
Reference in New Issue
Block a user