diff --git a/src/app_state.rs b/src/app_state.rs index b4322c3..1f2216c 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -10,14 +10,14 @@ use crate::s3_utils::check_file_exists; #[derive(Clone)] pub struct AppState { pub redis: MultiplexedConnection, - pub s3_client: S3Client, - pub s3_bucket: String, + pub storj_client: S3Client, + pub storj_bucket: String, pub aws_client: S3Client, pub aws_bucket: String, } -const FILE_LIST_CACHE_KEY: &str = "s3_file_list_cache"; // Ключ для хранения списка файлов в Redis -const PATH_MAPPING_KEY: &str = "path_mapping"; // Ключ для хранения маппинга путей +// const FILE_LIST_CACHE_KEY: &str = "s3_file_list_cache"; // Ключ для хранения списка файлов в Redis +const PATH_MAPPING_KEY: &str = "filepath_mapping"; // Ключ для хранения маппинга путей const CHECK_INTERVAL_SECONDS: u64 = 60 * 60; // Интервал обновления списка файлов: 1 час const WEEK_SECONDS: u64 = 604800; @@ -37,7 +37,7 @@ impl AppState { let s3_secret_key = env::var("STORJ_SECRET_KEY").expect("STORJ_SECRET_KEY must be set"); let s3_endpoint = env::var("STORJ_END_POINT") .unwrap_or_else(|_| "https://gateway.storjshare.io".to_string()); - let s3_bucket = env::var("STORJ_BUCKET_NAME").unwrap_or_else(|_| "discours-io".to_string()); + let storj_bucket = env::var("STORJ_BUCKET_NAME").unwrap_or_else(|_| "discours-io".to_string()); // Получаем конфигурацию для AWS S3 let aws_access_key = env::var("AWS_ACCESS_KEY").expect("AWS_ACCESS_KEY must be set"); @@ -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) @@ -55,12 +55,12 @@ impl AppState { s3_secret_key, None, None, - "rust-s3-client", + "rust-storj-client", )) .load() .await; - let s3_client = S3Client::new(&storj_config); + let storj_client = S3Client::new(&storj_config); // Конфигурируем клиент S3 для AWS let aws_config = aws_config::defaults(BehaviorVersion::latest()) @@ -80,53 +80,51 @@ impl AppState { let app_state = AppState { redis: redis_connection, - s3_client, - s3_bucket, + storj_client, + storj_bucket, aws_client, aws_bucket, }; - // Кэшируем список файлов из S3 при старте приложения - app_state.cache_file_list().await; + // Кэшируем список файлов из Storj S3 при старте приложения + app_state.cache_storj_filelist().await; app_state } /// Кэширует список файлов из Storj S3 в Redis. - pub async fn cache_file_list(&self) { + pub async fn cache_storj_filelist(&self) { let mut redis = self.redis.clone(); // Запрашиваем список файлов из Storj S3 - let list_objects_v2 = self.s3_client.list_objects_v2(); + let list_objects_v2 = self.storj_client.list_objects_v2(); let list_response = list_objects_v2 - .bucket(&self.s3_bucket) + .bucket(&self.storj_bucket) .send() .await - .expect("Failed to list files from S3"); + .expect("Failed to list files from Storj"); if let Some(objects) = list_response.contents { // Формируем список файлов без дубликатов по имени файла (без расширения) - let mut file_list = std::collections::HashMap::new(); + for object in objects.iter() { - if let Some(key) = &object.key { - let parts: Vec<&str> = key.split('.').collect(); - let filename = parts.first().unwrap_or(&""); - let ext = parts.get(1).unwrap_or(&""); - if ext.contains('/') { - continue; - } - let filename_with_extension = format!("{}.{}", filename, ext); - file_list.insert(filename_with_extension, key.clone()); + if let Some(storj_objkey) = &object.key { + let filekey = storj_objkey.split('.') + .chain(std::iter::once("")) // Ensure the chain doesn't break on empty strings + .filter(|s| !s.is_empty()) // Filter out any empty strings + .map(|s| s.split('/')) // Map to Split iterator + .nth(0) // Get the first non-empty split result or default to &"" + .and_then(|s| s.last()) // Call last() on the resulting iterator if it exists, otherwise None + .unwrap_or(&""); + + // Сохраняем список файлов в Redis, используя HSET для каждого файла + let _: () = redis + .hset(PATH_MAPPING_KEY, filekey, storj_objkey) + .await + .expect("Failed to cache file in Redis"); } } - // Сохраняем список файлов в Redis, используя HSET для каждого файла - for (filename, path) in file_list { - let _: () = redis - .hset(FILE_LIST_CACHE_KEY, filename, path) - .await - .expect("Failed to cache file in Redis"); - } } } @@ -135,7 +133,7 @@ impl AppState { let mut redis = self.redis.clone(); // Пытаемся получить кэшированный список из Redis - let cached_list: HashMap = redis.hgetall(FILE_LIST_CACHE_KEY).await.unwrap_or_default(); + let cached_list: HashMap = redis.hgetall(PATH_MAPPING_KEY).await.unwrap_or_default(); // Преобразуем HashMap в Vec, используя значения (пути файлов) cached_list.into_values().collect() @@ -146,20 +144,20 @@ impl AppState { let mut interval = interval(Duration::from_secs(CHECK_INTERVAL_SECONDS)); loop { interval.tick().await; - self.cache_file_list().await; + self.cache_storj_filelist().await; } } /// Сохраняет маппинг старого пути из AWS S3 на новый путь в Storj S3. - async fn save_path_by_filename_with_extension( + async fn save_aws2storj_mapping( &self, - filename_with_extension: &str, - path: &str, + aws_filekey: &str, + storj_filekey: &str, ) -> Result<(), actix_web::Error> { let mut redis = self.redis.clone(); // Храним маппинг в формате Hash: old_path -> new_path redis - .hset::<_, &str, &str, ()>(PATH_MAPPING_KEY, filename_with_extension, path) + .hset::<_, &str, &str, ()>(PATH_MAPPING_KEY, aws_filekey, storj_filekey) .await .map_err(|_| ErrorInternalServerError("Failed to save path mapping in Redis"))?; Ok(()) @@ -176,7 +174,7 @@ impl AppState { } /// Обновляет Storj S3 данными из Amazon S3 - pub async fn update_filelist_from_aws(&self) { + pub async fn cache_aws_filelist(&self) { // Получаем список объектов из AWS S3 let list_objects_v2 = self.aws_client.list_objects_v2(); @@ -188,23 +186,18 @@ impl AppState { if let Some(key) = object.key { // Получаем имя файла с расширением let parts: Vec<&str> = key.split('.').collect(); - let filename = parts.first().unwrap_or(&""); - let ext = parts.get(1).unwrap_or(&""); - if ext.contains('/') { - continue; - } - let filename_with_extension = format!("{}.{}", filename, ext); + let storj_filekey = parts.first().and_then(|s| s.split('/').last()).unwrap_or(parts.first().unwrap()); - if filename.is_empty() { + if storj_filekey.is_empty() { eprint!("[ERROR] empty filename: {}", key); } else { // Проверяем, существует ли файл на Storj S3 - match check_file_exists(&self.s3_client, &self.s3_bucket, &key).await + match check_file_exists(&self.storj_client, &self.storj_bucket, &storj_filekey).await { Ok(false) => { // Сохраняем маппинг пути if let Err(e) = - self.save_path_by_filename_with_extension(&filename_with_extension, &key).await + self.save_aws2storj_mapping(&key, &storj_filekey).await { eprintln!("[ERROR] save {}: {:?}", key, e); } else { @@ -212,12 +205,12 @@ impl AppState { } } Ok(true) => { - println!("Already exists in Storj: {}", filename_with_extension); + println!("Already exists in Storj: {}", storj_filekey); } Err(e) => { eprintln!( "[ERROR] check {}: {:?}", - filename_with_extension, e + storj_filekey, e ); } } diff --git a/src/handlers/proxy.rs b/src/handlers/proxy.rs index a3bac8c..5f5a0a3 100644 --- a/src/handlers/proxy.rs +++ b/src/handlers/proxy.rs @@ -2,105 +2,88 @@ use actix_web::{error::ErrorInternalServerError, web, HttpRequest, HttpResponse, 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::thumbnail::{parse_thumbnail_request, find_closest_width, generate_thumbnails}; 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, + requested_res: web::Path, state: web::Data, ) -> Result { - info!("proxy_handler: {}", req.path()); + info!("[proxy_handler] req.path: {}", req.path()); - let requested_path = match state.get_path(&file_key).await { + let requested_path = match state.get_path(&requested_res).await { Ok(Some(path)) => path, Ok(None) => { - warn!("Путь не найден: {}", req.path()); + warn!("[proxy_handler] wrong request: {}", req.path()); return Ok(HttpResponse::NotFound().finish()); } Err(e) => { - warn!("Ошибка: {}", e); + warn!("[proxy_handler] error: {}", 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); - } + info!("[proxy_handler] requested path: {}", requested_path); // Проверяем, запрошена ли миниатюра - if let Some((base_filename, requested_width, _ext)) = - parse_thumbnail_request(&requested_filekey) + if let Some((base_filename, requested_width, extension)) = + parse_thumbnail_request(&requested_res) { - info!("Запрошена миниатюра. Базов��е имя файла: {}, Запрошенная ширина: {}", base_filename, requested_width); + info!("[proxy_handler] thumbnail requested: {} width: {}, ext: {}", base_filename, requested_width, extension); // Находим ближайший подходящий размер let closest_width = find_closest_width(requested_width); - let thumbnail_key = format!("{}_{}", base_filename, closest_width); - info!("Ближайшая ширина: {}, Кюч миниатюры: {}", closest_width, thumbnail_key); + let thumb_filekey = format!("{}_{}", base_filename, closest_width); + info!("[proxy_handler] closest width: {}, thumb_filekey: {}", closest_width, thumb_filekey); // Проверяем наличие миниатюры в кэше let cached_files = state.get_cached_file_list().await; - if !cached_files.contains(&thumbnail_key) { - info!("Миниатюра не найдена в кэше"); + if !cached_files.contains(&thumb_filekey) { + info!("[proxy_handler] no thumb found"); if cached_files.contains(&base_filename) { - info!("Оригинальный файл найден в кэше, генерируем миниатюру"); + info!("[proxy_handler] no original file found"); // Загружаем оригинальный файл из S3 let original_data: Vec = - load_file_from_s3(&state.s3_client, &state.s3_bucket, &base_filename).await?; + load_file_from_s3(&state.storj_client, &state.storj_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?; + generate_thumbnails(&image).await?; let thumbnail_bytes = thumbnails_bytes[&closest_width].clone(); // Загружаем миниатюру в S3 upload_to_s3( - &state.s3_client, - &state.s3_bucket, - &thumbnail_key, + &state.storj_client, + &state.storj_bucket, + &thumb_filekey, thumbnail_bytes.clone(), "image/jpeg", ) .await?; - info!("Миниатюра сгенерирована и загружена в S3"); + info!("[proxy_handler] thumb was saved in storj"); return Ok(HttpResponse::Ok() .content_type("image/jpeg") .body(thumbnail_bytes)); } else { - warn!("Оригинальный файл не найден в кэше"); + warn!("[proxy_handler] original was not found"); } } else { - info!("Миниатюра найдена в кэше, возвращаем её"); - return serve_file(&thumbnail_key, &state).await; + info!("[proxy_handler] thumb was found"); + return serve_file(&thumb_filekey, &state).await; } } // Если запрошен целый файл - info!("Запрошен целый файл, возвращаем его"); - info!("Проверка наличия файла в S3: {}", requested_filekey); - match serve_file(&requested_filekey, &state).await { + info!("[proxy_handler] serving full file: {}", requested_path); + match serve_file(&requested_path, &state).await { Ok(response) => Ok(response), Err(e) => { - error!("Ошибка файла: {}", e); + error!("[proxy_handler] error: {}", e); Err(e) } } diff --git a/src/handlers/serve_file.rs b/src/handlers/serve_file.rs index cba4456..db70437 100644 --- a/src/handlers/serve_file.rs +++ b/src/handlers/serve_file.rs @@ -8,22 +8,22 @@ 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? { + if !check_file_exists(&state.storj_client, &state.storj_bucket, &file_key).await? { warn!("{}", file_key); - return Err(ErrorInternalServerError("File not found in S3")); + return Err(ErrorInternalServerError("File not found in Storj")); } let file_path = state.get_path(file_key).await.unwrap().unwrap(); // Получаем объект из Storj S3 let get_object_output = state - .s3_client + .storj_client .get_object() - .bucket(&state.s3_bucket) + .bucket(&state.storj_bucket) .key(file_path.clone()) .send() .await - .map_err(|_| ErrorInternalServerError("Failed to get object from S3"))?; + .map_err(|_| ErrorInternalServerError("Failed to get object from Storj"))?; let data: aws_sdk_s3::primitives::AggregatedBytes = get_object_output .body diff --git a/src/handlers/upload.rs b/src/handlers/upload.rs index 6388594..2fa9ffd 100644 --- a/src/handlers/upload.rs +++ b/src/handlers/upload.rs @@ -26,7 +26,7 @@ pub async fn upload_handler( // Получаем текущую квоту пользователя let this_week_amount: u64 = state.get_or_create_quota(&user_id).await.unwrap_or(0); - + let mut body = "ok".to_string(); while let Ok(Some(field)) = payload.try_next().await { let mut field = field; @@ -57,22 +57,21 @@ pub async fn upload_handler( let _ = state.increment_uploaded_bytes(&user_id, file_size).await?; // Определяем правильное расширение и ключ для S3 - let file_key = generate_key_with_extension(name, content_type.to_owned()); + body = generate_key_with_extension(name, content_type.to_owned()); // Загружаем файл в S3 upload_to_s3( - &state.s3_client, - &state.s3_bucket, - &file_key, + &state.storj_client, + &state.storj_bucket, + &body, file_bytes, &content_type, ) .await?; // Сохраняем информацию о загруженном файле для пользователя - user_added_file(&mut state.redis.clone(), &user_id, &file_key).await?; + user_added_file(&mut state.redis.clone(), &user_id, &body).await?; } } - - Ok(HttpResponse::Ok().json("File uploaded successfully")) + Ok(HttpResponse::Ok().body(body)) } diff --git a/src/main.rs b/src/main.rs index 7b7c68b..e817234 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,7 +22,7 @@ async fn main() -> std::io::Result<()> { spawn_blocking(move || { let rt = tokio::runtime::Handle::current(); rt.block_on(async move { - app_state_clone.update_filelist_from_aws().await; + app_state_clone.cache_aws_filelist().await; app_state_clone.refresh_file_list_periodically().await; }); }); diff --git a/src/s3_utils.rs b/src/s3_utils.rs index d8d406f..1186d0b 100644 --- a/src/s3_utils.rs +++ b/src/s3_utils.rs @@ -5,14 +5,14 @@ use std::str::FromStr; /// Загружает файл в S3 хранилище. pub async fn upload_to_s3( - s3_client: &S3Client, + storj_client: &S3Client, bucket: &str, key: &str, body: Vec, content_type: &str, ) -> Result { let body_stream = ByteStream::from(body); // Преобразуем тело файла в поток байтов - s3_client + storj_client .put_object() .bucket(bucket) .key(key) @@ -27,11 +27,11 @@ pub async fn upload_to_s3( /// Проверяет, существует ли файл в S3. pub async fn check_file_exists( - s3_client: &S3Client, + storj_client: &S3Client, bucket: &str, file_key: &str, ) -> Result { - match s3_client.head_object().bucket(bucket).key(file_key).send().await { + match storj_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) // Файл не найден @@ -42,11 +42,11 @@ pub async fn check_file_exists( /// Загружает файл из S3. pub async fn load_file_from_s3( - s3_client: &S3Client, + storj_client: &S3Client, bucket: &str, key: &str, ) -> Result, actix_web::Error> { - let get_object_output = s3_client + let get_object_output = storj_client .get_object() .bucket(bucket) .key(key) diff --git a/src/thumbnail.rs b/src/thumbnail.rs index a61b71d..9d1b670 100644 --- a/src/thumbnail.rs +++ b/src/thumbnail.rs @@ -2,7 +2,7 @@ use actix_web::error::ErrorInternalServerError; use image::{imageops::FilterType, DynamicImage}; use std::{collections::HashMap, io::Cursor}; -pub const ALLOWED_THUMBNAIL_WIDTHS: [u32; 6] = [10, 40, 110, 300, 600, 800]; +pub const THUMB_WIDTHS: [u32; 6] = [10, 40, 110, 300, 600, 800]; /// Парсит запрос на миниатюру, извлекая оригинальное имя файла и требуемую ширину. /// Пример: "filename_150.ext" -> ("filename.ext", 150) @@ -19,20 +19,17 @@ pub fn parse_thumbnail_request(path: &str) -> Option<(String, u32, String)> { /// Выбирает ближайший подходящий размер из предопределённых. pub fn find_closest_width(requested_width: u32) -> u32 { - *ALLOWED_THUMBNAIL_WIDTHS + *THUMB_WIDTHS .iter() .min_by_key(|&&width| (width as i32 - requested_width as i32).abs()) - .unwrap_or(&ALLOWED_THUMBNAIL_WIDTHS[0]) // Возвращаем самый маленький размер, если ничего не подошло + .unwrap_or(&THUMB_WIDTHS[0]) // Возвращаем самый маленький размер, если ничего не подошло } /// Генерирует миниатюры изображения для заданного набора ширин. -pub async fn generate_thumbnails( - image: &DynamicImage, - widths: &[u32], -) -> Result>, actix_web::Error> { +pub async fn generate_thumbnails(image: &DynamicImage) -> Result>, actix_web::Error> { let mut thumbnails = HashMap::new(); - for &width in widths { + for width in THUMB_WIDTHS { let thumbnail = image.resize(width, u32::MAX, FilterType::Lanczos3); // Ресайз изображения по ширине let mut buffer = Vec::new(); thumbnail