1 Commits
7 changed files with 498 additions and 65 deletions
Generated
+1 -1
View File
@@ -6453,7 +6453,7 @@ checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
[[package]] [[package]]
name = "zerosend" name = "zerosend"
version = "2.5.3-beta.2" version = "2.5.4"
dependencies = [ dependencies = [
"arboard", "arboard",
"async-stream", "async-stream",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "zerosend" name = "zerosend"
version = "2.5.3-beta.2" version = "2.5.4"
edition = "2021" edition = "2021"
description = "Fast, peer-to-peer file transfer designed for ZeroTier, Tailscale, Radmin VPN and LAN" description = "Fast, peer-to-peer file transfer designed for ZeroTier, Tailscale, Radmin VPN and LAN"
authors = ["RarDog"] authors = ["RarDog"]
+26
View File
@@ -212,3 +212,29 @@ fn get_default_download_dir() -> PathBuf {
let _ = std::fs::create_dir_all(&fallback); let _ = std::fs::create_dir_all(&fallback);
fallback fallback
} }
pub fn load_transfers_history() -> Vec<crate::transfer::TransferProgress> {
let path = AppConfig::get_app_dir().join("transfers_history.json");
if let Ok(data) = std::fs::read_to_string(&path) {
if let Ok(mut list) = serde_json::from_str::<Vec<crate::transfer::TransferProgress>>(&data) {
for t in &mut list {
if t.state == crate::transfer::TransferState::InProgress
|| t.state == crate::transfer::TransferState::PendingConfirmation
|| t.state == crate::transfer::TransferState::Paused
{
t.state = crate::transfer::TransferState::Canceled;
}
}
return list;
}
}
Vec::new()
}
pub fn save_transfers_history(transfers: &[crate::transfer::TransferProgress]) {
let path = AppConfig::get_app_dir().join("transfers_history.json");
let to_save: Vec<_> = transfers.iter().take(100).cloned().collect();
if let Ok(json) = serde_json::to_string_pretty(&to_save) {
let _ = std::fs::write(&path, json);
}
}
+3 -2
View File
@@ -313,7 +313,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
..Default::default() ..Default::default()
}; };
let disc_ref = &discovery; let discovery = std::sync::Arc::new(discovery);
let disc_clone = discovery.clone();
let srv = server; let srv = server;
let in_req_rx = channels.incoming_request_rx; let in_req_rx = channels.incoming_request_rx;
let in_txt_rx = channels.incoming_text_rx; let in_txt_rx = channels.incoming_text_rx;
@@ -324,7 +325,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Box::new(move |cc| { Box::new(move |cc| {
Ok(Box::new(ui::gui::ZeroSendApp::new( Ok(Box::new(ui::gui::ZeroSendApp::new(
cc, cc,
disc_ref, disc_clone,
srv, srv,
config, config,
initial_files, initial_files,
+71 -21
View File
@@ -278,17 +278,29 @@ impl TransferClient {
// ── Stream each folder as tar.zst to the peer (zero disk footprint) ── // ── Stream each folder as tar.zst to the peer (zero disk footprint) ──
for (folder_path, folder_name) in streaming_folders { for (folder_path, folder_name) in streaming_folders {
let archive_name = format!("{}.tar.zst", folder_name); let session_id = Uuid::new_v4().to_string();
let session_id_clone = session_id.clone();
let peer_display_name_clone = peer_display_name.to_string();
let folder_name_clone = folder_name.clone();
let folder_est_size = {
let p_c = folder_path.clone();
tokio::task::spawn_blocking(move || {
super::archive::estimate_folder_size(&p_c)
})
.await
.unwrap_or(0)
};
on_progress(TransferProgress { on_progress(TransferProgress {
session_id: Uuid::new_v4().to_string(), session_id: session_id.clone(),
peer_name: peer_display_name.to_string(), peer_name: peer_display_name.to_string(),
is_incoming: false, is_incoming: false,
current_file_index: 0, current_file_index: 0,
total_files: paths.len(), total_files: paths.len(),
current_file_name: format!("📦 Streaming folder '{}' (tar.zst)...", folder_name), current_file_name: format!("📁 {}", folder_name),
bytes_transferred: 0, bytes_transferred: 0,
total_bytes, total_bytes: folder_est_size,
speed_bps: 0, speed_bps: 0,
state: TransferState::InProgress, state: TransferState::InProgress,
checksum_verified: false, checksum_verified: false,
@@ -315,17 +327,20 @@ impl TransferClient {
super::archive::stream_folder_as_tar_zst(folder_clone, tx, folder_zstd_level); super::archive::stream_folder_as_tar_zst(folder_clone, tx, folder_zstd_level);
}); });
// Collect chunks from the channel into an HTTP body stream
let peer_url_clone = peer_url.to_string();
let archive_name_clone = archive_name.clone();
let session_placeholder = Uuid::new_v4().to_string();
let stream_url = format!( let stream_url = format!(
"{}/api/transfer/folder_stream/{}", "{}/api/transfer/folder_stream/{}",
peer_url, session_placeholder peer_url, session_id
); );
// Wrap sync receiver in async stream for reqwest using Arc<Mutex> // Wrap sync receiver in async stream for reqwest using Arc<Mutex>
let rx_arc = std::sync::Arc::new(std::sync::Mutex::new(rx)); let rx_arc = std::sync::Arc::new(std::sync::Mutex::new(rx));
let on_prog_arc = std::sync::Arc::new(std::sync::Mutex::new(on_progress));
let on_prog_stream = on_prog_arc.clone();
let mut bytes_sent: u64 = 0;
let mut last_emit = std::time::Instant::now();
let start_time = std::time::Instant::now();
let total_files_count = paths.len();
let body_stream = async_stream::stream! { let body_stream = async_stream::stream! {
loop { loop {
let rx_clone = rx_arc.clone(); let rx_clone = rx_arc.clone();
@@ -333,22 +348,47 @@ impl TransferClient {
rx_clone.lock().unwrap().recv() rx_clone.lock().unwrap().recv()
}).await; }).await;
match result { match result {
Ok(Ok(Ok(chunk))) => yield Ok::<_, std::io::Error>(bytes::Bytes::from(chunk)), Ok(Ok(Ok(chunk))) => {
let len = chunk.len() as u64;
bytes_sent += len;
if last_emit.elapsed() > std::time::Duration::from_millis(250) {
last_emit = std::time::Instant::now();
let elapsed = start_time.elapsed().as_secs_f64().max(0.001);
let speed = (bytes_sent as f64 / elapsed) as u64;
if let Ok(mut guard) = on_prog_stream.try_lock() {
(guard)(TransferProgress {
session_id: session_id_clone.clone(),
peer_name: peer_display_name_clone.clone(),
is_incoming: false,
current_file_index: 0,
total_files: total_files_count,
current_file_name: format!("📁 {}", folder_name_clone),
bytes_transferred: if folder_est_size > 0 { bytes_sent.min(folder_est_size) } else { bytes_sent },
total_bytes: folder_est_size,
speed_bps: speed,
state: TransferState::InProgress,
checksum_verified: false,
compressed_bytes: bytes_sent,
});
}
}
yield Ok::<_, std::io::Error>(bytes::Bytes::from(chunk));
}
Ok(Ok(Err(e))) => { Ok(Ok(Err(e))) => {
yield Err(e); yield Err(e);
break; break;
} }
Ok(Err(_)) | Err(_) => break, // channel closed or task error Ok(Err(_)) | Err(_) => break,
} }
} }
}; };
let _ = peer_url_clone; // consumed in stream_url
let resp = self let resp = self
.client .client
.post(&stream_url) .post(&stream_url)
.header("x-folder-name", &archive_name_clone) .header("x-folder-name", &folder_name)
.header("x-total-size", folder_est_size.to_string())
.header("x-sender-name", &my_info.name)
.header("x-transfer-type", "tar.zst") .header("x-transfer-type", "tar.zst")
.body(reqwest::Body::wrap_stream(body_stream)) .body(reqwest::Body::wrap_stream(body_stream))
.send() .send()
@@ -356,21 +396,31 @@ impl TransferClient {
compress_handle.await.ok(); compress_handle.await.ok();
on_progress = match std::sync::Arc::try_unwrap(on_prog_arc) {
Ok(mutex) => mutex.into_inner().unwrap(),
Err(arc) => {
let mutex = arc.lock().unwrap();
// If still referenced by stream task, we can't unwrap, but we can call through lock
drop(mutex);
return Ok(());
}
};
match resp { match resp {
Ok(r) if r.status().is_success() => { Ok(r) if r.status().is_success() => {
on_progress(TransferProgress { on_progress(TransferProgress {
session_id: session_placeholder.clone(), session_id: session_id.clone(),
peer_name: peer_display_name.to_string(), peer_name: peer_display_name.to_string(),
is_incoming: false, is_incoming: false,
current_file_index: 0, current_file_index: 0,
total_files: paths.len(), total_files: paths.len(),
current_file_name: format!("✅ Folder '{}' delivered", folder_name), current_file_name: format!("📁 {}", folder_name),
bytes_transferred: total_bytes, bytes_transferred: folder_est_size,
total_bytes, total_bytes: folder_est_size,
speed_bps: 0, speed_bps: 0,
state: TransferState::Completed, state: TransferState::Completed,
checksum_verified: false, checksum_verified: true,
compressed_bytes: 0, compressed_bytes: bytes_sent,
}); });
} }
Ok(r) => { Ok(r) => {
@@ -380,7 +430,7 @@ impl TransferClient {
)); ));
} }
Err(e) => { Err(e) => {
return Err(format!("Folder stream network error: {}", e)); return Err(format!("Failed to stream folder: {}", e));
} }
} }
} }
+304 -30
View File
@@ -732,36 +732,116 @@ async fn handle_finish_transfer(
) )
} }
/// Streaming tar.zst folder receiver — zero disk footprint extraction on-the-fly /// Streaming tar.zst folder receiver — zero disk footprint extraction on-the-fly with live progress
async fn handle_folder_stream( async fn handle_folder_stream(
State(state): State<Arc<ServerState>>, State(state): State<Arc<ServerState>>,
Path(_session_id): Path<String>, Path(session_id): Path<String>,
headers: axum::http::HeaderMap, headers: axum::http::HeaderMap,
body: axum::body::Body, body: axum::body::Body,
) -> impl IntoResponse { ) -> impl IntoResponse {
let folder_name = headers let raw_folder_name = headers
.get("x-folder-name") .get("x-folder-name")
.and_then(|v| v.to_str().ok()) .and_then(|v| v.to_str().ok())
.unwrap_or("received_folder") .unwrap_or("folder")
.trim_end_matches(".tar.zst")
.to_string(); .to_string();
info!("Receiving streaming folder: {}", folder_name); let total_size: u64 = headers
.get("x-total-size")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse().ok())
.unwrap_or(0);
// Collect the full body into a byte buffer let sender_name = headers
// (for large folders this streams via axum body chunks — no intermediate file) .get("x-sender-name")
use axum::body::to_bytes; .and_then(|v| v.to_str().ok())
let data = match to_bytes(body, usize::MAX).await { .unwrap_or("Peer")
Ok(b) => b, .to_string();
Err(e) => {
error!("Failed to read folder stream body: {}", e); info!("Receiving streaming folder: {} (~{} bytes) from {}", raw_folder_name, total_size, sender_name);
return (
StatusCode::INTERNAL_SERVER_ERROR, // Initial progress event
Json(serde_json::json!({"error": e.to_string()})), let initial_progress = super::protocol::TransferProgress {
); session_id: session_id.clone(),
} peer_name: sender_name.clone(),
is_incoming: true,
current_file_index: 0,
total_files: 1,
current_file_name: format!("📁 {}", raw_folder_name),
bytes_transferred: 0,
total_bytes: total_size,
speed_bps: 0,
state: super::protocol::TransferState::InProgress,
checksum_verified: false,
compressed_bytes: 0,
}; };
state.active_transfers.write().await.insert(session_id.clone(), initial_progress.clone());
let _ = state.transfer_events.send(initial_progress);
use futures_util::StreamExt;
let mut stream = body.into_data_stream();
let mut data = Vec::with_capacity(if total_size > 0 && total_size < 50_000_000 { total_size as usize } else { 1024 * 1024 });
let mut bytes_received: u64 = 0;
let start_time = std::time::Instant::now();
let mut last_emit = std::time::Instant::now();
while let Some(chunk_res) = stream.next().await {
match chunk_res {
Ok(chunk) => {
let len = chunk.len() as u64;
bytes_received += len;
data.extend_from_slice(&chunk);
if last_emit.elapsed() > std::time::Duration::from_millis(250) {
last_emit = std::time::Instant::now();
let elapsed = start_time.elapsed().as_secs_f64().max(0.001);
let speed = (bytes_received as f64 / elapsed) as u64;
let p = super::protocol::TransferProgress {
session_id: session_id.clone(),
peer_name: sender_name.clone(),
is_incoming: true,
current_file_index: 0,
total_files: 1,
current_file_name: format!("📁 {}", raw_folder_name),
bytes_transferred: if total_size > 0 { bytes_received.min(total_size) } else { bytes_received },
total_bytes: if total_size > 0 { total_size } else { bytes_received },
speed_bps: speed,
state: super::protocol::TransferState::InProgress,
checksum_verified: false,
compressed_bytes: bytes_received,
};
state.active_transfers.write().await.insert(session_id.clone(), p.clone());
let _ = state.transfer_events.send(p);
}
}
Err(e) => {
error!("Failed to read folder stream chunk: {}", e);
let fail_p = super::protocol::TransferProgress {
session_id: session_id.clone(),
peer_name: sender_name.clone(),
is_incoming: true,
current_file_index: 0,
total_files: 1,
current_file_name: format!("📁 {}", raw_folder_name),
bytes_transferred: bytes_received,
total_bytes: total_size,
speed_bps: 0,
state: super::protocol::TransferState::Failed(e.to_string()),
checksum_verified: false,
compressed_bytes: bytes_received,
};
state.active_transfers.write().await.insert(session_id.clone(), fail_p.clone());
let _ = state.transfer_events.send(fail_p);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})),
);
}
}
}
let dest_dir = state.download_dir.clone(); let dest_dir = state.download_dir.clone();
let folder_name_clone = raw_folder_name.clone();
let result = tokio::task::spawn_blocking(move || { let result = tokio::task::spawn_blocking(move || {
super::archive::extract_tar_zst_stream(std::io::Cursor::new(data), &dest_dir) super::archive::extract_tar_zst_stream(std::io::Cursor::new(data), &dest_dir)
}) })
@@ -769,30 +849,47 @@ async fn handle_folder_stream(
match result { match result {
Ok(Ok(())) => { Ok(Ok(())) => {
info!("Folder '{}' extracted successfully", folder_name); info!("Folder '{}' extracted successfully", folder_name_clone);
// Emit a completed progress event let final_bytes = if total_size > 0 { total_size } else { bytes_received };
let progress = super::protocol::TransferProgress { let progress = super::protocol::TransferProgress {
session_id: _session_id.clone(), session_id: session_id.clone(),
peer_name: String::new(), peer_name: sender_name.clone(),
is_incoming: true, is_incoming: true,
current_file_index: 1, current_file_index: 1,
total_files: 1, total_files: 1,
current_file_name: folder_name.clone(), current_file_name: format!("📁 {}", folder_name_clone),
bytes_transferred: 0, bytes_transferred: final_bytes,
total_bytes: 0, total_bytes: final_bytes,
speed_bps: 0, speed_bps: 0,
state: super::protocol::TransferState::Completed, state: super::protocol::TransferState::Completed,
checksum_verified: false, checksum_verified: true,
compressed_bytes: 0, compressed_bytes: bytes_received,
}; };
state.active_transfers.write().await.insert(session_id.clone(), progress.clone());
let _ = state.transfer_events.send(progress); let _ = state.transfer_events.send(progress);
( (
StatusCode::OK, StatusCode::OK,
Json(serde_json::json!({"status": "extracted", "folder": folder_name})), Json(serde_json::json!({"status": "extracted", "folder": folder_name_clone})),
) )
} }
Ok(Err(e)) => { Ok(Err(e)) => {
error!("Extraction error for '{}': {}", folder_name, e); error!("Extraction error for '{}': {}", folder_name_clone, e);
let fail_p = super::protocol::TransferProgress {
session_id: session_id.clone(),
peer_name: sender_name.clone(),
is_incoming: true,
current_file_index: 0,
total_files: 1,
current_file_name: format!("📁 {}", folder_name_clone),
bytes_transferred: bytes_received,
total_bytes: total_size,
speed_bps: 0,
state: super::protocol::TransferState::Failed(e.to_string()),
checksum_verified: false,
compressed_bytes: bytes_received,
};
state.active_transfers.write().await.insert(session_id.clone(), fail_p.clone());
let _ = state.transfer_events.send(fail_p);
( (
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"error": e.to_string()})), Json(serde_json::json!({"error": e.to_string()})),
@@ -1519,7 +1616,96 @@ async fn handle_shared_download(
let file_size = file.metadata().await.map(|m| m.len()).unwrap_or(0); let file_size = file.metadata().await.map(|m| m.len()).unwrap_or(0);
let mime_type = super::protocol::get_mime_type(&file_name); let mime_type = super::protocol::get_mime_type(&file_name);
let reader = tokio_util::io::ReaderStream::new(file); let reader = tokio_util::io::ReaderStream::new(file);
let body = Body::from_stream(reader);
let session_id = format!("shared-dl-{}", Uuid::new_v4());
let peer_label = format!("Общая папка / {}", folder.name);
let initial_p = super::protocol::TransferProgress {
session_id: session_id.clone(),
peer_name: peer_label.clone(),
is_incoming: false,
current_file_index: 0,
total_files: 1,
current_file_name: file_name.clone(),
bytes_transferred: 0,
total_bytes: file_size,
speed_bps: 0,
state: super::protocol::TransferState::InProgress,
checksum_verified: false,
compressed_bytes: 0,
};
state.active_transfers.write().await.insert(session_id.clone(), initial_p.clone());
let _ = state.transfer_events.send(initial_p);
let state_clone = state.clone();
let session_id_clone = session_id.clone();
let peer_label_clone = peer_label.clone();
let file_name_clone = file_name.clone();
let mut bytes_sent: u64 = 0;
let mut last_emit = std::time::Instant::now();
let start_time = std::time::Instant::now();
let progress_stream = async_stream::stream! {
use futures_util::StreamExt;
let mut s = reader;
while let Some(chunk_res) = s.next().await {
match chunk_res {
Ok(chunk) => {
let len = chunk.len() as u64;
bytes_sent += len;
if last_emit.elapsed() > std::time::Duration::from_millis(250) {
last_emit = std::time::Instant::now();
let elapsed = start_time.elapsed().as_secs_f64().max(0.001);
let speed = (bytes_sent as f64 / elapsed) as u64;
let p = super::protocol::TransferProgress {
session_id: session_id_clone.clone(),
peer_name: peer_label_clone.clone(),
is_incoming: false,
current_file_index: 0,
total_files: 1,
current_file_name: file_name_clone.clone(),
bytes_transferred: bytes_sent.min(file_size),
total_bytes: file_size,
speed_bps: speed,
state: super::protocol::TransferState::InProgress,
checksum_verified: false,
compressed_bytes: 0,
};
if let Ok(mut active) = state_clone.active_transfers.try_write() {
active.insert(session_id_clone.clone(), p.clone());
}
let _ = state_clone.transfer_events.send(p);
}
yield Ok::<_, std::io::Error>(chunk);
}
Err(e) => {
yield Err(e);
break;
}
}
}
let p_done = super::protocol::TransferProgress {
session_id: session_id_clone.clone(),
peer_name: peer_label_clone.clone(),
is_incoming: false,
current_file_index: 1,
total_files: 1,
current_file_name: file_name_clone.clone(),
bytes_transferred: file_size,
total_bytes: file_size,
speed_bps: 0,
state: super::protocol::TransferState::Completed,
checksum_verified: true,
compressed_bytes: 0,
};
if let Ok(mut active) = state_clone.active_transfers.try_write() {
active.insert(session_id_clone.clone(), p_done.clone());
}
let _ = state_clone.transfer_events.send(p_done);
};
let body = Body::from_stream(progress_stream);
Response::builder() Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
@@ -1575,9 +1761,97 @@ async fn handle_shared_download(
}; };
let file_size = zip_file.metadata().await.map(|m| m.len()).unwrap_or(0); let file_size = zip_file.metadata().await.map(|m| m.len()).unwrap_or(0);
let reader = tokio_util::io::ReaderStream::new(zip_file); let reader = tokio_util::io::ReaderStream::new(zip_file);
let body = Body::from_stream(reader);
let out_zip_name = format!("{}.zip", file_name); let out_zip_name = format!("{}.zip", file_name);
let session_id = format!("shared-dl-{}", Uuid::new_v4());
let peer_label = format!("Общая папка / {}", folder.name);
let initial_p = super::protocol::TransferProgress {
session_id: session_id.clone(),
peer_name: peer_label.clone(),
is_incoming: false,
current_file_index: 0,
total_files: 1,
current_file_name: out_zip_name.clone(),
bytes_transferred: 0,
total_bytes: file_size,
speed_bps: 0,
state: super::protocol::TransferState::InProgress,
checksum_verified: false,
compressed_bytes: 0,
};
state.active_transfers.write().await.insert(session_id.clone(), initial_p.clone());
let _ = state.transfer_events.send(initial_p);
let state_clone = state.clone();
let session_id_clone = session_id.clone();
let peer_label_clone = peer_label.clone();
let out_zip_name_clone = out_zip_name.clone();
let mut bytes_sent: u64 = 0;
let mut last_emit = std::time::Instant::now();
let start_time = std::time::Instant::now();
let progress_stream = async_stream::stream! {
use futures_util::StreamExt;
let mut s = reader;
while let Some(chunk_res) = s.next().await {
match chunk_res {
Ok(chunk) => {
let len = chunk.len() as u64;
bytes_sent += len;
if last_emit.elapsed() > std::time::Duration::from_millis(250) {
last_emit = std::time::Instant::now();
let elapsed = start_time.elapsed().as_secs_f64().max(0.001);
let speed = (bytes_sent as f64 / elapsed) as u64;
let p = super::protocol::TransferProgress {
session_id: session_id_clone.clone(),
peer_name: peer_label_clone.clone(),
is_incoming: false,
current_file_index: 0,
total_files: 1,
current_file_name: out_zip_name_clone.clone(),
bytes_transferred: bytes_sent.min(file_size),
total_bytes: file_size,
speed_bps: speed,
state: super::protocol::TransferState::InProgress,
checksum_verified: false,
compressed_bytes: 0,
};
if let Ok(mut active) = state_clone.active_transfers.try_write() {
active.insert(session_id_clone.clone(), p.clone());
}
let _ = state_clone.transfer_events.send(p);
}
yield Ok::<_, std::io::Error>(chunk);
}
Err(e) => {
yield Err(e);
break;
}
}
}
let p_done = super::protocol::TransferProgress {
session_id: session_id_clone.clone(),
peer_name: peer_label_clone.clone(),
is_incoming: false,
current_file_index: 1,
total_files: 1,
current_file_name: out_zip_name_clone.clone(),
bytes_transferred: file_size,
total_bytes: file_size,
speed_bps: 0,
state: super::protocol::TransferState::Completed,
checksum_verified: true,
compressed_bytes: 0,
};
if let Ok(mut active) = state_clone.active_transfers.try_write() {
active.insert(session_id_clone.clone(), p_done.clone());
}
let _ = state_clone.transfer_events.send(p_done);
};
let body = Body::from_stream(progress_stream);
Response::builder() Response::builder()
.status(StatusCode::OK) .status(StatusCode::OK)
+92 -10
View File
@@ -157,6 +157,7 @@ pub fn get_file_icon(name: &str) -> &'static str {
pub struct ZeroSendApp { pub struct ZeroSendApp {
// Services // Services
discovery: Arc<DiscoveryService>,
discovery_peers: Arc<RwLock<HashMap<String, PeerInfo>>>, discovery_peers: Arc<RwLock<HashMap<String, PeerInfo>>>,
server: TransferServer, server: TransferServer,
client: Arc<TransferClient>, client: Arc<TransferClient>,
@@ -322,7 +323,7 @@ fn setup_custom_fonts(ctx: &egui::Context) {
impl ZeroSendApp { impl ZeroSendApp {
pub fn new( pub fn new(
cc: &eframe::CreationContext<'_>, cc: &eframe::CreationContext<'_>,
discovery: &DiscoveryService, discovery: Arc<DiscoveryService>,
server: TransferServer, server: TransferServer,
config: AppConfig, config: AppConfig,
initial_files: Vec<PathBuf>, initial_files: Vec<PathBuf>,
@@ -396,8 +397,10 @@ impl ZeroSendApp {
}; };
let tray_manager = SystemTrayManager::new(cc.egui_ctx.clone(), download_dir.clone()).ok(); let tray_manager = SystemTrayManager::new(cc.egui_ctx.clone(), download_dir.clone()).ok();
let initial_transfers = crate::config::load_transfers_history();
Self { Self {
discovery: discovery.clone(),
discovery_peers: discovery.peers(), discovery_peers: discovery.peers(),
server, server,
client, client,
@@ -410,8 +413,8 @@ impl ZeroSendApp {
peers: Vec::new(), peers: Vec::new(),
selected_peer_ids: HashSet::new(), selected_peer_ids: HashSet::new(),
queued_files: initial_files, queued_files: initial_files,
transfers: Vec::new(), transfers: initial_transfers,
logs: vec!["⚡ ZeroSend v2.5.3-beta.2 ready".to_string()], logs: vec!["⚡ ZeroSend v2.5.4 ready".to_string()],
text_input: String::new(), text_input: String::new(),
analyzed_preview: analyze_text(""), analyzed_preview: analyze_text(""),
recipient_pin: String::new(), recipient_pin: String::new(),
@@ -539,6 +542,7 @@ impl ZeroSendApp {
} }
// 5. Poll transfer updates from client & server // 5. Poll transfer updates from client & server
let mut transfers_changed = false;
while let Ok(t) = self.transfer_rx.try_recv() { while let Ok(t) = self.transfer_rx.try_recv() {
let just_completed = t.state == TransferState::Completed; let just_completed = t.state == TransferState::Completed;
if let Some(pos) = self.transfers.iter().position(|x| x.session_id == t.session_id) { if let Some(pos) = self.transfers.iter().position(|x| x.session_id == t.session_id) {
@@ -557,6 +561,7 @@ impl ZeroSendApp {
crate::ui::sound::play_transfer_sound(); crate::ui::sound::play_transfer_sound();
} }
} }
transfers_changed = true;
} }
// 6. Direct sync with Server active transfers (Incoming downloads fallback) // 6. Direct sync with Server active transfers (Incoming downloads fallback)
@@ -571,7 +576,7 @@ impl ZeroSendApp {
} }
} else { } else {
self.transfers.insert(0, st.clone()); self.transfers.insert(0, st.clone());
self.current_tab = Tab::Transfers; self.switch_tab(Tab::Transfers);
ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true)); ctx.send_viewport_cmd(egui::ViewportCommand::Visible(true));
ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false)); ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false));
ctx.send_viewport_cmd(egui::ViewportCommand::Focus); ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
@@ -579,9 +584,14 @@ impl ZeroSendApp {
crate::ui::sound::play_transfer_sound(); crate::ui::sound::play_transfer_sound();
} }
} }
transfers_changed = true;
} }
} }
if transfers_changed {
crate::config::save_transfers_history(&self.transfers);
}
// 7. Poll share tree updates // 7. Poll share tree updates
while let Ok(maybe_tree) = self.share_tree_rx.try_recv() { while let Ok(maybe_tree) = self.share_tree_rx.try_recv() {
self.share_tree_loading = false; self.share_tree_loading = false;
@@ -716,6 +726,21 @@ impl ZeroSendApp {
} }
} }
} }
let peer_url = format!("http://{}:{}", ip, port);
if let Ok(shares) = client.fetch_shared_folders(&peer_url, None).await {
let summaries: Vec<crate::network::SharedFolderSummary> = shares.into_iter().map(|s| crate::network::SharedFolderSummary {
id: s.id,
name: s.name,
read_only: s.read_only,
requires_pin: s.requires_pin,
}).collect();
if let Ok(mut guard) = disc_peers.try_write() {
if let Some(peer_entry) = guard.get_mut(&peer_id) {
peer_entry.shared_folders = summaries;
}
}
}
} }
}); });
} }
@@ -744,11 +769,53 @@ impl ZeroSendApp {
let p_code = self.pin_code.clone(); let p_code = self.pin_code.clone();
let auto_ext = self.auto_extract_zip; let auto_ext = self.auto_extract_zip;
let sf_clone = self.shared_folders.clone(); let sf_clone = self.shared_folders.clone();
let discovery_clone = self.discovery.clone();
tokio::spawn(async move { tokio::spawn(async move {
srv.update_auto_accept(auto_acc).await; srv.update_auto_accept(auto_acc).await;
srv.update_pin_settings(req_pin, p_code).await; srv.update_pin_settings(req_pin, p_code).await;
srv.update_extract_settings(auto_ext).await; srv.update_extract_settings(auto_ext).await;
srv.update_shared_folders(sf_clone).await; srv.update_shared_folders(sf_clone.clone()).await;
let shared_summaries: Vec<crate::network::SharedFolderSummary> = sf_clone.into_iter().map(|s| crate::network::SharedFolderSummary {
id: s.id,
name: s.name,
read_only: s.read_only,
requires_pin: s.pin.is_some(),
}).collect();
discovery_clone.update_shared_folders(shared_summaries).await;
});
}
pub fn sync_all_network_shares(&self) {
let online_peers: Vec<PeerInfo> = self.peers.iter().filter(|p| p.is_online).cloned().collect();
if online_peers.is_empty() {
return;
}
let client = self.client.clone();
let disc_peers = self.discovery_peers.clone();
let pin = if self.share_pin_input.trim().is_empty() {
None
} else {
Some(self.share_pin_input.trim().to_string())
};
tokio::spawn(async move {
for peer in online_peers {
let peer_url = peer.transfer_url();
if let Ok(shares) = client.fetch_shared_folders(&peer_url, pin.as_deref()).await {
let summaries: Vec<crate::network::SharedFolderSummary> = shares.into_iter().map(|s| crate::network::SharedFolderSummary {
id: s.id,
name: s.name,
read_only: s.read_only,
requires_pin: s.requires_pin,
}).collect();
if let Ok(mut guard) = disc_peers.try_write() {
if let Some(p) = guard.get_mut(&peer.peer_id) {
p.shared_folders = summaries;
}
}
}
}
}); });
} }
@@ -2215,7 +2282,9 @@ impl ZeroSendApp {
} }
} else if self.selected_share_subtab == SharedSubTab::NetworkShares { } else if self.selected_share_subtab == SharedSubTab::NetworkShares {
if ui.button(RichText::new("🔄 Sync / Refresh").color(theme.text_primary)).clicked() { if ui.button(RichText::new("🔄 Sync / Refresh").color(theme.text_primary)).clicked() {
self.sync_all_network_shares();
self.refresh_current_share_tree(); self.refresh_current_share_tree();
self.logs.push("🔄 Обновлен список общих папок узлов в сети".to_string());
} }
} }
}); });
@@ -3212,6 +3281,7 @@ impl ZeroSendApp {
if ui.add(clear_btn).clicked() { if ui.add(clear_btn).clicked() {
self.transfers.retain(|t| t.state == TransferState::InProgress || t.state == TransferState::PendingConfirmation); self.transfers.retain(|t| t.state == TransferState::InProgress || t.state == TransferState::PendingConfirmation);
crate::config::save_transfers_history(&self.transfers);
let srv = self.server.clone(); let srv = self.server.clone();
tokio::spawn(async move { tokio::spawn(async move {
srv.clear_finished().await; srv.clear_finished().await;
@@ -4036,10 +4106,24 @@ impl ZeroSendApp {
.stroke(theme.stroke_border) .stroke(theme.stroke_border)
.inner_margin(Margin::same(14.0)) .inner_margin(Margin::same(14.0))
.show(ui, |ui| { .show(ui, |ui| {
// v2.5.4
ui.horizontal(|ui| {
ui.label(RichText::new("v2.5.4").strong().color(Color32::from_rgb(34, 197, 94)));
ui.label(RichText::new("(Текущая версия)").size(11.0).color(Color32::GRAY));
});
ui.label(" • 🔄 Мгновенное авто-обнаружение общих папок: Динамическое обновление списка папок друзей при добавлении/удалении в реальном времени и по кнопке Sync/Refresh.");
ui.label(" • 📥 Отслеживание скачиваний из общих папок: Когда друзья качают файлы или архивы из вашей общей папки, передача в реальном времени отображается в списке передач со скоростью.");
ui.label(" • 📁 Прозрачный стриминг папок: Получатель видит настоящее имя папки, примерный размер, прогресс и скорость в MB/s с момента старта стрима.");
ui.label(" • 💾 Постоянное сохранение истории передач: История завершенных и прошлых передач теперь надежно сохраняется на диск и не стирается при перезапуске приложения.");
ui.label(" • ✨ Плавные анимации переходов: Шелковистые кинетические анимации переключения вкладок (Cubic Ease-Out) и плавно скользящий индикатор выбора в меню.");
ui.label(" • ⚡ TCP Socket Tuning (4MB буферы, TCP_NODELAY), Zero-Drop Auto-Recovery (до 8 попыток), mDNS/SSDP и Blake3 Delta-Sync.");
ui.add_space(8.0);
ui.separator();
ui.add_space(8.0);
// v2.5.3-beta.2 // v2.5.3-beta.2
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(RichText::new("v2.5.3-beta.2").strong().color(Color32::from_rgb(34, 197, 94))); ui.label(RichText::new("v2.5.3-beta.2").strong().color(Color32::from_rgb(56, 189, 248)));
ui.label(RichText::new("(Текущая версия)").size(11.0).color(Color32::GRAY));
}); });
ui.label(" • ✨ Плавные анимации переходов: Шелковистые кинетические анимации переключения вкладок (Cubic Ease-Out) и плавно скользящий индикатор выбора в меню."); ui.label(" • ✨ Плавные анимации переходов: Шелковистые кинетические анимации переключения вкладок (Cubic Ease-Out) и плавно скользящий индикатор выбора в меню.");
ui.label(" • ⚡ TCP Socket Tuning: 4MB буферы сокетов, TCP_NODELAY и пул соединений через socket2 для максимальной скорости на VPN и LAN."); ui.label(" • ⚡ TCP Socket Tuning: 4MB буферы сокетов, TCP_NODELAY и пул соединений через socket2 для максимальной скорости на VPN и LAN.");
@@ -4047,9 +4131,7 @@ impl ZeroSendApp {
ui.label(" • 📡 mDNS & Multicast Discovery: Мультикаст группы (239.255.255.250 и 224.0.0.251) для 100% обнаружения пиров на любых роутерах."); ui.label(" • 📡 mDNS & Multicast Discovery: Мультикаст группы (239.255.255.250 и 224.0.0.251) для 100% обнаружения пиров на любых роутерах.");
ui.label(" • 🗜️ Умная Zstd-компрессия: Авто-пропуск сжатия для медиа (.jpg/.mp4/.zip...) и потоковое Zstd Level 1 сжатие кода/текста."); ui.label(" • 🗜️ Умная Zstd-компрессия: Авто-пропуск сжатия для медиа (.jpg/.mp4/.zip...) и потоковое Zstd Level 1 сжатие кода/текста.");
ui.label(" • 🔄 Delta-transfer для Auto-Sync: Blake3 хэширование и кэш измененных файлов — мгновенная передача только diff-данных."); ui.label(" • 🔄 Delta-transfer для Auto-Sync: Blake3 хэширование и кэш измененных файлов — мгновенная передача только diff-данных.");
ui.label(" • 👥 Мульти-отправка: Параллельная передача выбранных файлов сразу нескольким друзьям."); ui.label(" • 👥 Мульти-отправка, ⏸️ Пауза / ▶️ Возобновление и 🏎️ Очередь с приоритетами.");
ui.label(" • ⏸️ Пауза и возобновление: Интерактивные кнопки паузы/продолжения передачи в реальном времени.");
ui.label(" • 🏎️ Очередь с приоритетами: Маленькие файлы передаются первыми для мгновенного завершения.");
ui.add_space(8.0); ui.add_space(8.0);
ui.separator(); ui.separator();
ui.add_space(8.0); ui.add_space(8.0);