a.0.0.4
This commit is contained in:
1 parent
5723bfc532
commit
e88dc07696
5 files changed
+613
-104
No files matched your search
@@ -1,6 +1,7 @@
|
||||
use crate::models::{
|
||||
sanitize_filename, AppState, IncomingTextPayload, MediaItem, PeerDevice, TextItem,
|
||||
UiManualPeerRequest, UiSendTextRequest, UiTrustRequest, MEDIA_DIR,
|
||||
now_sec, parse_ip_range, sanitize_filename, url_decode, AppState, IncomingTextPayload,
|
||||
MediaItem, PeerDevice, ScanProgress, TextItem, TransferProgress, UiManualPeerRequest,
|
||||
UiScanRangeRequest, UiSendTextRequest, UiTrustRequest, MEDIA_DIR,
|
||||
};
|
||||
use axum::{
|
||||
body::Bytes,
|
||||
@@ -9,7 +10,8 @@ use axum::{
|
||||
response::{Html, IntoResponse},
|
||||
Json,
|
||||
};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use std::{net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
|
||||
use tokio::sync::Semaphore;
|
||||
|
||||
const INDEX_HTML: &str = include_str!("static/index.html");
|
||||
|
||||
@@ -18,17 +20,14 @@ pub async fn ui_index() -> Html<&'static str> {
|
||||
}
|
||||
|
||||
pub async fn ui_get_state(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
|
||||
let mut peers_list: Vec<PeerDevice> = state
|
||||
.peers
|
||||
.read()
|
||||
.unwrap()
|
||||
.values()
|
||||
.cloned()
|
||||
.collect();
|
||||
let mut peers_list: Vec<PeerDevice> =
|
||||
state.peers.read().unwrap().values().cloned().collect();
|
||||
peers_list.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
let texts: Vec<TextItem> = state.ram_texts.read().unwrap().iter().cloned().collect();
|
||||
let media: Vec<MediaItem> = state.disk_media.read().unwrap().iter().cloned().collect();
|
||||
let transfers: Vec<TransferProgress> = state.transfers.read().unwrap().clone();
|
||||
let scan = state.scan_status.read().unwrap().clone();
|
||||
let logs: Vec<String> = state.logs.read().unwrap().iter().cloned().collect();
|
||||
|
||||
Json(serde_json::json!({
|
||||
@@ -40,6 +39,8 @@ pub async fn ui_get_state(State(state): State<Arc<AppState>>) -> Json<serde_json
|
||||
"peers": peers_list,
|
||||
"texts": texts,
|
||||
"media": media,
|
||||
"transfers": transfers,
|
||||
"scan": scan,
|
||||
"logs": logs
|
||||
}))
|
||||
}
|
||||
@@ -84,6 +85,114 @@ pub async fn ui_add_manual_peer(
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
/// Асинхронное сканирование всего или части диапазона IP-адресов
|
||||
pub async fn ui_scan_range(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(req): Json<UiScanRangeRequest>,
|
||||
) -> Result<StatusCode, (StatusCode, String)> {
|
||||
{
|
||||
let scan = state.scan_status.read().unwrap();
|
||||
if scan.active {
|
||||
return Err((StatusCode::CONFLICT, "Сканирование уже выполняется".into()));
|
||||
}
|
||||
}
|
||||
|
||||
let port = req.port.unwrap_or(state.p2p_port);
|
||||
let (range_label, ips) =
|
||||
parse_ip_range(&req.range, &state.my_ip).map_err(|e| (StatusCode::BAD_REQUEST, e))?;
|
||||
|
||||
let total = ips.len();
|
||||
{
|
||||
let mut scan = state.scan_status.write().unwrap();
|
||||
*scan = ScanProgress {
|
||||
active: true,
|
||||
range: range_label.clone(),
|
||||
scanned: 0,
|
||||
total,
|
||||
found: 0,
|
||||
};
|
||||
}
|
||||
|
||||
state.add_log(format!(
|
||||
"Запущено сканирование диапазона {} ({} адресов, порт {})",
|
||||
range_label, total, port
|
||||
));
|
||||
|
||||
let state_bg = state.clone();
|
||||
tokio::spawn(async move {
|
||||
let semaphore = Arc::new(Semaphore::new(48));
|
||||
let mut handles = Vec::with_capacity(total);
|
||||
|
||||
for ip in ips {
|
||||
let sem = semaphore.clone();
|
||||
let st = state_bg.clone();
|
||||
handles.push(tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await.unwrap();
|
||||
|
||||
// Быстрая проверка открытости TCP-порта (350 мс) перед HTTPS-рукопожатием
|
||||
let sock_addr: Option<SocketAddr> = format!("{}:{}", ip, port).parse().ok();
|
||||
let mut found_node = false;
|
||||
|
||||
if let Some(addr) = sock_addr {
|
||||
if tokio::time::timeout(
|
||||
Duration::from_millis(350),
|
||||
tokio::net::TcpStream::connect(addr),
|
||||
)
|
||||
.await
|
||||
.map(|r| r.is_ok())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let url = format!("https://{}:{}/p2p/info", ip, port);
|
||||
if let Ok(resp) = st
|
||||
.probe_client
|
||||
.get(&url)
|
||||
.header("X-Node-Fp", &st.my_fingerprint)
|
||||
.header("X-Node-Name", &st.device_name)
|
||||
.header("X-Node-Ip", &st.my_ip)
|
||||
.header("X-Node-Port", st.p2p_port.to_string())
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
if let Ok(info) = resp.json::<serde_json::Value>().await {
|
||||
let name = info["name"].as_str().unwrap_or("Node").to_string();
|
||||
let fp = info["fingerprint"].as_str().unwrap_or("").to_string();
|
||||
let r_port = info["port"].as_u64().unwrap_or(port as u64) as u16;
|
||||
if fp.len() >= 8 {
|
||||
st.upsert_peer(name, ip, r_port, fp);
|
||||
found_node = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut scan = st.scan_status.write().unwrap();
|
||||
scan.scanned += 1;
|
||||
if found_node {
|
||||
scan.found += 1;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
|
||||
let found_total = {
|
||||
let mut scan = state_bg.scan_status.write().unwrap();
|
||||
scan.active = false;
|
||||
scan.found
|
||||
};
|
||||
|
||||
state_bg.add_log(format!(
|
||||
"Сканирование {} завершено. Найдено устройств: {}",
|
||||
range_label, found_total
|
||||
));
|
||||
});
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
pub async fn ui_ping_peers(State(state): State<Arc<AppState>>) -> StatusCode {
|
||||
let state_clone = state.clone();
|
||||
tokio::spawn(async move {
|
||||
@@ -131,7 +240,10 @@ pub async fn ui_send_text(
|
||||
if !peer.verified {
|
||||
return Err((
|
||||
StatusCode::FORBIDDEN,
|
||||
format!("Сначала заверьте сертификат устройства '{}' [PIN: {}]!", peer.name, peer.pin_code),
|
||||
format!(
|
||||
"Сначала заверьте сертификат устройства '{}' [PIN: {}]!",
|
||||
peer.name, peer.pin_code
|
||||
),
|
||||
));
|
||||
}
|
||||
let ip = peer
|
||||
@@ -165,6 +277,7 @@ pub async fn ui_send_text(
|
||||
}
|
||||
}
|
||||
|
||||
/// Почанковая отправка медиафайла с реальным отслеживанием прогресса по сети
|
||||
pub async fn ui_send_media(
|
||||
State(state): State<Arc<AppState>>,
|
||||
headers: HeaderMap,
|
||||
@@ -175,15 +288,19 @@ pub async fn ui_send_media(
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or((StatusCode::BAD_REQUEST, "Не указан ID получателя".to_string()))?;
|
||||
|
||||
let filename = headers
|
||||
let raw_filename = headers
|
||||
.get("X-Filename")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("file.bin");
|
||||
.unwrap_or("file.bin")
|
||||
.to_string();
|
||||
|
||||
let display_name = sanitize_filename(&url_decode(&raw_filename));
|
||||
|
||||
let mime = headers
|
||||
.get(header::CONTENT_TYPE)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("application/octet-stream");
|
||||
.unwrap_or("application/octet-stream")
|
||||
.to_string();
|
||||
|
||||
let (target_ip, port, peer_name) = {
|
||||
let peers = state.peers.read().unwrap();
|
||||
@@ -193,7 +310,10 @@ pub async fn ui_send_media(
|
||||
if !peer.verified {
|
||||
return Err((
|
||||
StatusCode::FORBIDDEN,
|
||||
format!("Сначала заверьте сертификат устройства '{}' [PIN: {}]!", peer.name, peer.pin_code),
|
||||
format!(
|
||||
"Сначала заверьте сертификат устройства '{}' [PIN: {}]!",
|
||||
peer.name, peer.pin_code
|
||||
),
|
||||
));
|
||||
}
|
||||
let ip = peer
|
||||
@@ -204,25 +324,103 @@ pub async fn ui_send_media(
|
||||
(ip, peer.port, peer.name.clone())
|
||||
};
|
||||
|
||||
let total_size = body.len();
|
||||
let transfer_id = format!("tx_{}_{}", now_sec(), total_size);
|
||||
let url = format!("https://{}:{}/p2p/media", target_ip, port);
|
||||
|
||||
let resp = state
|
||||
.http_client
|
||||
.post(&url)
|
||||
.header("X-Sender", &state.device_name)
|
||||
.header("X-Filename", filename)
|
||||
.header(header::CONTENT_TYPE, mime)
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("Ошибка TLS/Сети: {}", e)))?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
state.add_log(format!("Медиафайл отправлен на диск устройства '{}'", peer_name));
|
||||
Ok(StatusCode::OK)
|
||||
// Адаптивный размер чанка для плавного обновления прогресс-бара на любых файлах
|
||||
let chunk_size = if total_size < 1024 * 1024 {
|
||||
32 * 1024 // 32 КБ для файлов до 1 МБ
|
||||
} else if total_size < 15 * 1024 * 1024 {
|
||||
128 * 1024 // 128 КБ для файлов до 15 МБ
|
||||
} else {
|
||||
Err((resp.status(), "Удаленный узел отклонил медиафайл".to_string()))
|
||||
512 * 1024 // 512 КБ для крупных файлов
|
||||
};
|
||||
|
||||
state.upsert_transfer(TransferProgress {
|
||||
id: transfer_id.clone(),
|
||||
filename: display_name.clone(),
|
||||
peer_name: peer_name.clone(),
|
||||
direction: "out".to_string(),
|
||||
transferred_bytes: 0,
|
||||
total_bytes: total_size,
|
||||
percent: 0,
|
||||
done: false,
|
||||
error: None,
|
||||
updated_at: now_sec(),
|
||||
});
|
||||
|
||||
let mut offset = 0usize;
|
||||
loop {
|
||||
let end = (offset + chunk_size).min(total_size);
|
||||
let chunk = body.slice(offset..end);
|
||||
|
||||
let send_res = state
|
||||
.http_client
|
||||
.post(&url)
|
||||
.header("X-Sender", &state.device_name)
|
||||
.header("X-Filename", &raw_filename)
|
||||
.header("X-Transfer-Id", &transfer_id)
|
||||
.header("X-Offset", offset.to_string())
|
||||
.header("X-Total-Size", total_size.to_string())
|
||||
.header(header::CONTENT_TYPE, &mime)
|
||||
.body(chunk)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match send_res {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
offset = end;
|
||||
let is_done = offset >= total_size;
|
||||
let percent = if total_size == 0 {
|
||||
100
|
||||
} else {
|
||||
((offset as f64 / total_size as f64) * 100.0).min(100.0) as u8
|
||||
};
|
||||
|
||||
state.upsert_transfer(TransferProgress {
|
||||
id: transfer_id.clone(),
|
||||
filename: display_name.clone(),
|
||||
peer_name: peer_name.clone(),
|
||||
direction: "out".to_string(),
|
||||
transferred_bytes: offset,
|
||||
total_bytes: total_size,
|
||||
percent,
|
||||
done: is_done,
|
||||
error: None,
|
||||
updated_at: now_sec(),
|
||||
});
|
||||
|
||||
if is_done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(resp) => {
|
||||
let err_msg = format!("Узел отклонил чанк (код {})", resp.status());
|
||||
state.remove_transfer(&transfer_id);
|
||||
return Err((resp.status(), err_msg));
|
||||
}
|
||||
Err(e) => {
|
||||
let err_msg = format!("Ошибка сети при передаче файла: {}", e);
|
||||
state.remove_transfer(&transfer_id);
|
||||
return Err((StatusCode::BAD_GATEWAY, err_msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.add_log(format!(
|
||||
"Медиафайл '{}' ({} байт) успешно доставлен на '{}'",
|
||||
display_name, total_size, peer_name
|
||||
));
|
||||
|
||||
let st_cleanup = state.clone();
|
||||
let tid_cleanup = transfer_id.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(4)).await;
|
||||
st_cleanup.remove_transfer(&tid_cleanup);
|
||||
});
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
pub async fn ui_serve_saved_file(Path(filename): Path<String>) -> impl IntoResponse {
|
||||
|
||||
Reference in new issue
Block a user