mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-12 18:44:25 +08:00
fix(msd): 区分虚拟磁盘挂载能力与文件访问状态
使用原始文件元数据挂载虚拟磁盘,允许主机使用网页文件管理不支持的文件系统。连接期间只读取元数据;初始化和删除持有外层写锁,并同步控制器缓存。 API 的 used/free 改为可空值,新增 file_access 表示 available、unsupported、blocked_while_connected 或 unknown,需要前端配套处理。补充状态序列化、元数据校验和文件系统兼容测试。
This commit is contained in:
@@ -102,7 +102,9 @@ impl MsdErrorCode {
|
||||
Self::MsdDownloadIncomplete => "The remote image download was incomplete.",
|
||||
Self::MsdDriveNotInitialized => "The virtual drive is not initialized.",
|
||||
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::MsdStorageSpaceUnavailable => {
|
||||
"Available virtual media storage space could not be determined."
|
||||
@@ -183,7 +185,7 @@ impl MsdErrorCode {
|
||||
"Verify the remote server and network connection, then retry."
|
||||
}
|
||||
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 => {
|
||||
"Verify that virtual media storage is available, then retry."
|
||||
@@ -386,7 +388,7 @@ mod tests {
|
||||
(
|
||||
MsdDriveFilesystemUnsupported,
|
||||
"MSD_DRIVE_FILESYSTEM_UNSUPPORTED",
|
||||
"The virtual drive filesystem is unsupported.",
|
||||
"Web file management does not support this virtual drive format.",
|
||||
),
|
||||
(
|
||||
MsdDriveSizeInvalid,
|
||||
|
||||
@@ -8,9 +8,10 @@ use tracing::{debug, info, warn};
|
||||
use super::image::ImageManager;
|
||||
use super::monitor::MsdHealthMonitor;
|
||||
use super::types::{
|
||||
DiskMode, DownloadProgress, DownloadStatus, DriveInfo, ImageInfo, MountedMedia,
|
||||
MountedMediaKind, MsdState,
|
||||
DiskMode, DownloadProgress, DownloadStatus, DriveFileAccess, DriveInfo, ImageInfo,
|
||||
MountedMedia, MountedMediaKind, MsdState,
|
||||
};
|
||||
use super::ventoy_drive::VentoyDrive;
|
||||
use crate::error::{AppError, MsdErrorCode, Result};
|
||||
use crate::otg::{MsdFunction, MsdLunConfig, OtgService};
|
||||
|
||||
@@ -83,14 +84,9 @@ impl MsdController {
|
||||
state.available = true;
|
||||
|
||||
if self.drive_path.exists() {
|
||||
if let Ok(metadata) = std::fs::metadata(&self.drive_path) {
|
||||
let drive_info = DriveInfo {
|
||||
size: metadata.len(),
|
||||
used: 0,
|
||||
free: metadata.len(),
|
||||
initialized: true,
|
||||
path: self.drive_path.clone(),
|
||||
};
|
||||
if let Ok(drive_info) =
|
||||
VentoyDrive::new(self.drive_path.clone()).raw_info(DriveFileAccess::Unknown)
|
||||
{
|
||||
state.drive_info = Some(drive_info.clone());
|
||||
debug!(
|
||||
"Found existing virtual drive: {}",
|
||||
@@ -199,28 +195,6 @@ impl MsdController {
|
||||
|
||||
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
|
||||
.mounted_media
|
||||
.iter()
|
||||
@@ -229,8 +203,22 @@ impl MsdController {
|
||||
return Err(MsdErrorCode::MsdMediaAlreadyMounted.into());
|
||||
}
|
||||
|
||||
let drive_info =
|
||||
drive_info.ok_or_else(|| AppError::from(MsdErrorCode::MsdDriveNotInitialized))?;
|
||||
let drive_info = match self.drive_mount_info() {
|
||||
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)
|
||||
.ok_or_else(|| AppError::from(MsdErrorCode::MsdMediaSlotsFull))?;
|
||||
|
||||
@@ -240,6 +228,8 @@ impl MsdController {
|
||||
return Err(e);
|
||||
}
|
||||
state.mounted_media.push(media);
|
||||
state.drive_info =
|
||||
Some(drive_info.with_file_access(DriveFileAccess::BlockedWhileConnected));
|
||||
|
||||
info!(
|
||||
"Mounted virtual drive on LUN {}: {}",
|
||||
@@ -254,6 +244,15 @@ impl MsdController {
|
||||
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<()> {
|
||||
if !state.available {
|
||||
self.monitor
|
||||
@@ -293,6 +292,16 @@ impl MsdController {
|
||||
}
|
||||
|
||||
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.mounted_media.clear();
|
||||
}
|
||||
@@ -397,6 +406,12 @@ impl MsdController {
|
||||
|
||||
self.disconnect_lun(media.lun).await?;
|
||||
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");
|
||||
|
||||
drop(state);
|
||||
@@ -490,6 +505,16 @@ impl MsdController {
|
||||
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();
|
||||
info!("Disconnected all mounted media");
|
||||
|
||||
@@ -744,6 +769,29 @@ mod tests {
|
||||
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]
|
||||
fn single_disk_mode_only_exposes_lun_zero() {
|
||||
let mut state = MsdState::default();
|
||||
@@ -842,13 +890,7 @@ mod tests {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let drive_path = temp_dir.path().join("ventoy.img");
|
||||
std::fs::write(&drive_path, b"drive").unwrap();
|
||||
let drive = DriveInfo {
|
||||
size: 5,
|
||||
used: 0,
|
||||
free: 5,
|
||||
initialized: true,
|
||||
path: drive_path,
|
||||
};
|
||||
let drive = DriveInfo::from_raw(drive_path, 5, DriveFileAccess::Unknown);
|
||||
let mut state = MsdState::default();
|
||||
MsdController::reset_mounts_for_mode(&mut state, DiskMode::Multi);
|
||||
state.mounted_media.push(MountedMedia::drive(0, &drive));
|
||||
@@ -900,13 +942,11 @@ mod tests {
|
||||
let image_path = temp_dir.path().join("test.img");
|
||||
std::fs::write(&image_path, b"img").unwrap();
|
||||
let image = ImageInfo::new("test".into(), "test.img".into(), image_path, 3);
|
||||
let drive = DriveInfo {
|
||||
size: 5,
|
||||
used: 0,
|
||||
free: 5,
|
||||
initialized: true,
|
||||
path: temp_dir.path().join("ventoy.img"),
|
||||
};
|
||||
let drive = DriveInfo::from_raw(
|
||||
temp_dir.path().join("ventoy.img"),
|
||||
5,
|
||||
DriveFileAccess::Unknown,
|
||||
);
|
||||
let mut state = MsdState::default();
|
||||
state
|
||||
.mounted_media
|
||||
|
||||
@@ -8,8 +8,8 @@ pub use controller::MsdController;
|
||||
pub use image::ImageManager;
|
||||
pub use monitor::MsdHealthMonitor;
|
||||
pub use types::{
|
||||
DiskMode, DiskModeRequest, DownloadProgress, DownloadStatus, DriveFile, DriveInfo,
|
||||
DriveInitRequest, ImageDownloadRequest, ImageInfo, ImageMountRequest, MountedMedia,
|
||||
DiskMode, DiskModeRequest, DownloadProgress, DownloadStatus, DriveFile, DriveFileAccess,
|
||||
DriveInfo, DriveInitRequest, ImageDownloadRequest, ImageInfo, ImageMountRequest, MountedMedia,
|
||||
MountedMediaKind, MsdState, MsdStateResponse, MULTI_DISK_MSD_LUNS, SINGLE_DISK_MSD_LUNS,
|
||||
};
|
||||
pub use ventoy_drive::{VentoyDrive, MIN_DRIVE_SIZE_MB};
|
||||
|
||||
@@ -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)]
|
||||
pub struct DriveInfo {
|
||||
pub size: u64,
|
||||
pub used: u64,
|
||||
pub free: u64,
|
||||
pub used: Option<u64>,
|
||||
pub free: Option<u64>,
|
||||
pub initialized: bool,
|
||||
pub file_access: DriveFileAccess,
|
||||
#[serde(skip_serializing)]
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl DriveInfo {
|
||||
pub fn new(path: PathBuf, size: u64) -> Self {
|
||||
pub fn from_raw(path: PathBuf, size: u64, file_access: DriveFileAccess) -> Self {
|
||||
Self {
|
||||
size,
|
||||
used: 0,
|
||||
free: size,
|
||||
initialized: false,
|
||||
used: None,
|
||||
free: None,
|
||||
initialized: true,
|
||||
file_access,
|
||||
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)]
|
||||
@@ -267,4 +285,36 @@ mod tests {
|
||||
assert!(json.get("current_image").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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use tracing::{info, warn};
|
||||
|
||||
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};
|
||||
|
||||
const STREAM_CHUNK_SIZE: usize = 64 * 1024;
|
||||
@@ -35,11 +35,10 @@ impl VentoyDrive {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Returns just the raw file size without attempting to parse the filesystem.
|
||||
/// Used as a fallback when the image has been reformatted to an unsupported
|
||||
/// filesystem (e.g. NTFS/exFAT) that VentoyImage cannot open.
|
||||
pub fn raw_size(&self) -> Option<u64> {
|
||||
std::fs::metadata(&self.path).ok().map(|m| m.len())
|
||||
/// Read and validate only the backing file metadata, without parsing its
|
||||
/// partition table or filesystem.
|
||||
pub fn raw_info(&self, file_access: DriveFileAccess) -> Result<DriveInfo> {
|
||||
raw_drive_info(&self.path, file_access)
|
||||
}
|
||||
|
||||
pub async fn init(&self, size_mb: u32) -> Result<DriveInfo> {
|
||||
@@ -60,9 +59,10 @@ impl VentoyDrive {
|
||||
|
||||
Ok::<DriveInfo, AppError>(DriveInfo {
|
||||
size: metadata.len(),
|
||||
used: 0,
|
||||
free: metadata.len(),
|
||||
used: Some(0),
|
||||
free: Some(metadata.len()),
|
||||
initialized: true,
|
||||
file_access: DriveFileAccess::Available,
|
||||
path,
|
||||
})
|
||||
})
|
||||
@@ -74,20 +74,23 @@ impl VentoyDrive {
|
||||
}
|
||||
|
||||
pub async fn info(&self) -> Result<DriveInfo> {
|
||||
if !self.exists() {
|
||||
return Err(MsdErrorCode::MsdDriveNotInitialized.into());
|
||||
}
|
||||
|
||||
let path = self.path.clone();
|
||||
let _lock = self.lock.read().await;
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let metadata = std::fs::metadata(&path)
|
||||
.map_err(|error| drive_io_error("read drive metadata", error))?;
|
||||
let raw = raw_drive_info(&path, DriveFileAccess::Unsupported)?;
|
||||
|
||||
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
|
||||
.iter()
|
||||
@@ -95,14 +98,15 @@ impl VentoyDrive {
|
||||
.map(|f| f.size)
|
||||
.sum();
|
||||
|
||||
let size = metadata.len();
|
||||
let size = raw.size;
|
||||
let free = size.saturating_sub(used);
|
||||
|
||||
Ok(DriveInfo {
|
||||
size,
|
||||
used,
|
||||
free,
|
||||
used: Some(used),
|
||||
free: Some(free),
|
||||
initialized: true,
|
||||
file_access: DriveFileAccess::Available,
|
||||
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 {
|
||||
warn!(%err, "Virtual drive filesystem operation failed");
|
||||
match err {
|
||||
@@ -575,9 +608,71 @@ mod tests {
|
||||
|
||||
let info = drive.init(MIN_DRIVE_SIZE_MB).await.unwrap();
|
||||
assert!(info.initialized);
|
||||
assert_eq!(info.file_access, DriveFileAccess::Available);
|
||||
assert_eq!(info.used, Some(0));
|
||||
assert!(info.free.is_some());
|
||||
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]
|
||||
async fn test_drive_mkdir() {
|
||||
if !ensure_resources() {
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::config::apply::try_apply_lock;
|
||||
use super::*;
|
||||
|
||||
use crate::msd::{
|
||||
DiskModeRequest, DownloadProgress, DriveFile, DriveInfo, DriveInitRequest,
|
||||
DiskModeRequest, DownloadProgress, DriveFile, DriveFileAccess, DriveInfo, DriveInitRequest,
|
||||
ImageDownloadRequest, ImageInfo, ImageManager, ImageMountRequest, MsdErrorCode, MsdState,
|
||||
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 = VentoyDrive::new(drive_path);
|
||||
|
||||
if !drive.exists() {
|
||||
// 404: drive image file does not exist at all — truly not initialized
|
||||
return Err(MsdErrorCode::MsdDriveNotInitialized.into());
|
||||
let msd_guard = state.msd.read().await;
|
||||
let connected = match msd_guard.as_ref() {
|
||||
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
|
||||
.info()
|
||||
.await
|
||||
.map(Json)
|
||||
.map_err(|error| operation_failed("read virtual drive info", error))
|
||||
Ok(Json(info))
|
||||
}
|
||||
|
||||
/// Initialize Ventoy drive
|
||||
@@ -439,7 +449,6 @@ pub async fn msd_drive_init(
|
||||
payload: std::result::Result<Json<DriveInitRequest>, JsonRejection>,
|
||||
) -> Result<Json<DriveInfo>> {
|
||||
let req = parse_msd_json(payload)?;
|
||||
assert_drive_not_connected(&state).await?;
|
||||
let config = state.config.get();
|
||||
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)?;
|
||||
|
||||
// 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 = VentoyDrive::new(drive_path);
|
||||
|
||||
@@ -456,6 +475,9 @@ pub async fn msd_drive_init(
|
||||
.init(req.size_mb)
|
||||
.await
|
||||
.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))
|
||||
}
|
||||
|
||||
@@ -471,14 +493,15 @@ pub async fn msd_drive_delete(State(state): State<Arc<AppState>>) -> Result<Json
|
||||
return Err(MsdErrorCode::MsdDriveConnected.into());
|
||||
}
|
||||
}
|
||||
drop(msd_guard);
|
||||
|
||||
// Delete the drive file
|
||||
let drive_path = config.msd.drive_path();
|
||||
if drive_path.exists() {
|
||||
std::fs::remove_file(&drive_path)
|
||||
.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 {
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user