fix(msd): 区分虚拟磁盘挂载能力与文件访问状态

使用原始文件元数据挂载虚拟磁盘,允许主机使用网页文件管理不支持的文件系统。连接期间只读取元数据;初始化和删除持有外层写锁,并同步控制器缓存。

API 的 used/free 改为可空值,新增 file_access 表示 available、unsupported、blocked_while_connected 或 unknown,需要前端配套处理。补充状态序列化、元数据校验和文件系统兼容测试。
This commit is contained in:
mofeng-git
2026-09-05 14:10:53 +08:00
parent d67a3ab7f8
commit b573f478fc
6 changed files with 300 additions and 90 deletions

View File

@@ -102,7 +102,9 @@ impl MsdErrorCode {
Self::MsdDownloadIncomplete => "The remote image download was incomplete.", Self::MsdDownloadIncomplete => "The remote image download was incomplete.",
Self::MsdDriveNotInitialized => "The virtual drive is not initialized.", Self::MsdDriveNotInitialized => "The virtual drive is not initialized.",
Self::MsdDriveConnected => "The virtual drive is connected to the controlled computer.", Self::MsdDriveConnected => "The virtual drive is connected to the controlled computer.",
Self::MsdDriveFilesystemUnsupported => "The virtual drive filesystem is unsupported.", Self::MsdDriveFilesystemUnsupported => {
"Web file management does not support this virtual drive format."
}
Self::MsdDriveSizeInvalid => "The virtual drive size is invalid.", Self::MsdDriveSizeInvalid => "The virtual drive size is invalid.",
Self::MsdStorageSpaceUnavailable => { Self::MsdStorageSpaceUnavailable => {
"Available virtual media storage space could not be determined." "Available virtual media storage space could not be determined."
@@ -183,7 +185,7 @@ impl MsdErrorCode {
"Verify the remote server and network connection, then retry." "Verify the remote server and network connection, then retry."
} }
Self::MsdDriveFilesystemUnsupported => { Self::MsdDriveFilesystemUnsupported => {
"Reinitialize the virtual drive with a supported filesystem, then retry." "Mount the drive on the controlled computer, or use a supported format for web file management."
} }
Self::MsdStorageSpaceUnavailable => { Self::MsdStorageSpaceUnavailable => {
"Verify that virtual media storage is available, then retry." "Verify that virtual media storage is available, then retry."
@@ -386,7 +388,7 @@ mod tests {
( (
MsdDriveFilesystemUnsupported, MsdDriveFilesystemUnsupported,
"MSD_DRIVE_FILESYSTEM_UNSUPPORTED", "MSD_DRIVE_FILESYSTEM_UNSUPPORTED",
"The virtual drive filesystem is unsupported.", "Web file management does not support this virtual drive format.",
), ),
( (
MsdDriveSizeInvalid, MsdDriveSizeInvalid,

View File

@@ -8,9 +8,10 @@ use tracing::{debug, info, warn};
use super::image::ImageManager; use super::image::ImageManager;
use super::monitor::MsdHealthMonitor; use super::monitor::MsdHealthMonitor;
use super::types::{ use super::types::{
DiskMode, DownloadProgress, DownloadStatus, DriveInfo, ImageInfo, MountedMedia, DiskMode, DownloadProgress, DownloadStatus, DriveFileAccess, DriveInfo, ImageInfo,
MountedMediaKind, MsdState, MountedMedia, MountedMediaKind, MsdState,
}; };
use super::ventoy_drive::VentoyDrive;
use crate::error::{AppError, MsdErrorCode, Result}; use crate::error::{AppError, MsdErrorCode, Result};
use crate::otg::{MsdFunction, MsdLunConfig, OtgService}; use crate::otg::{MsdFunction, MsdLunConfig, OtgService};
@@ -83,14 +84,9 @@ impl MsdController {
state.available = true; state.available = true;
if self.drive_path.exists() { if self.drive_path.exists() {
if let Ok(metadata) = std::fs::metadata(&self.drive_path) { if let Ok(drive_info) =
let drive_info = DriveInfo { VentoyDrive::new(self.drive_path.clone()).raw_info(DriveFileAccess::Unknown)
size: metadata.len(), {
used: 0,
free: metadata.len(),
initialized: true,
path: self.drive_path.clone(),
};
state.drive_info = Some(drive_info.clone()); state.drive_info = Some(drive_info.clone());
debug!( debug!(
"Found existing virtual drive: {}", "Found existing virtual drive: {}",
@@ -199,28 +195,6 @@ impl MsdController {
self.assert_available(&state).await?; self.assert_available(&state).await?;
if !self.drive_path.exists() {
self.monitor
.report_error("Virtual drive not initialized", "drive_not_found")
.await;
return Err(MsdErrorCode::MsdDriveNotInitialized.into());
}
let drive_info = state.drive_info.clone().or_else(|| {
std::fs::metadata(&self.drive_path)
.ok()
.map(|metadata| DriveInfo {
size: metadata.len(),
used: 0,
free: metadata.len(),
initialized: true,
path: self.drive_path.clone(),
})
});
if state.drive_info.is_none() {
state.drive_info = drive_info.clone();
}
if state if state
.mounted_media .mounted_media
.iter() .iter()
@@ -229,8 +203,22 @@ impl MsdController {
return Err(MsdErrorCode::MsdMediaAlreadyMounted.into()); return Err(MsdErrorCode::MsdMediaAlreadyMounted.into());
} }
let drive_info = let drive_info = match self.drive_mount_info() {
drive_info.ok_or_else(|| AppError::from(MsdErrorCode::MsdDriveNotInitialized))?; Ok(info) => info,
Err(error) => {
if matches!(
&error,
AppError::Msd(msd) if msd.code() == MsdErrorCode::MsdDriveNotInitialized
) {
self.monitor
.report_error("Virtual drive not initialized", "drive_not_found")
.await;
}
return Err(error);
}
};
state.drive_info = Some(drive_info.clone());
let lun = Self::lowest_free_lun(&state) let lun = Self::lowest_free_lun(&state)
.ok_or_else(|| AppError::from(MsdErrorCode::MsdMediaSlotsFull))?; .ok_or_else(|| AppError::from(MsdErrorCode::MsdMediaSlotsFull))?;
@@ -240,6 +228,8 @@ impl MsdController {
return Err(e); return Err(e);
} }
state.mounted_media.push(media); state.mounted_media.push(media);
state.drive_info =
Some(drive_info.with_file_access(DriveFileAccess::BlockedWhileConnected));
info!( info!(
"Mounted virtual drive on LUN {}: {}", "Mounted virtual drive on LUN {}: {}",
@@ -254,6 +244,15 @@ impl MsdController {
Ok(()) Ok(())
} }
fn drive_mount_info(&self) -> Result<DriveInfo> {
VentoyDrive::new(self.drive_path.clone()).raw_info(DriveFileAccess::Unknown)
}
pub async fn set_drive_info(&self, drive_info: Option<DriveInfo>) {
self.state.write().await.drive_info = drive_info;
self.mark_device_info_dirty().await;
}
async fn assert_available(&self, state: &MsdState) -> Result<()> { async fn assert_available(&self, state: &MsdState) -> Result<()> {
if !state.available { if !state.available {
self.monitor self.monitor
@@ -293,6 +292,16 @@ impl MsdController {
} }
fn reset_mounts_for_mode(state: &mut MsdState, disk_mode: DiskMode) { fn reset_mounts_for_mode(state: &mut MsdState, disk_mode: DiskMode) {
if state
.mounted_media
.iter()
.any(|media| media.kind == MountedMediaKind::Drive)
{
state.drive_info = state
.drive_info
.take()
.map(|info| info.with_file_access(DriveFileAccess::Unknown));
}
state.disk_mode = disk_mode; state.disk_mode = disk_mode;
state.mounted_media.clear(); state.mounted_media.clear();
} }
@@ -397,6 +406,12 @@ impl MsdController {
self.disconnect_lun(media.lun).await?; self.disconnect_lun(media.lun).await?;
state.mounted_media.remove(index); state.mounted_media.remove(index);
if media.kind == MountedMediaKind::Drive {
state.drive_info = state
.drive_info
.take()
.map(|info| info.with_file_access(DriveFileAccess::Unknown));
}
info!("Unmounted media"); info!("Unmounted media");
drop(state); drop(state);
@@ -490,6 +505,16 @@ impl MsdController {
disconnected.push(media.clone()); disconnected.push(media.clone());
} }
if state
.mounted_media
.iter()
.any(|media| media.kind == MountedMediaKind::Drive)
{
state.drive_info = state
.drive_info
.take()
.map(|info| info.with_file_access(DriveFileAccess::Unknown));
}
state.mounted_media.clear(); state.mounted_media.clear();
info!("Disconnected all mounted media"); info!("Disconnected all mounted media");
@@ -744,6 +769,29 @@ mod tests {
assert!(state.mounted_media.is_empty()); assert!(state.mounted_media.is_empty());
} }
#[tokio::test]
async fn drive_mount_metadata_ignores_cached_drive_info() {
let temp_dir = TempDir::new().unwrap();
let controller = MsdController::new(Arc::new(OtgService::new()), temp_dir.path());
std::fs::create_dir_all(&controller.ventoy_dir).unwrap();
std::fs::write(&controller.drive_path, vec![0u8; 128]).unwrap();
controller.state.write().await.drive_info = Some(DriveInfo::from_raw(
controller.drive_path.clone(),
64,
DriveFileAccess::Available,
));
std::fs::write(&controller.drive_path, vec![0u8; 256]).unwrap();
let info = controller.drive_mount_info().unwrap();
assert_eq!(info.size, 256);
assert_eq!(info.used, None);
assert_eq!(info.file_access, DriveFileAccess::Unknown);
let media = MountedMedia::drive(0, &info);
let config = MsdController::media_config(&media);
assert_eq!(config.file, controller.drive_path);
}
#[test] #[test]
fn single_disk_mode_only_exposes_lun_zero() { fn single_disk_mode_only_exposes_lun_zero() {
let mut state = MsdState::default(); let mut state = MsdState::default();
@@ -842,13 +890,7 @@ mod tests {
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let drive_path = temp_dir.path().join("ventoy.img"); let drive_path = temp_dir.path().join("ventoy.img");
std::fs::write(&drive_path, b"drive").unwrap(); std::fs::write(&drive_path, b"drive").unwrap();
let drive = DriveInfo { let drive = DriveInfo::from_raw(drive_path, 5, DriveFileAccess::Unknown);
size: 5,
used: 0,
free: 5,
initialized: true,
path: drive_path,
};
let mut state = MsdState::default(); let mut state = MsdState::default();
MsdController::reset_mounts_for_mode(&mut state, DiskMode::Multi); MsdController::reset_mounts_for_mode(&mut state, DiskMode::Multi);
state.mounted_media.push(MountedMedia::drive(0, &drive)); state.mounted_media.push(MountedMedia::drive(0, &drive));
@@ -900,13 +942,11 @@ mod tests {
let image_path = temp_dir.path().join("test.img"); let image_path = temp_dir.path().join("test.img");
std::fs::write(&image_path, b"img").unwrap(); std::fs::write(&image_path, b"img").unwrap();
let image = ImageInfo::new("test".into(), "test.img".into(), image_path, 3); let image = ImageInfo::new("test".into(), "test.img".into(), image_path, 3);
let drive = DriveInfo { let drive = DriveInfo::from_raw(
size: 5, temp_dir.path().join("ventoy.img"),
used: 0, 5,
free: 5, DriveFileAccess::Unknown,
initialized: true, );
path: temp_dir.path().join("ventoy.img"),
};
let mut state = MsdState::default(); let mut state = MsdState::default();
state state
.mounted_media .mounted_media

View File

@@ -8,8 +8,8 @@ pub use controller::MsdController;
pub use image::ImageManager; pub use image::ImageManager;
pub use monitor::MsdHealthMonitor; pub use monitor::MsdHealthMonitor;
pub use types::{ pub use types::{
DiskMode, DiskModeRequest, DownloadProgress, DownloadStatus, DriveFile, DriveInfo, DiskMode, DiskModeRequest, DownloadProgress, DownloadStatus, DriveFile, DriveFileAccess,
DriveInitRequest, ImageDownloadRequest, ImageInfo, ImageMountRequest, MountedMedia, DriveInfo, DriveInitRequest, ImageDownloadRequest, ImageInfo, ImageMountRequest, MountedMedia,
MountedMediaKind, MsdState, MsdStateResponse, MULTI_DISK_MSD_LUNS, SINGLE_DISK_MSD_LUNS, MountedMediaKind, MsdState, MsdStateResponse, MULTI_DISK_MSD_LUNS, SINGLE_DISK_MSD_LUNS,
}; };
pub use ventoy_drive::{VentoyDrive, MIN_DRIVE_SIZE_MB}; pub use ventoy_drive::{VentoyDrive, MIN_DRIVE_SIZE_MB};

View File

@@ -156,26 +156,44 @@ impl DiskMode {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DriveFileAccess {
Available,
Unsupported,
BlockedWhileConnected,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DriveInfo { pub struct DriveInfo {
pub size: u64, pub size: u64,
pub used: u64, pub used: Option<u64>,
pub free: u64, pub free: Option<u64>,
pub initialized: bool, pub initialized: bool,
pub file_access: DriveFileAccess,
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub path: PathBuf, pub path: PathBuf,
} }
impl DriveInfo { impl DriveInfo {
pub fn new(path: PathBuf, size: u64) -> Self { pub fn from_raw(path: PathBuf, size: u64, file_access: DriveFileAccess) -> Self {
Self { Self {
size, size,
used: 0, used: None,
free: size, free: None,
initialized: false, initialized: true,
file_access,
path, path,
} }
} }
pub fn with_file_access(mut self, file_access: DriveFileAccess) -> Self {
self.used = None;
self.free = None;
self.file_access = file_access;
self
}
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -267,4 +285,36 @@ mod tests {
assert!(json.get("current_image").is_none()); assert!(json.get("current_image").is_none());
assert!(json.get("slots").is_none()); assert!(json.get("slots").is_none());
} }
#[test]
fn drive_info_json_has_stable_nullable_space_and_file_access() {
let info = DriveInfo::from_raw(
PathBuf::from("/tmp/drive.img"),
4096,
DriveFileAccess::Unsupported,
);
let value = serde_json::to_value(info).unwrap();
assert_eq!(value["size"], 4096);
assert_eq!(value["used"], serde_json::Value::Null);
assert_eq!(value["free"], serde_json::Value::Null);
assert_eq!(value["initialized"], true);
assert_eq!(value["file_access"], "unsupported");
assert!(value.get("path").is_none());
}
#[test]
fn drive_file_access_serializes_all_public_states() {
for (access, expected) in [
(DriveFileAccess::Available, "available"),
(DriveFileAccess::Unsupported, "unsupported"),
(
DriveFileAccess::BlockedWhileConnected,
"blocked_while_connected",
),
(DriveFileAccess::Unknown, "unknown"),
] {
assert_eq!(serde_json::to_value(access).unwrap(), expected);
}
}
} }

View File

@@ -5,7 +5,7 @@ use tracing::{info, warn};
use ventoy_img::{FileInfo as VentoyFileInfo, VentoyError, VentoyImage}; use ventoy_img::{FileInfo as VentoyFileInfo, VentoyError, VentoyImage};
use super::types::{DriveFile, DriveInfo}; use super::types::{DriveFile, DriveFileAccess, DriveInfo};
use crate::error::{AppError, MsdErrorCode, Result}; use crate::error::{AppError, MsdErrorCode, Result};
const STREAM_CHUNK_SIZE: usize = 64 * 1024; const STREAM_CHUNK_SIZE: usize = 64 * 1024;
@@ -35,11 +35,10 @@ impl VentoyDrive {
&self.path &self.path
} }
/// Returns just the raw file size without attempting to parse the filesystem. /// Read and validate only the backing file metadata, without parsing its
/// Used as a fallback when the image has been reformatted to an unsupported /// partition table or filesystem.
/// filesystem (e.g. NTFS/exFAT) that VentoyImage cannot open. pub fn raw_info(&self, file_access: DriveFileAccess) -> Result<DriveInfo> {
pub fn raw_size(&self) -> Option<u64> { raw_drive_info(&self.path, file_access)
std::fs::metadata(&self.path).ok().map(|m| m.len())
} }
pub async fn init(&self, size_mb: u32) -> Result<DriveInfo> { pub async fn init(&self, size_mb: u32) -> Result<DriveInfo> {
@@ -60,9 +59,10 @@ impl VentoyDrive {
Ok::<DriveInfo, AppError>(DriveInfo { Ok::<DriveInfo, AppError>(DriveInfo {
size: metadata.len(), size: metadata.len(),
used: 0, used: Some(0),
free: metadata.len(), free: Some(metadata.len()),
initialized: true, initialized: true,
file_access: DriveFileAccess::Available,
path, path,
}) })
}) })
@@ -74,20 +74,23 @@ impl VentoyDrive {
} }
pub async fn info(&self) -> Result<DriveInfo> { pub async fn info(&self) -> Result<DriveInfo> {
if !self.exists() {
return Err(MsdErrorCode::MsdDriveNotInitialized.into());
}
let path = self.path.clone(); let path = self.path.clone();
let _lock = self.lock.read().await; let _lock = self.lock.read().await;
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let metadata = std::fs::metadata(&path) let raw = raw_drive_info(&path, DriveFileAccess::Unsupported)?;
.map_err(|error| drive_io_error("read drive metadata", error))?;
let image = VentoyImage::open(&path).map_err(ventoy_to_app_error)?; let image = match VentoyImage::open(&path) {
Ok(image) => image,
Err(error) if is_unsupported_filesystem_error(&error) => return Ok(raw),
Err(error) => return Err(ventoy_to_app_error(error)),
};
let files = image.list_files_recursive().map_err(ventoy_to_app_error)?; let files = match image.list_files_recursive() {
Ok(files) => files,
Err(error) if is_unsupported_filesystem_error(&error) => return Ok(raw),
Err(error) => return Err(ventoy_to_app_error(error)),
};
let used: u64 = files let used: u64 = files
.iter() .iter()
@@ -95,14 +98,15 @@ impl VentoyDrive {
.map(|f| f.size) .map(|f| f.size)
.sum(); .sum();
let size = metadata.len(); let size = raw.size;
let free = size.saturating_sub(used); let free = size.saturating_sub(used);
Ok(DriveInfo { Ok(DriveInfo {
size, size,
used, used: Some(used),
free, free: Some(free),
initialized: true, initialized: true,
file_access: DriveFileAccess::Available,
path, path,
}) })
}) })
@@ -332,6 +336,35 @@ impl VentoyDrive {
} }
} }
fn raw_drive_info(path: &Path, file_access: DriveFileAccess) -> Result<DriveInfo> {
let metadata = std::fs::metadata(path).map_err(|error| {
if error.kind() == std::io::ErrorKind::NotFound {
AppError::from(MsdErrorCode::MsdDriveNotInitialized)
} else {
drive_io_error("read drive metadata", error)
}
})?;
if !metadata.is_file() || metadata.len() == 0 {
return Err(MsdErrorCode::MsdDriveSizeInvalid.into());
}
Ok(DriveInfo::from_raw(
path.to_path_buf(),
metadata.len(),
file_access,
))
}
fn is_unsupported_filesystem_error(error: &VentoyError) -> bool {
matches!(
error,
VentoyError::FilesystemError(_)
| VentoyError::ImageError(_)
| VentoyError::PartitionError(_)
)
}
fn ventoy_to_app_error(err: VentoyError) -> AppError { fn ventoy_to_app_error(err: VentoyError) -> AppError {
warn!(%err, "Virtual drive filesystem operation failed"); warn!(%err, "Virtual drive filesystem operation failed");
match err { match err {
@@ -575,9 +608,71 @@ mod tests {
let info = drive.init(MIN_DRIVE_SIZE_MB).await.unwrap(); let info = drive.init(MIN_DRIVE_SIZE_MB).await.unwrap();
assert!(info.initialized); assert!(info.initialized);
assert_eq!(info.file_access, DriveFileAccess::Available);
assert_eq!(info.used, Some(0));
assert!(info.free.is_some());
assert!(drive.exists()); assert!(drive.exists());
} }
#[tokio::test]
async fn raw_bytes_are_reported_as_unsupported_with_capacity() {
let temp_dir = TempDir::new().unwrap();
let drive_path = temp_dir.path().join("custom.img");
std::fs::write(&drive_path, vec![0x5a; 1024 * 1024]).unwrap();
let drive = VentoyDrive::new(drive_path);
let info = drive.info().await.unwrap();
assert_eq!(info.size, 1024 * 1024);
assert_eq!(info.used, None);
assert_eq!(info.free, None);
assert_eq!(info.file_access, DriveFileAccess::Unsupported);
assert!(matches!(
drive.list_files("/").await.unwrap_err(),
AppError::Msd(error)
if error.code() == MsdErrorCode::MsdDriveFilesystemUnsupported
));
}
#[test]
fn raw_metadata_rejects_missing_empty_and_non_file_paths() {
let temp_dir = TempDir::new().unwrap();
let missing = VentoyDrive::new(temp_dir.path().join("missing.img"));
assert!(matches!(
missing.raw_info(DriveFileAccess::Unknown).unwrap_err(),
AppError::Msd(error) if error.code() == MsdErrorCode::MsdDriveNotInitialized
));
let empty_path = temp_dir.path().join("empty.img");
std::fs::write(&empty_path, []).unwrap();
let empty = VentoyDrive::new(empty_path);
assert!(matches!(
empty.raw_info(DriveFileAccess::Unknown).unwrap_err(),
AppError::Msd(error) if error.code() == MsdErrorCode::MsdDriveSizeInvalid
));
let directory = VentoyDrive::new(temp_dir.path().to_path_buf());
assert!(matches!(
directory.raw_info(DriveFileAccess::Unknown).unwrap_err(),
AppError::Msd(error) if error.code() == MsdErrorCode::MsdDriveSizeInvalid
));
}
#[tokio::test]
async fn supported_drive_info_has_space_values() {
if !ensure_resources() {
return;
}
let temp_dir = TempDir::new().unwrap();
let drive = VentoyDrive::new(temp_dir.path().join("supported.img"));
drive.init(MIN_DRIVE_SIZE_MB).await.unwrap();
let info = drive.info().await.unwrap();
assert_eq!(info.file_access, DriveFileAccess::Available);
assert!(info.used.is_some());
assert!(info.free.is_some());
}
#[tokio::test] #[tokio::test]
async fn test_drive_mkdir() { async fn test_drive_mkdir() {
if !ensure_resources() { if !ensure_resources() {

View File

@@ -2,7 +2,7 @@ use super::config::apply::try_apply_lock;
use super::*; use super::*;
use crate::msd::{ use crate::msd::{
DiskModeRequest, DownloadProgress, DriveFile, DriveInfo, DriveInitRequest, DiskModeRequest, DownloadProgress, DriveFile, DriveFileAccess, DriveInfo, DriveInitRequest,
ImageDownloadRequest, ImageInfo, ImageManager, ImageMountRequest, MsdErrorCode, MsdState, ImageDownloadRequest, ImageInfo, ImageManager, ImageMountRequest, MsdErrorCode, MsdState,
MsdStateResponse, VentoyDrive, MIN_DRIVE_SIZE_MB, MsdStateResponse, VentoyDrive, MIN_DRIVE_SIZE_MB,
}; };
@@ -420,16 +420,26 @@ pub async fn msd_drive_info(State(state): State<Arc<AppState>>) -> Result<Json<D
let drive_path = config.msd.drive_path(); let drive_path = config.msd.drive_path();
let drive = VentoyDrive::new(drive_path); let drive = VentoyDrive::new(drive_path);
if !drive.exists() { let msd_guard = state.msd.read().await;
// 404: drive image file does not exist at all — truly not initialized let connected = match msd_guard.as_ref() {
return Err(MsdErrorCode::MsdDriveNotInitialized.into()); Some(controller) => controller.is_drive_connected().await,
None => false,
};
// Never parse the filesystem while the USB host owns it. Metadata is safe
// to read and still lets the UI show the backing image capacity.
let info = if connected {
drive.raw_info(DriveFileAccess::BlockedWhileConnected)
} else {
drive.info().await
}
.map_err(|error| operation_failed("read virtual drive info", error))?;
if let Some(controller) = msd_guard.as_ref() {
controller.set_drive_info(Some(info.clone())).await;
} }
drive Ok(Json(info))
.info()
.await
.map(Json)
.map_err(|error| operation_failed("read virtual drive info", error))
} }
/// Initialize Ventoy drive /// Initialize Ventoy drive
@@ -439,7 +449,6 @@ pub async fn msd_drive_init(
payload: std::result::Result<Json<DriveInitRequest>, JsonRejection>, payload: std::result::Result<Json<DriveInitRequest>, JsonRejection>,
) -> Result<Json<DriveInfo>> { ) -> Result<Json<DriveInfo>> {
let req = parse_msd_json(payload)?; let req = parse_msd_json(payload)?;
assert_drive_not_connected(&state).await?;
let config = state.config.get(); let config = state.config.get();
let msd_dir = config.msd.msd_dir_path(); let msd_dir = config.msd.msd_dir_path();
@@ -449,6 +458,16 @@ pub async fn msd_drive_init(
})?; })?;
validate_drive_init_size(req.size_mb, disk_space.available)?; validate_drive_init_size(req.size_mb, disk_space.available)?;
// Mount/unmount handlers also take this outer write lock. Holding it
// across image creation prevents a mount from racing the destructive
// reinitialization after the connected-state check.
let msd_guard = state.msd.write().await;
if let Some(controller) = msd_guard.as_ref() {
if controller.is_drive_connected().await {
return Err(MsdErrorCode::MsdDriveConnected.into());
}
}
let drive_path = config.msd.drive_path(); let drive_path = config.msd.drive_path();
let drive = VentoyDrive::new(drive_path); let drive = VentoyDrive::new(drive_path);
@@ -456,6 +475,9 @@ pub async fn msd_drive_init(
.init(req.size_mb) .init(req.size_mb)
.await .await
.map_err(|error| operation_failed("initialize virtual drive", error))?; .map_err(|error| operation_failed("initialize virtual drive", error))?;
if let Some(controller) = msd_guard.as_ref() {
controller.set_drive_info(Some(info.clone())).await;
}
Ok(Json(info)) Ok(Json(info))
} }
@@ -471,14 +493,15 @@ pub async fn msd_drive_delete(State(state): State<Arc<AppState>>) -> Result<Json
return Err(MsdErrorCode::MsdDriveConnected.into()); return Err(MsdErrorCode::MsdDriveConnected.into());
} }
} }
drop(msd_guard);
// Delete the drive file // Delete the drive file
let drive_path = config.msd.drive_path(); let drive_path = config.msd.drive_path();
if drive_path.exists() { if drive_path.exists() {
std::fs::remove_file(&drive_path) std::fs::remove_file(&drive_path)
.map_err(|error| classify_storage_error("delete virtual drive", error))?; .map_err(|error| classify_storage_error("delete virtual drive", error))?;
} }
if let Some(controller) = msd_guard.as_ref() {
controller.set_drive_info(None).await;
}
Ok(Json(LoginResponse { Ok(Json(LoginResponse {
success: true, success: true,