From 9fcce860753ffbaaed075f347aec76c5c27b2bdb Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 22 Oct 2024 00:36:42 +0300 Subject: [PATCH] handlers-refactored --- Cargo.lock | 2 +- src/app_state.rs | 9 +- src/auth.rs | 6 +- src/handlers.rs | 221 ------------------------------------- src/handlers/mod.rs | 10 ++ src/handlers/proxy.rs | 107 ++++++++++++++++++ src/handlers/serve_file.rs | 40 +++++++ src/handlers/upload.rs | 78 +++++++++++++ src/s3_utils.rs | 4 +- 9 files changed, 245 insertions(+), 232 deletions(-) delete mode 100644 src/handlers.rs create mode 100644 src/handlers/mod.rs create mode 100644 src/handlers/proxy.rs create mode 100644 src/handlers/serve_file.rs create mode 100644 src/handlers/upload.rs diff --git a/Cargo.lock b/Cargo.lock index 1d7ff9f..a6dc9cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "discoursio-quoter" -version = "0.0.4" +version = "0.0.5" dependencies = [ "actix-multipart", "actix-web", diff --git a/src/app_state.rs b/src/app_state.rs index 83616aa..b4322c3 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -46,7 +46,7 @@ impl AppState { env::var("AWS_END_POINT").unwrap_or_else(|_| "https://s3.amazonaws.com".to_string()); let aws_bucket = env::var("AWS_BUCKET_NAME").unwrap_or_else(|_| "discours-io".to_string()); - // Конфигурируем клиент S3 для Storj + // Конфигу��ируем клиент S3 для Storj let storj_config = aws_config::defaults(BehaviorVersion::latest()) .region("eu-west-1") .endpoint_url(s3_endpoint) @@ -166,10 +166,10 @@ impl AppState { } /// Получает путь в хранилище из ключа (имени файла) в Redis. - pub async fn get_path(&self, filename_with_extension: &str) -> Result, actix_web::Error> { + pub async fn get_path(&self, file_key: &str) -> Result, actix_web::Error> { let mut redis = self.redis.clone(); let new_path: Option = redis - .hget(PATH_MAPPING_KEY, filename_with_extension) + .hget(PATH_MAPPING_KEY, file_key) .await .map_err(|_| ErrorInternalServerError("Failed to get path mapping from Redis"))?; Ok(new_path) @@ -199,8 +199,7 @@ impl AppState { eprint!("[ERROR] empty filename: {}", key); } else { // Проверяем, существует ли файл на Storj S3 - match check_file_exists(&self.s3_client, &self.s3_bucket, &filename_with_extension) - .await + match check_file_exists(&self.s3_client, &self.s3_bucket, &key).await { Ok(false) => { // Сохраняем маппинг пути diff --git a/src/auth.rs b/src/auth.rs index 7d4cc20..e6c4c17 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -85,11 +85,11 @@ pub async fn get_id_by_token(token: &str) -> Result> { pub async fn user_added_file( redis: &mut MultiplexedConnection, user_id: &str, - filename: &str, + file_key: &str, ) -> Result<(), actix_web::Error> { redis - .sadd::<&str, &str, ()>(user_id, filename) + .sadd::<&str, &str, ()>(user_id, file_key) .await - .map_err(|_| ErrorInternalServerError("Failed to save filename in Redis"))?; // Добавляем имя файла в набор пользователя + .map_err(|_| ErrorInternalServerError("Failed to save file_key in Redis"))?; // Добавляем имя файла в набор пользователя Ok(()) } diff --git a/src/handlers.rs b/src/handlers.rs deleted file mode 100644 index 7862d11..0000000 --- a/src/handlers.rs +++ /dev/null @@ -1,221 +0,0 @@ -use actix_multipart::Multipart; -use actix_web::{error::ErrorInternalServerError, web, HttpRequest, HttpResponse, Result}; -use futures::StreamExt; -use mime_guess::MimeGuess; -use log::{info, warn, error}; - -use crate::app_state::AppState; -use crate::auth::{get_id_by_token, user_added_file}; -use crate::s3_utils::{ - check_file_exists, generate_key_with_extension, load_file_from_s3, upload_to_s3, -}; -use crate::thumbnail::{ - find_closest_width, generate_thumbnails, parse_thumbnail_request, ALLOWED_THUMBNAIL_WIDTHS, -}; - -// Лимит квоты на пользователя: 2 ГБ в неделю -pub const MAX_WEEK_BYTES: u64 = 2 * 1024 * 1024 * 1024; - -/// Функция для обслуживания файла по заданному пути. -async fn serve_file(filename_with_extension: &str, state: &AppState) -> Result { - // Проверяем наличие файла в Storj S3 - if !check_file_exists(&state.s3_client, &state.s3_bucket, filename_with_extension).await? { - warn!("{}", filename_with_extension); - return Err(ErrorInternalServerError("File not found in S3")); - } - - let checked_filekey = state.get_path(filename_with_extension).await.unwrap().unwrap(); - - // Получаем объект из Storj S3 - let get_object_output = state - .s3_client - .get_object() - .bucket(&state.s3_bucket) - .key(checked_filekey) - .send() - .await - .map_err(|_| ErrorInternalServerError("Failed to get object from S3"))?; - - let data: aws_sdk_s3::primitives::AggregatedBytes = get_object_output - .body - .collect() - .await - .map_err(|_| ErrorInternalServerError("Failed to read object body"))?; - - let data_bytes = data.into_bytes(); - let mime_type = MimeGuess::from_path(filename_with_extension).first_or_octet_stream(); // Определяем MIME-тип файла - - Ok(HttpResponse::Ok() - .content_type(mime_type.as_ref()) - .body(data_bytes)) -} - -/// Обработчик для аплоада файлов. -pub async fn upload_handler( - req: HttpRequest, - mut payload: Multipart, - state: web::Data, -) -> Result { - // Получаем токен из заголовка авторизации - let token = req - .headers() - .get("Authorization") - .and_then(|header_value| header_value.to_str().ok()); - if token.is_none() { - return Err(actix_web::error::ErrorUnauthorized("Unauthorized")); // Если токен отсутствует, возвращаем ошибку - } - - let user_id = get_id_by_token(token.unwrap()).await?; - - // Получаем текущую квоту пользователя - let this_week_amount: u64 = state.get_or_create_quota(&user_id).await.unwrap_or(0); - - while let Some(field) = payload.next().await { - let mut field = field?; - let content_type = field.content_type().unwrap().to_string(); - let file_name = field - .content_disposition() - .unwrap() - .get_filename() - .map(|f| f.to_string()); - - if let Some(name) = file_name { - let mut file_bytes = Vec::new(); - let mut file_size: u64 = 0; - - // Читаем данные файла - while let Some(chunk) = field.next().await { - let data = chunk?; - file_size += data.len() as u64; - file_bytes.extend_from_slice(&data); - } - - // Проверяем, что добавление файла не превышает лимит квоты - if this_week_amount + file_size > MAX_WEEK_BYTES { - return Err(actix_web::error::ErrorUnauthorized("Quota exceeded")); - // Квота превышена - } - - // инкрементируем квоту пользователя - let _ = state.increment_uploaded_bytes(&user_id, file_size).await?; - - // Определяем правильное расширение и ключ для S3 - let file_key = generate_key_with_extension(name, content_type.to_owned()); - - // Загружаем файл в S3 - upload_to_s3( - &state.s3_client, - &state.s3_bucket, - &file_key, - file_bytes, - &content_type, - ) - .await?; - - // Сохраняем информацию о загруженном файле для пользователя - user_added_file(&mut state.redis.clone(), &user_id, &file_key).await?; - } - } - - Ok(HttpResponse::Ok().json("File uploaded successfully")) -} - -/// Обработчик для скачивания файла и генерации миниатюры, если она недоступна. -pub async fn proxy_handler( - _req: HttpRequest, - path: web::Path, - state: web::Data, -) -> Result { - info!("proxy_handler: {}", path); - - let requested_path = match state.get_path(&path).await { - Ok(Some(path)) => path, - Ok(None) => { - warn!("Путь не найден: {}", path); - return Ok(HttpResponse::NotFound().finish()); - } - Err(e) => { - warn!("Ошибка: {}", e); - return Ok(HttpResponse::InternalServerError().finish()); - } - }; - info!("Запрошенный путь: {}", requested_path); - - // имя файла - let filename_with_extension = requested_path.split("/").last().ok_or_else(|| { - actix_web::error::ErrorInternalServerError("Неверный формат пути") - })?; - info!("Имя файла с расширением: {}", filename_with_extension); - - // Разделяем имя и расширение файла, сохраняя их для дальнейшего использования - let (requested_filekey, extension) = filename_with_extension - .rsplit_once('.') - .map(|(name, ext)| (name.to_string(), Some(ext.to_lowercase()))) - .unwrap_or((filename_with_extension.to_string(), None)); - info!("Запрошенный ключ файла: {}", requested_filekey); - if let Some(ext) = &extension { - info!("Расширение файла: {}", ext); - } - - // Проверяем, запрошена ли миниатюра - if let Some((base_filename, requested_width, _ext)) = - parse_thumbnail_request(&requested_filekey) - { - info!("Запрошена миниатюра. Базовое имя файла: {}, Запрошенная ширина: {}", base_filename, requested_width); - - // Находим ближайший подходящий размер - let closest_width = find_closest_width(requested_width); - let thumbnail_key = format!("{}_{}", base_filename, closest_width); - info!("Ближайшая ширина: {}, Кюч миниатюры: {}", closest_width, thumbnail_key); - - // Проверяем наличие миниатюры в кэше - let cached_files = state.get_cached_file_list().await; - if !cached_files.contains(&thumbnail_key) { - info!("Миниатюра не найдена в кэше"); - if cached_files.contains(&base_filename) { - info!("Оригинальный файл найден в кэше, генерируем миниатюру"); - // Загружаем оригинальный файл из S3 - let original_data = - load_file_from_s3(&state.s3_client, &state.s3_bucket, &base_filename).await?; - - // Генерируем миниатюру для ближайшего подходящего размера - let image = image::load_from_memory(&original_data).map_err(|_| { - ErrorInternalServerError("Failed to load image for thumbnail generation") - })?; - let thumbnails_bytes = - generate_thumbnails(&image, &ALLOWED_THUMBNAIL_WIDTHS).await?; - let thumbnail_bytes = thumbnails_bytes[&closest_width].clone(); - - // Загружаем миниатюру в S3 - upload_to_s3( - &state.s3_client, - &state.s3_bucket, - &thumbnail_key, - thumbnail_bytes.clone(), - "image/jpeg", - ) - .await?; - info!("Миниатюра сгенерирована и загружена в S3"); - return Ok(HttpResponse::Ok() - .content_type("image/jpeg") - .body(thumbnail_bytes)); - } else { - warn!("Оригинальный файл не найден в кэше"); - } - } else { - info!("Миниатюра найдена в кэше, возвращаем её"); - return serve_file(&thumbnail_key, &state).await; - } - } - - // Если запрошен целый файл - info!("Запрошен целый файл, возвращаем его"); - info!("Проверка наличия файла в S3: {}", requested_filekey); - match serve_file(&requested_filekey, &state).await { - Ok(response) => Ok(response), - Err(e) => { - error!("Ошибка файла: {}", e); - Err(e) - } - } -} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..18775d2 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,10 @@ +mod upload; +mod proxy; +mod serve_file; + +pub use upload::upload_handler; +pub use proxy::proxy_handler; +// pub use serve_file::serve_file; + +// Лимит квоты на пользователя: 2 ГБ в неделю +pub const MAX_WEEK_BYTES: u64 = 2 * 1024 * 1024 * 1024; diff --git a/src/handlers/proxy.rs b/src/handlers/proxy.rs new file mode 100644 index 0000000..a3bac8c --- /dev/null +++ b/src/handlers/proxy.rs @@ -0,0 +1,107 @@ +use actix_web::{error::ErrorInternalServerError, web, HttpRequest, HttpResponse, Result}; +use log::{info, warn, error}; + +use crate::app_state::AppState; +use crate::thumbnail::{parse_thumbnail_request, find_closest_width, generate_thumbnails, ALLOWED_THUMBNAIL_WIDTHS}; +use crate::s3_utils::{load_file_from_s3, upload_to_s3}; +use crate::handlers::serve_file::serve_file; + +/// Обработчик для скачивания файла и генерации миниатюры, если она недоступна. +pub async fn proxy_handler( + req: HttpRequest, + file_key: web::Path, + state: web::Data, +) -> Result { + info!("proxy_handler: {}", req.path()); + + let requested_path = match state.get_path(&file_key).await { + Ok(Some(path)) => path, + Ok(None) => { + warn!("Путь не найден: {}", req.path()); + return Ok(HttpResponse::NotFound().finish()); + } + Err(e) => { + warn!("Ошибка: {}", e); + return Ok(HttpResponse::InternalServerError().finish()); + } + }; + info!("Запрошенный путь: {}", requested_path); + + // имя файла + let filename_with_extension = requested_path.split("/").last().ok_or_else(|| { + actix_web::error::ErrorInternalServerError("Неверный формат пути") + })?; + info!("Имя файла с расширением: {}", filename_with_extension); + + // Разделяем имя и расширение файла, сохраняя их для дальнейшего использования + let (requested_filekey, extension) = filename_with_extension + .rsplit_once('.') + .map(|(name, ext)| (name.to_string(), Some(ext.to_lowercase()))) + .unwrap_or((filename_with_extension.to_string(), None)); + info!("Запрошенный ключ файла: {}", requested_filekey); + if let Some(ext) = &extension { + info!("Расширение файла: {}", ext); + } + + // Проверяем, запрошена ли миниатюра + if let Some((base_filename, requested_width, _ext)) = + parse_thumbnail_request(&requested_filekey) + { + info!("Запрошена миниатюра. Базов��е имя файла: {}, Запрошенная ширина: {}", base_filename, requested_width); + + // Находим ближайший подходящий размер + let closest_width = find_closest_width(requested_width); + let thumbnail_key = format!("{}_{}", base_filename, closest_width); + info!("Ближайшая ширина: {}, Кюч миниатюры: {}", closest_width, thumbnail_key); + + // Проверяем наличие миниатюры в кэше + let cached_files = state.get_cached_file_list().await; + if !cached_files.contains(&thumbnail_key) { + info!("Миниатюра не найдена в кэше"); + if cached_files.contains(&base_filename) { + info!("Оригинальный файл найден в кэше, генерируем миниатюру"); + // Загружаем оригинальный файл из S3 + let original_data: Vec = + load_file_from_s3(&state.s3_client, &state.s3_bucket, &base_filename).await?; + + // Генерируем миниатюру для ближайшего подходящего размера + let image = image::load_from_memory(&original_data).map_err(|_| { + ErrorInternalServerError("Failed to load image for thumbnail generation") + })?; + let thumbnails_bytes = + generate_thumbnails(&image, &ALLOWED_THUMBNAIL_WIDTHS).await?; + let thumbnail_bytes = thumbnails_bytes[&closest_width].clone(); + + // Загружаем миниатюру в S3 + upload_to_s3( + &state.s3_client, + &state.s3_bucket, + &thumbnail_key, + thumbnail_bytes.clone(), + "image/jpeg", + ) + .await?; + info!("Миниатюра сгенерирована и загружена в S3"); + return Ok(HttpResponse::Ok() + .content_type("image/jpeg") + .body(thumbnail_bytes)); + } else { + warn!("Оригинальный файл не найден в кэше"); + } + } else { + info!("Миниатюра найдена в кэше, возвращаем её"); + return serve_file(&thumbnail_key, &state).await; + } + } + + // Если запрошен целый файл + info!("Запрошен целый файл, возвращаем его"); + info!("Проверка наличия файла в S3: {}", requested_filekey); + match serve_file(&requested_filekey, &state).await { + Ok(response) => Ok(response), + Err(e) => { + error!("Ошибка файла: {}", e); + Err(e) + } + } +} diff --git a/src/handlers/serve_file.rs b/src/handlers/serve_file.rs new file mode 100644 index 0000000..cba4456 --- /dev/null +++ b/src/handlers/serve_file.rs @@ -0,0 +1,40 @@ +use actix_web::{error::ErrorInternalServerError, HttpResponse, Result}; +use mime_guess::MimeGuess; +use log::warn; + +use crate::app_state::AppState; +use crate::s3_utils::check_file_exists; + +/// Функция для обслуживания файла по заданному пути. +pub async fn serve_file(file_key: &str, state: &AppState) -> Result { + // Проверяем наличие файла в Storj S3 + if !check_file_exists(&state.s3_client, &state.s3_bucket, &file_key).await? { + warn!("{}", file_key); + return Err(ErrorInternalServerError("File not found in S3")); + } + + let file_path = state.get_path(file_key).await.unwrap().unwrap(); + + // Получаем объект из Storj S3 + let get_object_output = state + .s3_client + .get_object() + .bucket(&state.s3_bucket) + .key(file_path.clone()) + .send() + .await + .map_err(|_| ErrorInternalServerError("Failed to get object from S3"))?; + + let data: aws_sdk_s3::primitives::AggregatedBytes = get_object_output + .body + .collect() + .await + .map_err(|_| ErrorInternalServerError("Failed to read object body"))?; + + let data_bytes = data.into_bytes(); + let mime_type = MimeGuess::from_path(&file_path).first_or_octet_stream(); + + Ok(HttpResponse::Ok() + .content_type(mime_type.as_ref()) + .body(data_bytes)) +} diff --git a/src/handlers/upload.rs b/src/handlers/upload.rs new file mode 100644 index 0000000..6388594 --- /dev/null +++ b/src/handlers/upload.rs @@ -0,0 +1,78 @@ +use actix_multipart::Multipart; +use actix_web::{web, HttpRequest, HttpResponse, Result}; + +use crate::app_state::AppState; +use crate::auth::{get_id_by_token, user_added_file}; +use crate::s3_utils::{generate_key_with_extension, upload_to_s3}; +use futures::TryStreamExt; +use crate::handlers::MAX_WEEK_BYTES; + +/// Обработчик для аплоада файлов. +pub async fn upload_handler( + req: HttpRequest, + mut payload: Multipart, + state: web::Data, +) -> Result { + // Получаем токен из заголовка авторизации + let token = req + .headers() + .get("Authorization") + .and_then(|header_value| header_value.to_str().ok()); + if token.is_none() { + return Err(actix_web::error::ErrorUnauthorized("Unauthorized")); // Если токен отсутствует, возвращаем ошибку + } + + let user_id = get_id_by_token(token.unwrap()).await?; + + // Получаем текущую квоту пользователя + let this_week_amount: u64 = state.get_or_create_quota(&user_id).await.unwrap_or(0); + + while let Ok(Some(field)) = payload.try_next().await { + let mut field = field; + + let content_type = field.content_type().unwrap().to_string(); + let file_name = field + .content_disposition() + .unwrap() + .get_filename() + .map(|f| f.to_string()); + + if let Some(name) = file_name { + let mut file_bytes = Vec::new(); + let mut file_size: u64 = 0; + + // Читаем данные файла + while let Ok(Some(chunk)) = field.try_next().await { + file_size += chunk.len() as u64; + file_bytes.extend_from_slice(&chunk); + } + + // Проверяем, что добавление файла не превышает лимит квоты + if this_week_amount + file_size > MAX_WEEK_BYTES { + return Err(actix_web::error::ErrorUnauthorized("Quota exceeded")); + // Квота превышена + } + + // инкрементируем квоту пользователя + let _ = state.increment_uploaded_bytes(&user_id, file_size).await?; + + // Определяем правильное расширение и ключ для S3 + let file_key = generate_key_with_extension(name, content_type.to_owned()); + + // Загружаем файл в S3 + upload_to_s3( + &state.s3_client, + &state.s3_bucket, + &file_key, + file_bytes, + &content_type, + ) + .await?; + + // Сохраняем информацию о загруженном файле для пользователя + user_added_file(&mut state.redis.clone(), &user_id, &file_key).await?; + } + } + + Ok(HttpResponse::Ok().json("File uploaded successfully")) +} diff --git a/src/s3_utils.rs b/src/s3_utils.rs index 50be364..d8d406f 100644 --- a/src/s3_utils.rs +++ b/src/s3_utils.rs @@ -29,9 +29,9 @@ pub async fn upload_to_s3( pub async fn check_file_exists( s3_client: &S3Client, bucket: &str, - key: &str, + file_key: &str, ) -> Result { - match s3_client.head_object().bucket(bucket).key(key).send().await { + match s3_client.head_object().bucket(bucket).key(file_key).send().await { Ok(_) => Ok(true), // Файл найден Err(SdkError::ServiceError(service_error)) if service_error.err().is_not_found() => { Ok(false) // Файл не найден