use actix_multipart::Multipart; use actix_web::{HttpRequest, HttpResponse, Result, web}; use log::{error, info, warn}; use crate::app_state::AppState; use crate::auth::{extract_user_id_from_token, user_added_file}; use crate::handlers::MAX_USER_QUOTA_BYTES; use crate::lookup::store_file_info; use crate::s3_utils::{self, generate_key_with_extension, upload_to_s3}; use super::common::extract_and_validate_token; use futures::TryStreamExt; // use crate::thumbnail::convert_heic_to_jpeg; // Максимальный размер одного файла: 500 МБ const MAX_SINGLE_FILE_BYTES: u64 = 500 * 1024 * 1024; /// Обработчик для аплоада файлов с улучшенной логикой квот и валидацией. pub async fn upload_handler( req: HttpRequest, mut payload: Multipart, state: web::Data, ) -> Result { // Извлекаем и валидируем токен let token = extract_and_validate_token(&req)?; // Затем извлекаем user_id let user_id = extract_user_id_from_token(token).map_err(|e| { warn!("Failed to extract user_id from token: {}", e); actix_web::error::ErrorUnauthorized("Invalid authorization token") })?; // Получаем текущую квоту пользователя let current_quota: u64 = state.get_or_create_quota(&user_id).await.unwrap_or(0); info!("Author {} current quota: {} bytes", user_id, current_quota); // Предварительная проверка: есть ли вообще место для файлов if current_quota >= MAX_USER_QUOTA_BYTES { warn!( "Author {} quota already at maximum: {}", user_id, current_quota ); return Err(actix_web::error::ErrorPayloadTooLarge( "Author quota limit exceeded", )); } let mut uploaded_files = Vec::new(); while let Ok(Some(field)) = payload.try_next().await { let mut field = field; let mut file_bytes = Vec::new(); let mut file_size: u64 = 0; // Читаем данные файла с проверкой размера while let Ok(Some(chunk)) = field.try_next().await { let chunk_size = chunk.len() as u64; // Проверка лимита одного файла if file_size + chunk_size > MAX_SINGLE_FILE_BYTES { warn!( "File size exceeds single file limit: {} > {}", file_size + chunk_size, MAX_SINGLE_FILE_BYTES ); return Err(actix_web::error::ErrorPayloadTooLarge( "Single file size limit exceeded", )); } // Проверка общей квоты пользователя if current_quota + file_size + chunk_size > MAX_USER_QUOTA_BYTES { warn!( "Upload would exceed user quota: current={}, adding={}, limit={}", current_quota, file_size + chunk_size, MAX_USER_QUOTA_BYTES ); return Err(actix_web::error::ErrorPayloadTooLarge( "Author quota limit would be exceeded", )); } file_size += chunk_size; file_bytes.extend_from_slice(&chunk); } // Пропускаем пустые файлы if file_size == 0 { warn!("Skipping empty file upload"); continue; } info!("Processing file: {} bytes", file_size); // Определяем MIME-тип из содержимого файла let detected_mime_type = match s3_utils::detect_mime_type(&file_bytes) { Some(mime) => { info!("Detected MIME type: {}", mime); mime } None => { warn!("Unsupported file format detected"); return Err(actix_web::error::ErrorUnsupportedMediaType( "Unsupported file format", )); } }; // Для HEIC файлов просто сохраняем как есть let (file_bytes, content_type) = if detected_mime_type == "image/heic" { info!("Processing HEIC file (saved as original)"); (file_bytes, detected_mime_type) } else { (file_bytes, detected_mime_type) }; // Получаем расширение из MIME-типа let extension = match s3_utils::get_extension_from_mime(&content_type) { Some(ext) => ext, None => { warn!("No file extension found for MIME type: {}", content_type); return Err(actix_web::error::ErrorUnsupportedMediaType( "Unsupported content type", )); } }; // Генерируем имя файла с правильным расширением let filename = format!("{}.{}", uuid::Uuid::new_v4(), extension); info!("Generated filename: {}", filename); // Загружаем файл в S3 storj match upload_to_s3( &state.storj_client, &state.bucket, &filename, file_bytes, &content_type, ) .await { Ok(_) => { info!( "File {} successfully uploaded to S3 ({} bytes)", filename, file_size ); // Обновляем квоту пользователя if let Err(e) = state.increment_uploaded_bytes(&user_id, file_size).await { error!("Failed to increment quota for user {}: {}", user_id, e); return Err(actix_web::error::ErrorInternalServerError( "Failed to update user quota", )); } // Сохраняем информацию о файле в Redis let mut redis = state.redis.clone(); if let Err(e) = store_file_info(&mut redis, &filename, &content_type).await { error!("Failed to store file info in Redis: {}", e); // Не прерываем процесс, файл уже загружен в S3 } if let Err(e) = user_added_file(&mut redis, &user_id, &filename).await { error!("Failed to record user file association: {}", e); // Не прерываем процесс } // Сохраняем маппинг пути let generated_key = generate_key_with_extension(filename.clone(), content_type.clone()); state.set_path(&filename, &generated_key).await; // Логируем новую квоту if let Ok(new_quota) = state.get_or_create_quota(&user_id).await { info!( "Updated quota for user {}: {} bytes ({:.1}% used)", user_id, new_quota, (new_quota as f64 / MAX_USER_QUOTA_BYTES as f64) * 100.0 ); } uploaded_files.push(filename); } Err(e) => { error!("Failed to upload file to S3: {}", e); return Err(actix_web::error::ErrorInternalServerError( "File upload failed", )); } } } // Возвращаем результат match uploaded_files.len() { 0 => { warn!("No files were uploaded"); Err(actix_web::error::ErrorBadRequest( "No files provided or all files were empty", )) } 1 => { info!("Successfully uploaded 1 file: {}", uploaded_files[0]); Ok(HttpResponse::Ok().body(uploaded_files[0].clone())) } n => { info!("Successfully uploaded {} files", n); Ok(HttpResponse::Ok().json(serde_json::json!({ "uploaded_files": uploaded_files, "count": n }))) } } }