use actix_web::error::ErrorInternalServerError; use aws_config::BehaviorVersion; use aws_sdk_s3::{config::Credentials, Client as S3Client}; use redis::{aio::MultiplexedConnection, AsyncCommands, Client as RedisClient}; use std::{env, time::Duration}; use tokio::time::interval; use std::collections::HashMap; use crate::s3_utils::get_s3_filelist; #[derive(Clone)] pub struct AppState { pub redis: MultiplexedConnection, pub storj_client: S3Client, pub storj_bucket: String } const PATH_MAPPING_KEY: &str = "filepath_mapping"; // Ключ для хранения маппинга путей const CHECK_INTERVAL_SECONDS: u64 = 60 * 60; // Интервал обновления списка файлов: 1 час const WEEK_SECONDS: u64 = 604800; impl AppState { /// Инициализация нового состояния приложения. pub async fn new() -> Self { // Получаем конфигурацию для Redis let redis_url = env::var("REDIS_URL").expect("REDIS_URL must be set"); let redis_client = RedisClient::open(redis_url).expect("Invalid Redis URL"); let redis_connection = redis_client .get_multiplexed_async_connection() .await .unwrap(); // Получаем конфигурацию для S3 (Storj) let s3_access_key = env::var("STORJ_ACCESS_KEY").expect("STORJ_ACCESS_KEY must be set"); 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 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"); // let aws_secret_key = env::var("AWS_SECRET_KEY").expect("AWS_SECRET_KEY must be set"); // let aws_endpoint = // 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 let storj_config = aws_config::defaults(BehaviorVersion::latest()) .region("eu-west-1") .endpoint_url(s3_endpoint) .credentials_provider(Credentials::new( s3_access_key, s3_secret_key, None, None, "rust-storj-client", )) .load() .await; let storj_client = S3Client::new(&storj_config); // Конфигурируем клиент S3 для AWS /* let aws_config = aws_config::defaults(BehaviorVersion::latest()) .region("eu-west-1") .endpoint_url(aws_endpoint) .credentials_provider(Credentials::new( aws_access_key, aws_secret_key, None, None, "rust-aws-client", )) .load() .await; */ // let aws_client = S3Client::new(&aws_config); let app_state = AppState { redis: redis_connection, storj_client, storj_bucket }; // Кэшируем список файлов из Storj S3 при старте приложения app_state.cache_storj_filelist().await; app_state } /// Кэширует список файлов из Storj S3 в Redis. pub async fn cache_storj_filelist(&self) { let mut redis = self.redis.clone(); // Запрашиваем список файлов из Storj S3 let filekeyed_list = get_s3_filelist(&self.storj_client, &self.storj_bucket).await; for [filekey, filepath] in filekeyed_list { // Сохраняем список файлов в Redis, используя HSET для каждого файла let _: () = redis .hset(PATH_MAPPING_KEY, filekey, filepath) .await .expect(&format!("Failed to cache file {} in Redis", filekey)); } info!("cached {} files", filekeyed_list.len()); } /// Получает кэшированный список файлов из Redis. pub async fn get_cached_file_list(&self) -> Vec { let mut redis = self.redis.clone(); // Пытаемся получить кэшированный список из Redis let cached_list: HashMap = redis.hgetall(PATH_MAPPING_KEY).await.unwrap_or_default(); // Преобразуем HashMap в Vec, используя значения (пути файлов) cached_list.into_values().collect() } /// Периодически обновляет кэшированный список файлов из Storj S3. pub async fn refresh_file_list_periodically(&self) { let mut interval = interval(Duration::from_secs(CHECK_INTERVAL_SECONDS)); loop { interval.tick().await; self.cache_storj_filelist().await; } } /// Получает путь в Storj из ключа (имени файла) в Redis. 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, file_key) .await .map_err(|_| ErrorInternalServerError("Failed to get path mapping from Redis"))?; Ok(new_path) } /// создает или получает текущее значение квоты пользователя pub async fn get_or_create_quota(&self, user_id: &str) -> Result { let mut redis = self.redis.clone(); let quota_key = format!("quota:{}", user_id); // Попытка получить квоту из Redis let quota: u64 = redis.get("a_key).await.unwrap_or(0); if quota == 0 { // Если квота не найдена, устанавливаем её в 0 байт и задаем TTL на одну неделю redis .set_ex::<&str, u64, ()>("a_key, 0, WEEK_SECONDS) .await .map_err(|_| { ErrorInternalServerError("Failed to set initial user quota in Redis") })?; Ok(0) // Возвращаем 0 как начальную квоту } else { Ok(quota) } } /// инкрементирует значение квоты пользователя в байтах pub async fn increment_uploaded_bytes( &self, user_id: &str, bytes: u64, ) -> Result { let mut redis = self.redis.clone(); let quota_key = format!("quota:{}", user_id); // Проверяем, существует ли ключ в Redis let exists: bool = redis.exists::<_, bool>("a_key).await.map_err(|_| { ErrorInternalServerError("Failed to check if user quota exists in Redis") })?; // Если ключ не существует, создаем его с начальным значением и устанавливаем TTL if !exists { redis .set_ex::<_, u64, ()>("a_key, bytes, WEEK_SECONDS) .await .map_err(|_| { ErrorInternalServerError("Failed to set initial user quota in Redis") })?; return Ok(bytes); } // Если ключ существует, инкрементируем его значение на заданное количество байт let new_quota: u64 = redis .incr::<_, u64, u64>("a_key, bytes) .await .map_err(|_| ErrorInternalServerError("Failed to increment user quota in Redis"))?; Ok(new_quota) } }