Add unmount functionality using fingerprint, enhance error handling in volume management, and improve UI integration for ejecting volumes

This commit is contained in:
Jamie Pine
2024-11-03 05:40:16 -08:00
parent fbeb6c9373
commit e9ce227128
7 changed files with 191 additions and 17 deletions

View File

@@ -59,9 +59,9 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
.procedure(
"unmount",
R.with2(library())
.mutation(|(node, _), volume_id: Vec<u8>| async move {
.mutation(|(node, _), fingerprint: Vec<u8>| async move {
node.volumes
.unmount_volume(volume_id)
.unmount_volume(fingerprint)
.await
.map_err(Into::into)
}),

View File

@@ -376,7 +376,10 @@ impl VolumeManagerActor {
VolumeManagerMessage::UnmountVolume {
volume_fingerprint,
ack,
} => todo!(),
} => {
let result = self.handle_unmount_volume(volume_fingerprint).await;
let _ = ack.send(result);
}
VolumeManagerMessage::SpeedTest {
volume_fingerprint,
ack,
@@ -495,6 +498,45 @@ impl VolumeManagerActor {
Ok(())
}
async fn handle_unmount_volume(
&mut self,
volume_fingerprint: Vec<u8>,
) -> Result<(), VolumeError> {
// First get the volume from state to get its mount point
let volume = self
.state
.write()
.await
.volumes
.get(&volume_fingerprint)
.ok_or(VolumeError::NotFound(volume_fingerprint.clone()))?
.clone();
// Check if volume is actually mounted
if !volume.is_mounted {
return Err(VolumeError::NotMounted(volume.mount_point.clone()));
}
// Call the platform-specific unmount function
super::os::unmount_volume(&volume.mount_point).await?;
// If unmount succeeded, update our state
let mut state = self.state.write().await;
if let Some(vol) = state.volumes.get_mut(&volume_fingerprint) {
vol.is_mounted = false;
}
// Emit unmount event
if let Some(pub_id) = volume.pub_id {
let _ = self.event_tx.send(VolumeEvent::VolumeMountChanged {
id: pub_id,
is_mounted: false,
});
}
Ok(())
}
async fn handle_library_deletion(&mut self, library: Arc<Library>) -> Result<(), VolumeError> {
// Clean up volumes associated with deleted library
let _state = self.state.write().await;

View File

@@ -29,6 +29,10 @@ pub enum VolumeError {
#[error("No mount point found for volume")]
NoMountPoint,
/// Volume is already mounted
#[error("Volume with fingerprint {} is not found", hex::encode(.0))]
NotFound(Vec<u8>),
/// Volume isn't in database yet
#[error("Volume not yet tracked in database")]
NotInDatabase,

View File

@@ -10,6 +10,14 @@ pub use self::macos::get_volumes;
#[cfg(target_os = "windows")]
pub use self::windows::get_volumes;
// Re-export platform-specific unmount_volume function
#[cfg(target_os = "linux")]
pub use self::linux::unmount_volume;
#[cfg(target_os = "macos")]
pub use self::macos::unmount_volume;
#[cfg(target_os = "windows")]
pub use self::windows::unmount_volume;
/// Common utilities for volume detection across platforms
mod common {
pub fn parse_size(size_str: &str) -> u64 {
@@ -135,6 +143,43 @@ pub mod macos {
.map(|line| line.contains("read-only"))
.unwrap_or(false))
}
pub async fn unmount_volume(path: &std::path::Path) -> Result<(), VolumeError> {
use std::process::Command;
use tokio::process::Command as TokioCommand;
// First try diskutil
let result = TokioCommand::new("diskutil")
.arg("unmount")
.arg(path)
.output()
.await;
match result {
Ok(output) => {
if output.status.success() {
return Ok(());
}
// If diskutil fails, try umount as fallback
let fallback = Command::new("umount")
.arg(path)
.output()
.map_err(|e| VolumeError::Platform(format!("Unmount failed: {}", e)))?;
if fallback.status.success() {
Ok(())
} else {
Err(VolumeError::Platform(format!(
"Failed to unmount volume: {}",
String::from_utf8_lossy(&fallback.stderr)
)))
}
}
Err(e) => Err(VolumeError::Platform(format!(
"Failed to execute unmount command: {}",
e
))),
}
}
}
#[cfg(target_os = "linux")]
@@ -209,6 +254,52 @@ pub mod linux {
disk.available_space(),
))
}
pub async fn unmount_volume(path: &std::path::Path) -> Result<(), VolumeError> {
use tokio::process::Command;
// Try umount first
let result = Command::new("umount")
.arg(path)
.output()
.await
.map_err(|e| VolumeError::Platform(format!("Unmount failed: {}", e)))?;
if result.status.success() {
Ok(())
} else {
// If regular unmount fails, try with force option
let force_result = Command::new("umount")
.arg("-f") // Force unmount
.arg(path)
.output()
.await
.map_err(|e| VolumeError::Platform(format!("Force unmount failed: {}", e)))?;
if force_result.status.success() {
Ok(())
} else {
// If both attempts fail, try udisksctl as a last resort
let udisks_result = Command::new("udisksctl")
.arg("unmount")
.arg("-b")
.arg(path)
.output()
.await
.map_err(|e| {
VolumeError::Platform(format!("udisksctl unmount failed: {}", e))
})?;
if udisks_result.status.success() {
Ok(())
} else {
Err(VolumeError::Platform(format!(
"All unmount attempts failed: {}",
String::from_utf8_lossy(&udisks_result.stderr)
)))
}
}
}
}
}
#[cfg(target_os = "windows")]
pub mod windows {
@@ -337,4 +428,44 @@ pub mod windows {
// using IOCTL_STORAGE_QUERY_PROPERTY
DiskType::Unknown
}
pub async fn unmount_volume(path: &std::path::Path) -> Result<(), VolumeError> {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use windows::core::PWSTR;
use windows::Win32::Storage::FileSystem::{
DeleteVolumeMountPointW, GetVolumeNameForVolumeMountPointW,
};
// Convert path to wide string for Windows API
let wide_path: Vec<u16> = OsStr::new(path)
.encode_wide()
.chain(std::iter::once(0))
.collect();
unsafe {
// Buffer for volume name
let mut volume_name = [0u16; 50];
let mut volume_name_ptr = PWSTR(volume_name.as_mut_ptr());
// Get the volume name for the mount point
let result = GetVolumeNameForVolumeMountPointW(wide_path.as_ptr(), volume_name_ptr);
if !result.as_bool() {
return Err(VolumeError::Platform(
"Failed to get volume name".to_string(),
));
}
// Delete the mount point
let result = DeleteVolumeMountPointW(wide_path.as_ptr());
if result.as_bool() {
Ok(())
} else {
Err(VolumeError::Platform(
"Failed to unmount volume".to_string(),
))
}
}
}
}

View File

@@ -16,7 +16,7 @@ pub struct VolumeManagerState {
/// All tracked volumes by fingerprint
pub volumes: HashMap<Vec<u8>, Volume>,
/// Mapping of library volumes to system volumes
/// LibraryPubId -> VolumePubId -> Fingerprint
/// LibraryPubId -> Fingerprint -> VolumePubId
pub library_volume_mapping: HashMap<Vec<u8>, HashMap<Vec<u8>, Vec<u8>>>,
/// Volume manager options
pub options: VolumeOptions,

View File

@@ -12,13 +12,6 @@ use tracing::{debug, error, warn};
const DEBOUNCE_MS: u64 = 100;
#[derive(Debug)]
pub struct WatcherState {
pub watcher: Arc<VolumeWatcher>,
pub last_event: Instant,
pub paused: bool,
}
#[derive(Debug)]
pub struct VolumeWatcher {
event_tx: broadcast::Sender<VolumeEvent>,

View File

@@ -24,7 +24,13 @@ import { SeeMore } from '../../SidebarLayout/SeeMore';
const Name = tw.span`truncate`;
// Improved eject button that actually unmounts the volume
const EjectButton = ({ volumeId, className }: { volumeId: Uint8Array; className?: string }) => {
const EjectButton = ({
fingerprint,
className
}: {
fingerprint: Uint8Array;
className?: string;
}) => {
const unmountMutation = useLibraryMutation('volumes.unmount');
return (
@@ -34,9 +40,7 @@ const EjectButton = ({ volumeId, className }: { volumeId: Uint8Array; className?
onClick={async (e: MouseEvent) => {
e.preventDefault(); // Prevent navigation
try {
await unmountMutation.mutateAsync({
fingerprint: Array.from(volumeId) // Convert Uint8Array to number[]
});
await unmountMutation.mutateAsync(Array.from(fingerprint));
toast.success('Volume ejected successfully');
} catch (error) {
toast.error('Failed to eject volume');
@@ -164,8 +168,8 @@ export default function LocalSection() {
>
<SidebarIcon name={getVolumeIcon(volume)} />
<Name>{displayName}</Name>
{volume.mount_type === 'External' && volume.pub_id && (
<EjectButton volumeId={new Uint8Array(volume.pub_id)} />
{volume.mount_type === 'External' && volume.fingerprint && (
<EjectButton fingerprint={new Uint8Array(volume.fingerprint)} />
)}
</EphemeralLocation>
);