added file audio video and files
This commit is contained in:
1 parent
8cb6143bac
commit
9016889a78
11 files changed
+988
-7
No files matched your search
@@ -4,6 +4,7 @@ use crate::utils;
|
||||
use axum::extract::{Json, State, ws::Message};
|
||||
use sqlx::Row;
|
||||
use std::sync::Arc;
|
||||
use tokio::fs;
|
||||
|
||||
// Delete message endpoint
|
||||
pub async fn delete_message_handler(
|
||||
@@ -66,6 +67,32 @@ pub async fn delete_message_handler(
|
||||
});
|
||||
}
|
||||
|
||||
// Получаем тип сообщения и его содержимое
|
||||
let message_details = sqlx::query("SELECT message_type, content FROM messages WHERE id = ?")
|
||||
.bind(&payload.message_id)
|
||||
.fetch_optional(&state.db_pool)
|
||||
.await;
|
||||
|
||||
let (message_type, content) = match message_details {
|
||||
Ok(Some(row)) => (
|
||||
row.get::<String, _>("message_type"),
|
||||
row.get::<String, _>("content"),
|
||||
),
|
||||
Ok(None) => {
|
||||
return Json(model::DeleteMessageResponse {
|
||||
success: false,
|
||||
message: "Message not found".to_string(),
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching message details: {}", e);
|
||||
return Json(model::DeleteMessageResponse {
|
||||
success: false,
|
||||
message: "Database error".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if payload.for_everyone {
|
||||
// Удаление для всех
|
||||
if from_user != username {
|
||||
@@ -75,6 +102,28 @@ pub async fn delete_message_handler(
|
||||
});
|
||||
}
|
||||
|
||||
// Если это сообщение с файлом, удаляем физический файл
|
||||
if message_type != "text" {
|
||||
// Создаем временную структуру для извлечения пути к файлу
|
||||
#[derive(serde::Deserialize)]
|
||||
struct FilePathExtractor {
|
||||
filepath: String,
|
||||
}
|
||||
|
||||
if let Ok(file_extractor) = serde_json::from_str::<FilePathExtractor>(&content) {
|
||||
let file_path =
|
||||
format!("{}{}", state.config.files.web_dir, file_extractor.filepath);
|
||||
|
||||
// Пытаемся удалить файл из файловой системы
|
||||
if let Err(e) = fs::remove_file(&file_path).await {
|
||||
eprintln!("Error deleting file {}: {}", file_path, e);
|
||||
// Продолжаем выполнение, даже если удаление файла не удалось
|
||||
} else {
|
||||
println!("Deleted file: {}", file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = sqlx::query("UPDATE messages SET deleted_for_everyone = TRUE WHERE id = ?")
|
||||
.bind(&payload.message_id)
|
||||
.execute(&state.db_pool)
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
use crate::model::{AudioInfo, FileInfo, PhotoInfo, VideoInfo};
|
||||
use crate::state;
|
||||
use crate::utils;
|
||||
use axum::extract::{Multipart, State};
|
||||
use axum::response::Json;
|
||||
use image::GenericImageView;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
pub async fn upload_file_handler(
|
||||
State(state): State<Arc<state::AppState>>,
|
||||
headers: axum::http::HeaderMap,
|
||||
mut multipart: Multipart,
|
||||
) -> Json<serde_json::Value> {
|
||||
// Extract token from header
|
||||
let token = if let Some(auth_header) = headers.get("authorization") {
|
||||
auth_header.to_str().unwrap_or("").replace("Bearer ", "")
|
||||
} else {
|
||||
return Json(serde_json::json!({"success": false, "error": "No token"}));
|
||||
};
|
||||
|
||||
// Get current user from token
|
||||
match utils::verify_token(&state.db_pool, &token).await {
|
||||
Ok(Some(_user)) => {} // We verify the token is valid, but don't need the username for file upload
|
||||
_ => return Json(serde_json::json!({"success": false, "error": "Invalid token"})),
|
||||
};
|
||||
|
||||
// Process uploaded file
|
||||
while let Some(field) = match multipart.next_field().await {
|
||||
Ok(field_opt) => field_opt,
|
||||
Err(e) => {
|
||||
eprintln!("Error reading multipart field: {}", e);
|
||||
return Json(
|
||||
serde_json::json!({"success": false, "error": "Error reading multipart data"}),
|
||||
);
|
||||
}
|
||||
} {
|
||||
let name = field.name().unwrap_or("unknown").to_string();
|
||||
if name == "file" {
|
||||
// Get field properties before consuming the field
|
||||
let filename = field.file_name().unwrap_or("unknown").to_string();
|
||||
let content_type = field
|
||||
.content_type()
|
||||
.unwrap_or("application/octet-stream")
|
||||
.to_string();
|
||||
|
||||
let file_data = match field.bytes().await {
|
||||
Ok(data) => {
|
||||
if data.is_empty() {
|
||||
eprintln!(
|
||||
"Received empty file data for file: {}, content-type: {}",
|
||||
filename, content_type
|
||||
);
|
||||
return Json(
|
||||
serde_json::json!({"success": false, "error": "Empty file received"}),
|
||||
);
|
||||
}
|
||||
|
||||
// Check file size against the configured limit
|
||||
if data.len() > state.config.files.max_upload_size {
|
||||
eprintln!(
|
||||
"File {} exceeds size limit. Size: {} bytes, Limit: {} bytes",
|
||||
filename,
|
||||
data.len(),
|
||||
state.config.files.max_upload_size
|
||||
);
|
||||
return Json(
|
||||
serde_json::json!({"success": false, "error": "File exceeds size limit"}),
|
||||
);
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"Successfully read {} bytes for file: {}, content-type: {}",
|
||||
data.len(),
|
||||
filename,
|
||||
content_type
|
||||
);
|
||||
data
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Error reading file data for {}: {} - Request may not contain proper file data",
|
||||
filename, e
|
||||
);
|
||||
eprintln!("Content-Type: {}, Filename: {}", content_type, filename);
|
||||
return Json(
|
||||
serde_json::json!({"success": false, "error": format!("No valid file data found in request for {}", filename)}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Determine file type and validate
|
||||
let file_type = determine_file_type(&content_type, &filename);
|
||||
|
||||
if file_type.is_none() {
|
||||
return Json(
|
||||
serde_json::json!({"success": false, "error": "Unsupported file type"}),
|
||||
);
|
||||
}
|
||||
|
||||
// Generate unique filename to prevent conflicts
|
||||
let ext = Path::new(&filename)
|
||||
.extension()
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
.unwrap_or("tmp");
|
||||
let unique_filename = format!(
|
||||
"{}_{}.{}",
|
||||
chrono::Utc::now().timestamp(),
|
||||
uuid::Uuid::new_v4().simple(),
|
||||
ext
|
||||
);
|
||||
|
||||
// Create upload directory if it doesn't exist
|
||||
let upload_dir = &state.config.files.upload_dir;
|
||||
if let Err(e) = fs::create_dir_all(upload_dir).await {
|
||||
eprintln!("Error creating upload directory: {}", e);
|
||||
return Json(
|
||||
serde_json::json!({"success": false, "error": "Error creating upload directory"}),
|
||||
);
|
||||
}
|
||||
|
||||
// Save file
|
||||
let filepath = format!("{}/{}", upload_dir, unique_filename);
|
||||
eprintln!("Saving file to: {}", filepath);
|
||||
let filepathweb = format!("/uploads/{}", unique_filename); // Web-accessible path - files are in web/uploads
|
||||
let mut file = match fs::File::create(&filepath).await {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!("Error creating file: {}", e);
|
||||
return Json(
|
||||
serde_json::json!({"success": false, "error": "Error creating file"}),
|
||||
);
|
||||
}
|
||||
};
|
||||
if let Err(e) = file.write_all(&file_data).await {
|
||||
eprintln!("Error writing file: {}", e);
|
||||
return Json(serde_json::json!({"success": false, "error": "Error writing file"}));
|
||||
}
|
||||
drop(file); // Close the file
|
||||
|
||||
// Get file size
|
||||
let metadata = match fs::metadata(&filepath).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
eprintln!("Error getting file metadata: {}", e);
|
||||
return Json(
|
||||
serde_json::json!({"success": false, "error": "Error getting file metadata"}),
|
||||
);
|
||||
}
|
||||
};
|
||||
let size = metadata.len();
|
||||
|
||||
// Prepare response based on file type
|
||||
let response = match file_type.unwrap() {
|
||||
FileType::Image => {
|
||||
// Try to get image dimensions
|
||||
let img_result = image::open(&filepath);
|
||||
let (width, height) = if let Ok(img) = img_result {
|
||||
let (w, h) = img.dimensions();
|
||||
(w, h)
|
||||
} else {
|
||||
(0, 0) // fallback if we can't read dimensions
|
||||
};
|
||||
|
||||
let photo_info = PhotoInfo {
|
||||
filename: filename.clone(),
|
||||
filepath: filepathweb.clone(),
|
||||
size,
|
||||
width,
|
||||
height,
|
||||
mime_type: content_type.clone(),
|
||||
};
|
||||
|
||||
Json(serde_json::json!({
|
||||
"success": true,
|
||||
"file_type": "photo",
|
||||
"info": photo_info
|
||||
}))
|
||||
}
|
||||
FileType::Video => {
|
||||
// For now, we'll just return basic info for videos
|
||||
// In a real implementation, you might want to extract duration using ffmpeg
|
||||
let video_info = VideoInfo {
|
||||
filename: filename.clone(),
|
||||
filepath: filepathweb.clone(),
|
||||
size,
|
||||
duration: None, // Duration extraction would require additional libraries
|
||||
mime_type: content_type.clone(),
|
||||
};
|
||||
|
||||
Json(serde_json::json!({
|
||||
"success": true,
|
||||
"file_type": "video",
|
||||
"info": video_info
|
||||
}))
|
||||
}
|
||||
FileType::Audio => {
|
||||
// For now, we'll just return basic info for audio
|
||||
// In a real implementation, you might want to extract duration using ffmpeg
|
||||
let audio_info = AudioInfo {
|
||||
filename: filename.clone(),
|
||||
filepath: filepathweb.clone(),
|
||||
size,
|
||||
duration: None, // Duration extraction would require additional libraries
|
||||
mime_type: content_type.clone(),
|
||||
};
|
||||
|
||||
Json(serde_json::json!({
|
||||
"success": true,
|
||||
"file_type": "audio",
|
||||
"info": audio_info
|
||||
}))
|
||||
}
|
||||
FileType::Generic => {
|
||||
let file_info = FileInfo {
|
||||
filename: filename.clone(),
|
||||
filepath: filepathweb.clone(),
|
||||
size,
|
||||
mime_type: content_type.clone(),
|
||||
};
|
||||
|
||||
Json(serde_json::json!({
|
||||
"success": true,
|
||||
"file_type": "file",
|
||||
"info": file_info
|
||||
}))
|
||||
}
|
||||
};
|
||||
return response; // Return the response from the function
|
||||
}
|
||||
}
|
||||
|
||||
Json(serde_json::json!({"success": false, "error": "No file found"}))
|
||||
}
|
||||
|
||||
enum FileType {
|
||||
Image,
|
||||
Video,
|
||||
Audio,
|
||||
Generic,
|
||||
}
|
||||
|
||||
fn determine_file_type(content_type: &str, filename: &str) -> Option<FileType> {
|
||||
let lower_ct = content_type.to_lowercase();
|
||||
let lower_fn = filename.to_lowercase();
|
||||
|
||||
// Check content type first
|
||||
if lower_ct.starts_with("image/") {
|
||||
return Some(FileType::Image);
|
||||
} else if lower_ct.starts_with("video/") {
|
||||
return Some(FileType::Video);
|
||||
} else if lower_ct.starts_with("audio/") {
|
||||
return Some(FileType::Audio);
|
||||
}
|
||||
|
||||
// Fallback to file extension - expanded list of supported formats
|
||||
if lower_fn.ends_with(".jpg")
|
||||
|| lower_fn.ends_with(".jpeg")
|
||||
|| lower_fn.ends_with(".png")
|
||||
|| lower_fn.ends_with(".gif")
|
||||
|| lower_fn.ends_with(".webp")
|
||||
|| lower_fn.ends_with(".bmp")
|
||||
|| lower_fn.ends_with(".svg")
|
||||
|| lower_fn.ends_with(".tiff")
|
||||
|| lower_fn.ends_with(".tif")
|
||||
{
|
||||
return Some(FileType::Image);
|
||||
} else if lower_fn.ends_with(".mp4")
|
||||
|| lower_fn.ends_with(".avi")
|
||||
|| lower_fn.ends_with(".mov")
|
||||
|| lower_fn.ends_with(".mkv")
|
||||
|| lower_fn.ends_with(".webm")
|
||||
|| lower_fn.ends_with(".wmv")
|
||||
|| lower_fn.ends_with(".flv")
|
||||
|| lower_fn.ends_with(".m4v")
|
||||
|| lower_fn.ends_with(".3gp")
|
||||
|| lower_fn.ends_with(".mpeg")
|
||||
|| lower_fn.ends_with(".mpg")
|
||||
{
|
||||
return Some(FileType::Video);
|
||||
} else if lower_fn.ends_with(".mp3")
|
||||
|| lower_fn.ends_with(".wav")
|
||||
|| lower_fn.ends_with(".ogg")
|
||||
|| lower_fn.ends_with(".flac")
|
||||
|| lower_fn.ends_with(".m4a")
|
||||
|| lower_fn.ends_with(".aac")
|
||||
|| lower_fn.ends_with(".wma")
|
||||
|| lower_fn.ends_with(".opus")
|
||||
{
|
||||
return Some(FileType::Audio);
|
||||
}
|
||||
|
||||
Some(FileType::Generic)
|
||||
}
|
||||
Reference in new issue
Block a user