Дезайн ыыыы
This commit is contained in:
parent
403d7086d4
commit
e7c02c7feb
174 changed files with 15802 additions and 9725 deletions
385
src/commands.rs
Normal file
385
src/commands.rs
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
//! Tauri command surface — thin wrappers over domain/fs/protocols/storage/transfer.
|
||||
//! See docs/TAURI_BACKEND_API.md for the design this implements.
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use tauri::State;
|
||||
|
||||
use crate::domain::connection::{ConnectionParams, Protocol};
|
||||
use crate::domain::file_entry::FileEntry;
|
||||
use crate::domain::site::Site;
|
||||
use crate::domain::transfer::{TaskState, TransferKind, TransferTask};
|
||||
use crate::fs::remote::RemoteRegistry;
|
||||
use crate::protocols::{RemoteFs, ftp::FtpClient, sftp::SftpClient};
|
||||
use crate::storage::db::{self, HistoryRow};
|
||||
use crate::transfer::queue::TransferQueue;
|
||||
|
||||
pub struct AppState {
|
||||
pub db: Arc<std::sync::Mutex<rusqlite::Connection>>,
|
||||
pub registry: Arc<RemoteRegistry>,
|
||||
pub queue: TransferQueue,
|
||||
pub max_concurrent: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
// --- Connections ---------------------------------------------------------
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn connect(state: State<'_, AppState>, params: ConnectionParams) -> Result<String, String> {
|
||||
let registry = state.registry.clone();
|
||||
let db = state.db.clone();
|
||||
|
||||
// Пароль приходит с фронтенда и хранится в БД (в sites.password).
|
||||
// Для истории используем канонический conn_id, чтобы повторные
|
||||
// подключения не плодили записи.
|
||||
let canonical_id = db
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|conn| {
|
||||
db::find_history_conn_id(&conn, ¶ms.host, params.port, ¶ms.username)
|
||||
.ok()
|
||||
.flatten()
|
||||
})
|
||||
.unwrap_or_else(|| params.id.clone());
|
||||
|
||||
let fs: Arc<dyn RemoteFs> = match params.protocol {
|
||||
Protocol::Sftp => {
|
||||
if let Some(key_path) = ¶ms.key_path {
|
||||
Arc::new(
|
||||
SftpClient::connect_key(
|
||||
¶ms.host,
|
||||
params.port,
|
||||
¶ms.username,
|
||||
key_path,
|
||||
params.password.as_deref(),
|
||||
)
|
||||
.map_err(|e| e.to_string())?,
|
||||
)
|
||||
} else if let Some(password) = ¶ms.password {
|
||||
Arc::new(
|
||||
SftpClient::connect_password(¶ms.host, params.port, ¶ms.username, password)
|
||||
.map_err(|e| e.to_string())?,
|
||||
)
|
||||
} else {
|
||||
Arc::new(
|
||||
SftpClient::connect_auto(¶ms.host, params.port, ¶ms.username)
|
||||
.map_err(|e| e.to_string())?,
|
||||
)
|
||||
}
|
||||
}
|
||||
Protocol::Ftp => {
|
||||
let password = params
|
||||
.password
|
||||
.clone()
|
||||
.ok_or("Password is required for FTP connections.")?;
|
||||
Arc::new(
|
||||
FtpClient::connect(¶ms.host, params.port, ¶ms.username, &password)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?,
|
||||
)
|
||||
}
|
||||
Protocol::Ftps => return Err("FTPS not yet implemented".into()),
|
||||
};
|
||||
|
||||
registry.insert(params.id.clone(), fs);
|
||||
|
||||
if let Ok(conn) = db.lock() {
|
||||
let _ = db::add_history_entry(
|
||||
&conn,
|
||||
¶ms.host,
|
||||
params.port,
|
||||
¶ms.username,
|
||||
&canonical_id,
|
||||
¶ms.protocol,
|
||||
params.key_path.as_deref(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(params.id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn disconnect(state: State<'_, AppState>, connection_id: String) {
|
||||
state.registry.remove(&connection_id);
|
||||
}
|
||||
|
||||
// --- Remote file ops -------------------------------------------------------
|
||||
|
||||
fn get_remote(state: &AppState, connection_id: &str) -> Result<Arc<dyn RemoteFs>, String> {
|
||||
state
|
||||
.registry
|
||||
.get(connection_id)
|
||||
.ok_or_else(|| "connection not found".to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remote_list(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
path: String,
|
||||
) -> Result<Vec<FileEntry>, String> {
|
||||
get_remote(&state, &connection_id)?
|
||||
.list(&path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remote_stat(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
path: String,
|
||||
) -> Result<FileEntry, String> {
|
||||
get_remote(&state, &connection_id)?
|
||||
.stat(&path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remote_mkdir(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
path: String,
|
||||
) -> Result<(), String> {
|
||||
get_remote(&state, &connection_id)?
|
||||
.mkdir(&path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remote_rename(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
from: String,
|
||||
to: String,
|
||||
) -> Result<(), String> {
|
||||
get_remote(&state, &connection_id)?
|
||||
.rename(&from, &to)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remote_delete(
|
||||
state: State<'_, AppState>,
|
||||
connection_id: String,
|
||||
path: String,
|
||||
) -> Result<(), String> {
|
||||
get_remote(&state, &connection_id)?
|
||||
.delete(&path)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// --- Local file ops --------------------------------------------------------
|
||||
|
||||
#[tauri::command]
|
||||
pub fn local_list(path: String) -> Result<Vec<FileEntry>, String> {
|
||||
crate::fs::local::list(&path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn local_home_dir() -> String {
|
||||
crate::fs::local::home_dir()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn local_mkdir(path: String) -> Result<(), String> {
|
||||
crate::fs::local::mkdir(&path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn local_rename(from: String, to: String) -> Result<(), String> {
|
||||
crate::fs::local::rename(&from, &to).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn local_delete(path: String) -> Result<(), String> {
|
||||
crate::fs::local::delete(&path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn local_open(path: String) -> Result<(), String> {
|
||||
crate::fs::local::open(&path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn local_move_into(src_path: String, dest_dir: String) -> Result<(), String> {
|
||||
crate::fs::local::move_into(&src_path, &dest_dir).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
// --- Transfers --------------------------------------------------------------
|
||||
|
||||
#[tauri::command]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn enqueue_transfer(
|
||||
state: State<'_, AppState>,
|
||||
kind: TransferKind,
|
||||
connection_id: String,
|
||||
local_path: String,
|
||||
remote_path: String,
|
||||
file_name: String,
|
||||
total_bytes: u64,
|
||||
) -> String {
|
||||
let task = TransferTask::new(kind, connection_id, local_path, remote_path, file_name, total_bytes);
|
||||
let id = task.id.clone();
|
||||
state.queue.push(task);
|
||||
id
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_tasks(state: State<'_, AppState>) -> Vec<TransferTask> {
|
||||
state.queue.all()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn pause_task(state: State<'_, AppState>, id: String) {
|
||||
state.queue.update_state(&id, TaskState::Paused);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn resume_task(state: State<'_, AppState>, id: String) {
|
||||
state.queue.update_state(&id, TaskState::Queued);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn cancel_task(state: State<'_, AppState>, id: String) {
|
||||
state.queue.update_state(&id, TaskState::Cancelled);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_task(state: State<'_, AppState>, id: String) {
|
||||
state.queue.remove(&id);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_max_concurrent(state: State<'_, AppState>, n: u32) {
|
||||
state.max_concurrent.store(n.max(1), Ordering::Relaxed);
|
||||
if let Ok(conn) = state.db.lock() {
|
||||
db::set_u32(&conn, "max_concurrent_transfers", n.max(1));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sites / bookmarks / history / settings ---------------------------------
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_sites(state: State<'_, AppState>) -> Result<Vec<Site>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::get_sites(&conn).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_site(state: State<'_, AppState>, site: Site) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::save_site(&conn, &site).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_site(state: State<'_, AppState>, id: String) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::delete_site(&conn, &id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_bookmarks(state: State<'_, AppState>) -> Result<Vec<(i64, String, String)>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::get_bookmarks(&conn).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_bookmark(state: State<'_, AppState>, name: String, path: String) -> Result<i64, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::add_bookmark(&conn, &name, &path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_bookmark(state: State<'_, AppState>, id: i64) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::remove_bookmark(&conn, id).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn list_history(state: State<'_, AppState>) -> Result<Vec<HistoryRow>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::get_history(&conn).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn clear_history(state: State<'_, AppState>) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::clear_history(&conn).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn find_history_conn_id(
|
||||
state: State<'_, AppState>,
|
||||
host: String,
|
||||
port: u16,
|
||||
username: String,
|
||||
) -> Result<Option<String>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::find_history_conn_id(&conn, &host, port, &username).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_pref(state: State<'_, AppState>, key: String) -> Result<Option<String>, String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
Ok(db::get_setting(&conn, &key))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn set_pref(state: State<'_, AppState>, key: String, value: String) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
db::set_setting(&conn, &key, &value).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Удалить пароль из сохранённого сайта (очистить поле password в БД).
|
||||
#[tauri::command]
|
||||
pub fn delete_password(state: State<'_, AppState>, site_id: String) -> Result<(), String> {
|
||||
let conn = state.db.lock().map_err(|e| e.to_string())?;
|
||||
conn.execute(
|
||||
"UPDATE sites SET password = NULL WHERE id = ?1",
|
||||
rusqlite::params![site_id],
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The folder holding the sqlite db (Settings → About "reveal in folder").
|
||||
#[tauri::command]
|
||||
pub fn app_data_dir() -> String {
|
||||
dirs::data_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join("wherry")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlatformInfo {
|
||||
pub os: &'static str,
|
||||
/// Linux only: XDG_CURRENT_DESKTOP, lowercased ("kde", "gnome", "hyprland", …).
|
||||
pub desktop: Option<String>,
|
||||
/// Linux only: "wayland" | "x11".
|
||||
pub session: Option<String>,
|
||||
}
|
||||
|
||||
/// Из webview на Linux не различить KDE/GNOME/Hyprland — окружение видно
|
||||
/// только процессу; фронтенд вешает результат как data-атрибуты на <html>.
|
||||
#[tauri::command]
|
||||
pub fn platform_info() -> PlatformInfo {
|
||||
let os = std::env::consts::OS;
|
||||
let (desktop, session) = if os == "linux" {
|
||||
(
|
||||
std::env::var("XDG_CURRENT_DESKTOP")
|
||||
.ok()
|
||||
.map(|d| d.to_lowercase()),
|
||||
std::env::var("XDG_SESSION_TYPE").ok(),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
PlatformInfo { os, desktop, session }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue