diff --git a/desktop/src-tauri/src/commands.rs b/desktop/src-tauri/src/commands.rs new file mode 100644 index 0000000..d020dad --- /dev/null +++ b/desktop/src-tauri/src/commands.rs @@ -0,0 +1,374 @@ +use std::fs::File; +use std::io::{Read, Write}; +use std::path::PathBuf; +use std::sync::Arc; + +use libnspire::dir::EntryType; +use libnspire::PID_CX2; +use serde::Serialize; +use tauri::{Emitter, Runtime, WebviewWindow}; + +use crate::device::{ + add_device, get_open_dev, AddDevice, DevId, DeviceState, FileInfo, ProgressUpdate, DEVICES, +}; +use crate::error::SerializedError; + +/// If the device vanished mid-operation, drop it from the registry and tell +/// the frontend, then pass the original result through unchanged. +fn err_wrap( + res: Result, + dev: DevId, + window: &WebviewWindow, +) -> Result { + if let Err(libnspire::Error::NoDevice) = res { + DEVICES + .write() + .unwrap() + .remove(&(dev.bus_number, dev.address)); + if let Err(msg) = window.emit("removeDevice", dev) { + eprintln!("{}", msg); + }; + } + res +} + +/// Emits a `progress` event on every 6th callback, and always on completion, +/// to avoid flooding the IPC channel during large transfers. +fn progress_sender( + window: &WebviewWindow, + dev: DevId, + total: usize, +) -> impl FnMut(usize) + '_ { + let mut i = 0; + move |remaining| { + if i > 5 { + i = 0; + } + if i == 0 || remaining == 0 { + if let Err(msg) = window.emit( + "progress", + ProgressUpdate { + dev, + remaining, + total, + }, + ) { + eprintln!("{}", msg); + }; + } + i += 1; + } +} + +#[tauri::command] +pub fn enumerate(window: WebviewWindow) -> Result, SerializedError> { + let devices: Vec<_> = rusb::devices()?.iter().collect(); + let mut map = DEVICES.write().unwrap(); + + // Replaces hashbrown's `drain_filter`: collect the keys of devices that are + // no longer present on the bus, then remove them. std HashMap has no stable + // drain-with-predicate, and this is clearer than the original anyway. + let stale: Vec<(u8, u8)> = map + .keys() + .filter(|k| { + devices + .iter() + .all(|d| d.bus_number() != k.0 || d.address() != k.1) + }) + .copied() + .collect(); + + for key in stale { + map.remove(&key); + if let Err(msg) = window.emit( + "removeDevice", + DevId { + bus_number: key.0, + address: key.1, + }, + ) { + eprintln!("{}", msg); + } + } + + let filtered: Vec<_> = devices + .into_iter() + .filter(|d| !map.contains_key(&(d.bus_number(), d.address()))) + .collect(); + + Ok( + filtered + .into_iter() + .filter_map(|dev| add_device(Arc::new(dev)).ok()) + .map(|dev| { + let msg = AddDevice { + dev: DevId { + bus_number: (dev.0).0, + address: (dev.0).1, + }, + name: (dev.1).name.clone(), + is_cx_ii: (dev.1) + .device + .device_descriptor() + .map(|d| d.product_id() == PID_CX2) + .unwrap_or(false), + needs_drivers: (dev.1).needs_drivers, + }; + map.insert(dev.0, dev.1); + msg + }) + .collect(), + ) +} + +#[tauri::command] +pub fn open_device(bus_number: u8, address: u8) -> Result { + let device = if let Some(dev) = DEVICES.read().unwrap().get(&(bus_number, address)) { + if !matches!(dev.state, DeviceState::Closed) { + return Err("Already open".into()); + }; + dev.device.clone() + } else { + return Err("Failed to find device".into()); + }; + let handle = libnspire::Handle::new(device.open()?)?; + let info = handle.info()?; + { + let mut guard = DEVICES.write().unwrap(); + let device = guard + .get_mut(&(bus_number, address)) + .ok_or_else(|| anyhow::anyhow!("Device lost"))?; + device.state = DeviceState::Open(Arc::new(std::sync::Mutex::new(handle)), info.clone()); + } + Ok(info) +} + +#[tauri::command] +pub fn close_device(bus_number: u8, address: u8) -> Result { + let mut guard = DEVICES.write().unwrap(); + let device = guard + .get_mut(&(bus_number, address)) + .ok_or_else(|| anyhow::anyhow!("Device lost"))?; + device.state = DeviceState::Closed; + Ok(()) +} + +#[tauri::command] +pub fn update_device( + bus_number: u8, + address: u8, + window: WebviewWindow, +) -> Result { + let dev = DevId { + bus_number, + address, + }; + let handle = get_open_dev(&dev)?; + let handle = handle.lock().unwrap(); + let info = err_wrap(handle.info(), dev, &window)?; + Ok(info) +} + +#[tauri::command] +pub fn list_dir( + bus_number: u8, + address: u8, + path: String, + window: WebviewWindow, +) -> Result { + let dev = DevId { + bus_number, + address, + }; + let handle = get_open_dev(&dev)?; + let handle = handle.lock().unwrap(); + let dir = err_wrap(handle.list_dir(&path), dev, &window)?; + + Ok( + dir + .iter() + .map(|file| FileInfo { + path: file.name().to_string_lossy().to_string(), + is_dir: file.entry_type() == EntryType::Directory, + date: file.date(), + size: file.size(), + }) + .collect::>(), + ) +} + +#[tauri::command] +pub fn download_file( + bus_number: u8, + address: u8, + path: (String, u64), + dest: String, + window: WebviewWindow, +) -> Result { + let dev = DevId { + bus_number, + address, + }; + let (file, size) = path; + let dest = PathBuf::from(dest); + let handle = get_open_dev(&dev)?; + let handle = handle.lock().unwrap(); + let mut buf = vec![0; size as usize]; + err_wrap( + handle.read_file( + &file, + &mut buf, + &mut progress_sender(&window, dev, size as usize), + ), + dev, + &window, + )?; + if let Some(name) = file.split('/').last() { + File::create(dest.join(name))?.write_all(&buf)?; + } + Ok(()) +} + +#[tauri::command] +pub fn upload_file( + bus_number: u8, + address: u8, + path: String, + src: String, + window: WebviewWindow, +) -> Result { + let dev = DevId { + bus_number, + address, + }; + let file = PathBuf::from(src); + let handle = get_open_dev(&dev)?; + let handle = handle.lock().unwrap(); + let mut buf = vec![]; + File::open(&file)?.read_to_end(&mut buf)?; + let name = file + .file_name() + .ok_or_else(|| anyhow::anyhow!("Failed to get file name"))? + .to_string_lossy() + .to_string(); + err_wrap( + handle.write_file( + &format!("{}/{}", path, name), + &buf, + &mut progress_sender(&window, dev, buf.len()), + ), + dev, + &window, + )?; + Ok(()) +} + +#[tauri::command] +pub fn upload_os( + bus_number: u8, + address: u8, + src: String, + window: WebviewWindow, +) -> Result { + let dev = DevId { + bus_number, + address, + }; + let handle = get_open_dev(&dev)?; + let handle = handle.lock().unwrap(); + let mut buf = vec![]; + File::open(&src)?.read_to_end(&mut buf)?; + err_wrap( + handle.send_os(&buf, &mut progress_sender(&window, dev, buf.len())), + dev, + &window, + )?; + Ok(()) +} + +#[tauri::command] +pub fn delete_file( + bus_number: u8, + address: u8, + path: String, + window: WebviewWindow, +) -> Result { + let dev = DevId { + bus_number, + address, + }; + let handle = get_open_dev(&dev)?; + let handle = handle.lock().unwrap(); + err_wrap(handle.delete_file(&path), dev, &window)?; + Ok(()) +} + +#[tauri::command] +pub fn delete_dir( + bus_number: u8, + address: u8, + path: String, + window: WebviewWindow, +) -> Result { + let dev = DevId { + bus_number, + address, + }; + let handle = get_open_dev(&dev)?; + let handle = handle.lock().unwrap(); + err_wrap(handle.delete_dir(&path), dev, &window)?; + Ok(()) +} + +#[tauri::command] +pub fn create_nspire_dir( + bus_number: u8, + address: u8, + path: String, + window: WebviewWindow, +) -> Result { + let dev = DevId { + bus_number, + address, + }; + let handle = get_open_dev(&dev)?; + let handle = handle.lock().unwrap(); + err_wrap(handle.create_dir(&path), dev, &window)?; + Ok(()) +} + +#[tauri::command] +pub fn move_file( + bus_number: u8, + address: u8, + src: String, + dest: String, + window: WebviewWindow, +) -> Result { + let dev = DevId { + bus_number, + address, + }; + let handle = get_open_dev(&dev)?; + let handle = handle.lock().unwrap(); + err_wrap(handle.move_file(&src, &dest), dev, &window)?; + Ok(()) +} + +#[tauri::command] +pub fn copy( + bus_number: u8, + address: u8, + src: String, + dest: String, + window: WebviewWindow, +) -> Result { + let dev = DevId { + bus_number, + address, + }; + let handle = get_open_dev(&dev)?; + let handle = handle.lock().unwrap(); + err_wrap(handle.copy_file(&src, &dest), dev, &window)?; + Ok(()) +} diff --git a/desktop/src-tauri/src/device.rs b/desktop/src-tauri/src/device.rs index d3008c7..8e51560 100644 --- a/desktop/src-tauri/src/device.rs +++ b/desktop/src-tauri/src/device.rs @@ -43,7 +43,7 @@ pub struct AddDevice { pub needs_drivers: bool, } -#[derive(Debug, Serialize)] +#[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ProgressUpdate { #[serde(flatten)] diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index 7319f97..38c0477 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -1,5 +1,6 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +mod commands; mod device; mod error;