Files
quoter/src/handlers/proxy.rs
Untone 7746d1f38e
Some checks failed
CI / lint (push) Successful in 2m11s
Deploy / deploy (push) Has been skipped
CI / test (push) Failing after 9m6s
[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 принцип применен - убрали избыточность, оставили суть.
2025-09-02 14:39:54 +03:00

267 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use actix_web::error::ErrorNotFound;
use actix_web::{HttpRequest, HttpResponse, Result, error::ErrorInternalServerError, web};
use log::{error, info, warn};
use super::common::{
create_file_response_with_analytics, create_vercel_compatible_response, is_vercel_request,
};
use crate::app_state::AppState;
use crate::handlers::serve_file::serve_file;
use crate::lookup::{find_file_by_pattern, get_mime_type};
use crate::s3_utils::{check_file_exists, load_file_from_s3, upload_to_s3};
use crate::thumbnail::parse_file_path;
// Удалена дублирующая функция, используется из common модуля
/// Обработчик для скачивания файла
/// без генерации миниатюр - это делает Vercel Edge API
#[allow(clippy::collapsible_if)]
pub async fn proxy_handler(
req: HttpRequest,
requested_res: web::Path<String>,
state: web::Data<AppState>,
) -> Result<HttpResponse, actix_web::Error> {
let start_time = std::time::Instant::now();
info!("GET {} [START]", requested_res);
let normalized_path = if requested_res.ends_with("/webp") {
info!("Converting to WebP format: {}", requested_res);
requested_res.replace("/webp", "")
} else {
requested_res.to_string()
};
// Парсим GET запрос
let (base_filename, requested_width, extension) = parse_file_path(&normalized_path);
let ext = extension.as_str().to_lowercase();
let filekey = format!("{}.{}", base_filename, &ext);
info!(
"Parsed request - base: {}, width: {}, ext: {}",
base_filename, requested_width, ext
);
// Caching handled by Vercel Edge - focus on fast file serving
let content_type = match get_mime_type(&ext) {
Some(mime) => mime.to_string(),
None => match find_file_by_pattern(None, &base_filename).await {
Ok(Some(found_file)) => {
if let Some(found_ext) = found_file.split('.').next_back() {
get_mime_type(found_ext)
.unwrap_or("application/octet-stream")
.to_string()
} else {
"application/octet-stream".to_string()
}
}
_ => {
error!("Unsupported file format for: {}", base_filename);
return Err(ErrorInternalServerError("Unsupported file format"));
}
},
};
info!("Content-Type: {}", content_type);
return match state.get_path(&filekey).await {
Ok(Some(stored_path)) => {
warn!("Found stored path in DB: {}", stored_path);
warn!(
"Checking Storj path - bucket: {}, path: {}",
state.bucket, stored_path
);
if check_file_exists(&state.storj_client, &state.bucket, &stored_path).await? {
warn!("File exists in Storj: {}", stored_path);
// Просто отдаем файл, миниатюры генерирует Vercel Edge API
serve_file(&stored_path, &state, &req).await
} else {
warn!(
"Attempting to load from AWS - bucket: {}, path: {}",
state.bucket, stored_path
);
// Определяем тип медиа из content_type
let media_type = content_type.split("/").next().unwrap_or("image");
// Создаем варианты путей с обоими регистрами расширения
let paths_lower = vec![
stored_path.clone(),
// format!("production/{}", stored_path),
format!("production/{}/{}", media_type, stored_path),
];
// Создаем те же пути, но с оригинальным регистром расширения
let orig_ext = extension.as_str(); // оригинальное расширение
let orig_stored_path = format!("{}.{}", base_filename, orig_ext);
let paths_orig = vec![
orig_stored_path.clone(),
// format!("production/{}", orig_stored_path),
format!("production/{}/{}", media_type, orig_stored_path),
];
// Объединяем все пути для проверки
let all_paths = paths_lower.into_iter().chain(paths_orig.into_iter());
for path in all_paths {
warn!("Trying AWS path: {}", path);
match load_file_from_s3(&state.aws_client, &state.bucket, &path).await {
Ok(filedata) => {
warn!(
"Successfully loaded file from AWS, size: {} bytes",
filedata.len()
);
warn!("Attempting to upload to Storj with key: {}", filekey);
if let Err(e) = upload_to_s3(
&state.storj_client,
&state.bucket,
&filekey,
filedata.clone(),
&content_type,
)
.await
{
error!("Failed to upload to Storj: {} - Error: {}", filekey, e);
} else {
warn!("Successfully uploaded to Storj: {}", filekey);
}
let elapsed = start_time.elapsed();
info!("File served from AWS in {:?}: {}", elapsed, path);
// Используем Vercel-совместимый ответ для Vercel запросов
if is_vercel_request(&req) {
let etag = format!("\"{:x}\"", md5::compute(&filedata));
return Ok(create_vercel_compatible_response(
&content_type,
filedata,
&etag,
));
} else {
return Ok(create_file_response_with_analytics(
&content_type,
filedata,
&req,
&path,
));
}
}
Err(err) => {
warn!("Failed to load from AWS path {}: {:?}", path, err);
continue;
}
}
}
error!("Failed to load from any AWS path for: {}", stored_path);
Err(ErrorInternalServerError("Failed to load file from AWS"))
}
}
Ok(None) => {
warn!("No stored path found in DB for: {}", filekey);
let ct_parts = content_type.split("/").collect::<Vec<&str>>();
// Создаем два варианта пути - с оригинальным расширением и с нижним регистром
let filepath_lower = format!("production/{}/{}.{}", ct_parts[0], base_filename, ext);
let filepath_orig =
format!("production/{}/{}.{}", ct_parts[0], base_filename, extension);
warn!(
"Looking up files with paths: {} or {} in bucket: {}",
filepath_lower, filepath_orig, state.bucket
);
// Проверяем существование файла с обоими вариантами расширения
let exists_in_aws_lower =
check_file_exists(&state.aws_client, &state.bucket, &filepath_lower).await?;
let exists_in_aws_orig =
check_file_exists(&state.aws_client, &state.bucket, &filepath_orig).await?;
let filepath = if exists_in_aws_orig {
filepath_orig
} else if exists_in_aws_lower {
filepath_lower
} else {
// Если файл не найден ни с одним из расширений, используем нижний регистр по умолчанию
filepath_lower
};
let exists_in_storj =
check_file_exists(&state.storj_client, &state.bucket, &filepath).await?;
warn!("Checking existence in Storj: {}", exists_in_storj);
if exists_in_storj {
warn!("file {} exists in storj, serving directly", filepath);
// Файл существует в Storj, отдаем его напрямую
return serve_file(&filepath, &state, &req).await;
} else {
warn!("file {} does not exist in storj", filepath);
}
let exists_in_aws =
check_file_exists(&state.aws_client, &state.bucket, &filepath).await?;
warn!("Checking existence in AWS: {}", exists_in_aws);
if exists_in_aws {
warn!("File found in AWS, attempting to download: {}", filepath);
match load_file_from_s3(&state.aws_client, &state.bucket, &filepath).await {
Ok(filedata) => {
warn!(
"Successfully downloaded file from AWS, size: {} bytes",
filedata.len()
);
if let Err(e) = upload_to_s3(
&state.storj_client,
&state.bucket,
&filekey,
filedata.clone(),
&content_type,
)
.await
{
warn!("cannot upload to storj: {}", e);
} else {
warn!("file {} uploaded to storj", filekey);
state.set_path(&filekey, &filepath).await;
}
let elapsed = start_time.elapsed();
info!("File served from AWS in {:?}: {}", elapsed, filepath);
// Используем Vercel-совместимый ответ для Vercel запросов
if is_vercel_request(&req) {
let etag = format!("\"{:x}\"", md5::compute(&filedata));
Ok(create_vercel_compatible_response(
&content_type,
filedata,
&etag,
))
} else {
Ok(create_file_response_with_analytics(
&content_type,
filedata,
&req,
&filepath,
))
}
}
Err(e) => {
error!("Failed to download from AWS: {} - Error: {}", filepath, e);
Err(ErrorInternalServerError(e))
}
}
} else {
error!("File not found in either Storj or AWS: {}", filepath);
Err(ErrorNotFound("file does not exist"))
}
}
Err(e) => {
let elapsed = start_time.elapsed();
error!(
"Database error while getting path: {} in {:?} - Full error: {:?}",
filekey, elapsed, e
);
Err(ErrorInternalServerError(e))
}
};
}