Initial commit: LoFlum FTP/SFTP client
Tauri 2 + Rust + Svelte 5. SFTP via ssh2, FTP via suppaftp. Transfer queue with async worker, SQLite site storage, keychain.
This commit is contained in:
parent
0f182ab76f
commit
79855585bd
60 changed files with 14532 additions and 18 deletions
32
src-tauri/src/transfer/manager.rs
Normal file
32
src-tauri/src/transfer/manager.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
use std::sync::Arc;
|
||||
use tauri::AppHandle;
|
||||
use crate::transfer::queue::TransferQueue;
|
||||
use crate::transfer::worker::spawn_worker;
|
||||
use crate::fs::remote::RemoteRegistry;
|
||||
|
||||
/// Хранит очередь передач и запускает воркер.
|
||||
pub struct TransferManager {
|
||||
pub queue: TransferQueue,
|
||||
registry: Arc<RemoteRegistry>,
|
||||
app: AppHandle,
|
||||
}
|
||||
|
||||
impl TransferManager {
|
||||
pub fn new(registry: Arc<RemoteRegistry>, app: AppHandle) -> Arc<Self> {
|
||||
let manager = Arc::new(Self {
|
||||
queue: TransferQueue::default(),
|
||||
registry: registry.clone(),
|
||||
app: app.clone(),
|
||||
});
|
||||
spawn_worker(manager.clone(), registry, app);
|
||||
manager
|
||||
}
|
||||
|
||||
pub fn registry(&self) -> &RemoteRegistry {
|
||||
&self.registry
|
||||
}
|
||||
|
||||
pub fn app(&self) -> &AppHandle {
|
||||
&self.app
|
||||
}
|
||||
}
|
||||
5
src-tauri/src/transfer/mod.rs
Normal file
5
src-tauri/src/transfer/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod manager;
|
||||
pub mod progress;
|
||||
pub mod queue;
|
||||
pub mod task;
|
||||
pub mod worker;
|
||||
35
src-tauri/src/transfer/progress.rs
Normal file
35
src-tauri/src/transfer/progress.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Throttle прогресс-событий — максимум 10 в секунду.
|
||||
/// Пользователь разницы не увидит, а тысяч emit'ов не будет.
|
||||
pub struct ProgressThrottle {
|
||||
last_emit: Instant,
|
||||
interval: Duration,
|
||||
}
|
||||
|
||||
impl Default for ProgressThrottle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_emit: Instant::now() - Duration::from_secs(1),
|
||||
interval: Duration::from_millis(100), // 10 fps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProgressThrottle {
|
||||
pub fn should_emit(&mut self) -> bool {
|
||||
let now = Instant::now();
|
||||
if now.duration_since(self.last_emit) >= self.interval {
|
||||
self.last_emit = now;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Всегда emit при завершении/ошибке
|
||||
pub fn force(&mut self) -> bool {
|
||||
self.last_emit = Instant::now() - self.interval;
|
||||
true
|
||||
}
|
||||
}
|
||||
46
src-tauri/src/transfer/queue.rs
Normal file
46
src-tauri/src/transfer/queue.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use crate::domain::transfer::{TransferTask, TaskState};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct TransferQueue {
|
||||
inner: Arc<Mutex<VecDeque<TransferTask>>>,
|
||||
}
|
||||
|
||||
impl TransferQueue {
|
||||
pub fn push(&self, task: TransferTask) {
|
||||
self.inner.lock().unwrap().push_back(task);
|
||||
}
|
||||
|
||||
pub fn pop(&self) -> Option<TransferTask> {
|
||||
self.inner.lock().unwrap().pop_front()
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Vec<TransferTask> {
|
||||
self.inner.lock().unwrap().iter().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn update_state(&self, id: &str, state: TaskState) {
|
||||
let mut q = self.inner.lock().unwrap();
|
||||
if let Some(t) = q.iter_mut().find(|t| t.id == id) {
|
||||
t.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_progress(&self, id: &str, transferred: u64, speed: u64) {
|
||||
let mut q = self.inner.lock().unwrap();
|
||||
if let Some(t) = q.iter_mut().find(|t| t.id == id) {
|
||||
t.transferred_bytes = transferred;
|
||||
t.speed = Some(speed);
|
||||
if speed > 0 {
|
||||
let remaining = t.total_bytes.saturating_sub(transferred);
|
||||
t.eta_secs = Some(remaining / speed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&self, id: &str) {
|
||||
let mut q = self.inner.lock().unwrap();
|
||||
q.retain(|t| t.id != id);
|
||||
}
|
||||
}
|
||||
19
src-tauri/src/transfer/task.rs
Normal file
19
src-tauri/src/transfer/task.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use crate::domain::transfer::{TransferTask, TransferKind};
|
||||
|
||||
pub fn new_task(
|
||||
kind: TransferKind,
|
||||
connection_id: impl Into<String>,
|
||||
local_path: impl Into<String>,
|
||||
remote_path: impl Into<String>,
|
||||
file_name: impl Into<String>,
|
||||
total_bytes: u64,
|
||||
) -> TransferTask {
|
||||
TransferTask::new(
|
||||
kind,
|
||||
connection_id.into(),
|
||||
local_path.into(),
|
||||
remote_path.into(),
|
||||
file_name.into(),
|
||||
total_bytes,
|
||||
)
|
||||
}
|
||||
99
src-tauri/src/transfer/worker.rs
Normal file
99
src-tauri/src/transfer/worker.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
use crate::domain::transfer::{TaskState, TransferKind};
|
||||
use crate::fs::remote::RemoteRegistry;
|
||||
use crate::transfer::manager::TransferManager;
|
||||
|
||||
/// Событие для фронтенда
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TransferProgress {
|
||||
task_id: String,
|
||||
state: String,
|
||||
transferred_bytes: u64,
|
||||
total_bytes: u64,
|
||||
speed: Option<u64>,
|
||||
eta_secs: Option<u64>,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
pub fn spawn_worker(
|
||||
manager: Arc<TransferManager>,
|
||||
registry: Arc<RemoteRegistry>,
|
||||
app: AppHandle,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let task = {
|
||||
let task = manager.queue.pop();
|
||||
if let Some(ref t) = task {
|
||||
if t.state != TaskState::Queued {
|
||||
// paused/cancelled — не трогаем, вернём
|
||||
manager.queue.push(t.clone());
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
task
|
||||
};
|
||||
|
||||
let mut task = match task {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Старт
|
||||
task.state = TaskState::Running;
|
||||
manager.queue.update_state(&task.id, TaskState::Running);
|
||||
emit_progress(&app, &task, None);
|
||||
|
||||
let fs = match registry.get(&task.connection_id) {
|
||||
Some(fs) => fs,
|
||||
None => {
|
||||
task.state = TaskState::Failed("connection not found".into());
|
||||
manager.queue.update_state(&task.id, task.state.clone());
|
||||
emit_progress(&app, &task, Some("connection not found"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let result = match task.kind {
|
||||
TransferKind::Upload => fs.upload(&task.local_path, &task.remote_path).await,
|
||||
TransferKind::Download => fs.download(&task.remote_path, &task.local_path).await,
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
task.state = TaskState::Completed;
|
||||
task.transferred_bytes = task.total_bytes;
|
||||
manager.queue.update_state(&task.id, TaskState::Completed);
|
||||
manager.queue.update_progress(&task.id, task.total_bytes, 0);
|
||||
emit_progress(&app, &task, None);
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
task.state = TaskState::Failed(msg.clone());
|
||||
manager.queue.update_state(&task.id, TaskState::Failed(msg.clone()));
|
||||
emit_progress(&app, &task, Some(&msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn emit_progress(app: &AppHandle, task: &crate::domain::transfer::TransferTask, error: Option<&str>) {
|
||||
let _ = app.emit("transfer-progress", TransferProgress {
|
||||
task_id: task.id.clone(),
|
||||
state: task.state.to_string(),
|
||||
transferred_bytes: task.transferred_bytes,
|
||||
total_bytes: task.total_bytes,
|
||||
speed: task.speed,
|
||||
eta_secs: task.eta_secs,
|
||||
error: error.map(|s| s.to_string()),
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue