Files
quoter/src/handlers/proxy.rs
Untone 112f102bb5
Some checks failed
Deploy / deploy (push) Has been skipped
CI / lint (push) Failing after 7m37s
CI / test (push) Has been cancelled
fmt
2025-09-01 22:58:03 +03:00

342 lines
16 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 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::{find_closest_width, parse_file_path, thumbdata_save};
/// Создает HTTP ответ с оптимальными заголовками кэширования
fn create_cached_response(content_type: &str, data: Vec<u8>, file_etag: &str) -> HttpResponse {
HttpResponse::Ok()
.content_type(content_type)
.insert_header(("etag", file_etag))
.insert_header(("cache-control", "public, max-age=31536000, immutable")) // 1 год
.insert_header(("access-control-allow-origin", "*"))
.body(data)
}
/// Обработчик для скачивания файла и генерации миниатюры, если она недоступна.
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()
};
// Проверяем If-None-Match заголовок для кэширования
let client_etag = req
.headers()
.get("if-none-match")
.and_then(|h| h.to_str().ok());
// парсим 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
);
// Генерируем ETag для кэширования
let file_etag = format!("\"{}\"", &filekey);
if let Some(etag) = client_etag {
if etag == file_etag {
info!("Cache hit for {}, returning 304", filekey);
return Ok(HttpResponse::NotModified().finish());
}
}
let content_type = match get_mime_type(&ext) {
Some(mime) => mime.to_string(),
None => {
let mut redis = state.redis.clone();
match find_file_by_pattern(&mut redis, &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);
if content_type.starts_with("image") {
warn!("Processing image file with width: {}", requested_width);
if requested_width == 0 {
warn!("Serving original file without resizing");
serve_file(&stored_path, &state).await
} else {
let closest: u32 = find_closest_width(requested_width);
warn!(
"Calculated closest width: {} for requested: {}",
closest, requested_width
);
let thumb_filename = &format!("{}_{}.{}", base_filename, closest, ext);
warn!("Generated thumbnail filename: {}", thumb_filename);
// Проверяем, существует ли уже миниатюра в Storj
match check_file_exists(&state.storj_client, &state.bucket, thumb_filename)
.await
{
Ok(true) => {
warn!("serve existed thumb file: {}", thumb_filename);
serve_file(thumb_filename, &state).await
}
Ok(false) => {
// Миниатюра не существует, возвращаем оригинал и запускаем генерацию миниатюры
let original_file = serve_file(&stored_path, &state).await?;
// Запускаем асинхронную задачу для генерации миниатюры
let state_clone = state.clone();
let stored_path_clone = stored_path.clone();
let filekey_clone = filekey.clone();
let content_type_clone = content_type.to_string();
actix_web::rt::spawn(async move {
if let Ok(filedata) = load_file_from_s3(
&state_clone.storj_client,
&state_clone.bucket,
&stored_path_clone,
)
.await
{
warn!("generate new thumb files: {}", stored_path_clone);
if let Err(e) = thumbdata_save(
filedata,
&state_clone,
&filekey_clone,
content_type_clone,
)
.await
{
error!("Failed to generate thumbnail: {}", e);
}
}
});
Ok(original_file)
}
Err(e) => {
error!("ошибка при проверке существования миниатюры: {}", e);
Err(ErrorInternalServerError("failed to load thumbnail"))
}
}
}
} else {
warn!("File is not an image, proceeding with normal serving");
serve_file(&stored_path, &state).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);
return Ok(create_cached_response(&content_type, filedata, &file_etag));
}
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, try to generate thumbnails",
filepath
);
match load_file_from_s3(&state.aws_client, &state.bucket, &filepath).await {
Ok(filedata) => {
let _ = thumbdata_save(
filedata.clone(),
&state,
&filekey,
content_type.to_string(),
)
.await;
}
Err(e) => {
error!("cannot download {} from storj: {}", filekey, e);
return Err(ErrorInternalServerError(e));
}
}
} 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()
);
let _ = thumbdata_save(
filedata.clone(),
&state,
&filekey,
content_type.to_string(),
)
.await;
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);
Ok(create_cached_response(&content_type, filedata, &file_etag))
}
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))
}
};
}