Skip to content

Commit

Permalink
Add unmount functionality using fingerprint, enhance error handling i…
Browse files Browse the repository at this point in the history
…n volume management, and improve UI integration for ejecting volumes
  • Loading branch information
jamiepine committed Nov 3, 2024
1 parent fbeb6c9 commit e9ce227
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 17 deletions.
4 changes: 2 additions & 2 deletions core/src/api/volumes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}),
Expand Down
44 changes: 43 additions & 1 deletion core/src/volume/actor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions core/src/volume/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
131 changes: 131 additions & 0 deletions core/src/volume/os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(),
))
}
}
}
}
2 changes: 1 addition & 1 deletion core/src/volume/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 0 additions & 7 deletions core/src/volume/watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down
16 changes: 10 additions & 6 deletions interface/app/$libraryId/Layout/Sidebar/sections/Local/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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');
Expand Down Expand Up @@ -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>
);
Expand Down

0 comments on commit e9ce227

Please sign in to comment.