mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-18 21:36:56 -04:00
Add unmount functionality using fingerprint, enhance error handling in volume management, and improve UI integration for ejecting volumes
This commit is contained in:
@@ -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)
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user