added file audio video and files
Continuous integration / Check (push) Failing after 10s
Continuous integration / Rustfmt (push) Successful in 36s
Continuous integration / build (push) Successful in 36s
Continuous integration / build-release (push) Successful in 57s

This commit is contained in:
wt committed 2026-02-15 16:55:51 +07:00
1 parent 8cb6143bac
commit 9016889a78
11 files changed
+988 -7

No files matched your search

+49
View File
@@ -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)
+296
View File
@@ -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)
}