124 lines
3.7 KiB
Rust
124 lines
3.7 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use std::{
|
|
collections::{HashMap, VecDeque},
|
|
sync::{Arc, RwLock},
|
|
time::{SystemTime, UNIX_EPOCH},
|
|
};
|
|
|
|
/// Лимит одного передаваемого медиафайла (200 МБ)
|
|
pub const MAX_PAYLOAD_SIZE: usize = 200 * 1024 * 1024;
|
|
/// Максимум текстовых сообщений, хранимых в ОЗУ
|
|
pub const MAX_RAM_TEXTS: usize = 50;
|
|
/// Папка на диске для сохранения входящего медиаконтента
|
|
pub const MEDIA_DIR: &str = "received_media";
|
|
/// Тип сервиса для обнаружения через mDNS
|
|
pub const SERVICE_TYPE: &str = "_ramshare._tcp.local.";
|
|
|
|
/// Текстовое сообщение (хранится ТОЛЬКО в ОЗУ)
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct TextItem {
|
|
pub sender: String,
|
|
pub text: String,
|
|
pub timestamp: u64,
|
|
}
|
|
|
|
/// Метаданные медиафайла (сам файл сохраняется НА ДИСК)
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct MediaItem {
|
|
pub sender: String,
|
|
pub filename: String,
|
|
pub saved_path: String,
|
|
pub mime: String,
|
|
pub size: usize,
|
|
pub timestamp: u64,
|
|
}
|
|
|
|
/// Устройство в локальной сети, найденное через mDNS
|
|
#[derive(Clone, Serialize, Deserialize)]
|
|
pub struct PeerNode {
|
|
pub name: String,
|
|
pub ip: String,
|
|
pub port: u16,
|
|
pub fingerprint: String,
|
|
pub pin_code: String,
|
|
pub verified: bool,
|
|
}
|
|
|
|
/// Глобальное состояние приложения
|
|
pub struct AppState {
|
|
pub device_name: String,
|
|
pub my_ip: String,
|
|
pub p2p_port: u16,
|
|
pub my_fingerprint: String,
|
|
pub ram_texts: RwLock<VecDeque<TextItem>>,
|
|
pub disk_media: RwLock<VecDeque<MediaItem>>,
|
|
pub peers: RwLock<HashMap<String, PeerNode>>,
|
|
pub trusted_fingerprints: Arc<RwLock<HashMap<String, String>>>,
|
|
pub http_client: reqwest::Client,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
pub struct IncomingTextPayload {
|
|
pub sender: String,
|
|
pub text: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct UiSendTextRequest {
|
|
pub target_ip: String,
|
|
pub text: String,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
pub struct UiTrustRequest {
|
|
pub target_ip: String,
|
|
pub trust: bool,
|
|
}
|
|
|
|
pub fn now_sec() -> u64 {
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs()
|
|
}
|
|
|
|
/// Декодирование URL-encoded имени файла (для поддержки кириллицы и пробелов)
|
|
pub fn url_decode(input: &str) -> String {
|
|
let mut bytes = Vec::with_capacity(input.len());
|
|
let mut chars = input.as_bytes().iter().copied();
|
|
while let Some(b) = chars.next() {
|
|
if b == b'%' {
|
|
if let (Some(c1), Some(c2)) = (chars.next(), chars.next()) {
|
|
if let Ok(decoded) = u8::from_str_radix(&format!("{}{}", c1 as char, c2 as char), 16)
|
|
{
|
|
bytes.push(decoded);
|
|
continue;
|
|
}
|
|
}
|
|
} else if b == b'+' {
|
|
bytes.push(b' ');
|
|
} else {
|
|
bytes.push(b);
|
|
}
|
|
}
|
|
String::from_utf8_lossy(&bytes).into_owned()
|
|
}
|
|
|
|
/// Защита от Path Traversal при сохранении медиа на диск
|
|
pub fn sanitize_filename(name: &str) -> String {
|
|
let decoded = url_decode(name);
|
|
let clean: String = decoded
|
|
.chars()
|
|
.map(|c| match c {
|
|
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' | '\0' => '_',
|
|
_ => c,
|
|
})
|
|
.collect();
|
|
let trimmed = clean.trim_matches('.').trim();
|
|
if trimmed.is_empty() {
|
|
"media_file.bin".to_string()
|
|
} else {
|
|
trimmed.to_string()
|
|
}
|
|
}
|