diff --git a/libs/ventoy-img-rs/Cargo.toml b/libs/ventoy-img-rs/Cargo.toml index 1f301d90..a5da56c5 100644 --- a/libs/ventoy-img-rs/Cargo.toml +++ b/libs/ventoy-img-rs/Cargo.toml @@ -11,8 +11,6 @@ clap = { version = "4", features = ["derive"] } # Error handling thiserror = "2" - -[dev-dependencies] tempfile = "3" [profile.release] diff --git a/libs/ventoy-img-rs/src/image.rs b/libs/ventoy-img-rs/src/image.rs index a74ef32e..c3f11100 100644 --- a/libs/ventoy-img-rs/src/image.rs +++ b/libs/ventoy-img-rs/src/image.rs @@ -28,8 +28,15 @@ impl VentoyImage { path.display() ); - // Create sparse file - let mut file = File::create(path)?; + // Build beside the destination so a failed create cannot leave a partial image. + let parent = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")); + let mut temp = tempfile::Builder::new() + .prefix(".ventoy-") + .tempfile_in(parent)?; + let mut file = temp.as_file_mut(); file.set_len(size)?; // Write boot code @@ -64,6 +71,8 @@ impl VentoyImage { format_exfat(&mut file, layout.data_offset(), layout.data_size(), label)?; file.flush()?; + temp.persist(path) + .map_err(|error| VentoyError::Io(error.error))?; println!("[INFO] Ventoy IMG created successfully!"); @@ -274,3 +283,20 @@ impl VentoyImage { &self.path } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn failed_create_does_not_publish_partial_image() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("ventoy.img"); + + let error = VentoyImage::create(&path, "64M", "TEST").err().unwrap(); + + assert!(matches!(error, VentoyError::ResourceNotFound(_))); + assert!(!path.exists()); + assert_eq!(std::fs::read_dir(dir.path()).unwrap().count(), 0); + } +} diff --git a/src/msd/ventoy_drive.rs b/src/msd/ventoy_drive.rs index 8c9748c7..0a9dee2a 100644 --- a/src/msd/ventoy_drive.rs +++ b/src/msd/ventoy_drive.rs @@ -56,7 +56,7 @@ impl VentoyDrive { info!("Creating {} MB Ventoy drive at {}", size_mb, path.display()); let info = tokio::task::spawn_blocking(move || { - VentoyImage::create(&path, &size_str, DEFAULT_LABEL).map_err(ventoy_to_app_error)?; + VentoyImage::create(&path, &size_str, DEFAULT_LABEL).map_err(drive_init_error)?; let metadata = std::fs::metadata(&path) .map_err(|e| AppError::Internal(format!("Failed to read drive metadata: {}", e)))?; @@ -354,6 +354,30 @@ fn ventoy_to_app_error(err: VentoyError) -> AppError { } } +fn drive_init_error(err: VentoyError) -> AppError { + let VentoyError::Io(error) = err else { + return ventoy_to_app_error(err); + }; + + #[cfg(unix)] + match error.raw_os_error() { + Some(libc::EFBIG) => AppError::BadRequest( + "MSD directory filesystem does not support a virtual drive file of this size".into(), + ), + Some(libc::ENOSPC) => AppError::BadRequest( + "MSD directory does not have enough free space for the virtual drive".into(), + ), + Some(libc::EROFS) => AppError::BadRequest("MSD directory filesystem is read-only".into()), + Some(libc::EACCES | libc::EPERM) => AppError::BadRequest( + "One-KVM does not have permission to write to the MSD directory".into(), + ), + _ => AppError::Io(error), + } + + #[cfg(not(unix))] + AppError::Io(error) +} + fn ventoy_file_to_drive_file(info: VentoyFileInfo, parent_path: &str) -> DriveFile { let full_path = if parent_path.is_empty() || parent_path == "/" { format!("/{}", info.name) @@ -436,12 +460,26 @@ impl Drop for ChannelWriter { #[cfg(test)] mod tests { use super::*; + use crate::error::AppError; use std::process::Command; use std::sync::OnceLock; use tempfile::TempDir; static RESOURCE_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../ventoy-img-rs/resources"); + #[test] + fn classifies_drive_creation_io_errors() { + for (errno, expected) in [ + (libc::EFBIG, "does not support"), + (libc::ENOSPC, "enough free space"), + (libc::EROFS, "read-only"), + (libc::EACCES, "permission"), + ] { + let error = drive_init_error(VentoyError::Io(std::io::Error::from_raw_os_error(errno))); + assert!(matches!(error, AppError::BadRequest(message) if message.contains(expected))); + } + } + fn init_ventoy_resources() -> bool { static INIT: OnceLock = OnceLock::new(); *INIT.get_or_init(|| { diff --git a/web/src/components/MsdDialog.vue b/web/src/components/MsdDialog.vue index 4c187e93..b194011a 100644 --- a/web/src/components/MsdDialog.vue +++ b/web/src/components/MsdDialog.vue @@ -469,7 +469,16 @@ async function createDrive() { showDriveInitDialog.value = false } catch (e) { console.error('Failed to initialize drive:', e) - toast.error(t('msd.driveCreateFailed')) + let description: string | undefined + if (e instanceof ApiError) { + const message = e.message + if (message.includes('does not support a virtual drive file')) description = t('msd.driveFileTooLarge') + else if (message.includes('does not have enough free space')) description = t('msd.driveSpaceUnavailable') + else if (message.includes('filesystem is read-only')) description = t('msd.driveReadOnly') + else if (message.includes('permission to write')) description = t('msd.drivePermissionDenied') + else description = message + } + toast.error(t('msd.driveCreateFailed'), { description }) } finally { initializingDrive.value = false } diff --git a/web/src/i18n/en-US.ts b/web/src/i18n/en-US.ts index f4273036..461d6b01 100644 --- a/web/src/i18n/en-US.ts +++ b/web/src/i18n/en-US.ts @@ -396,7 +396,10 @@ export default { driveSpaceUnknown: 'Unable to read available space for the MSD directory filesystem', driveSpaceUnavailable: 'Not enough space on the MSD directory filesystem to create a virtual drive', driveSpaceTooSmall: 'Available space is less than {min} MB; cannot create a virtual drive', - driveCreateFailed: 'Failed to create virtual drive. Check available space for the MSD directory', + driveCreateFailed: 'Failed to create virtual drive', + driveFileTooLarge: 'The MSD directory filesystem does not support a virtual drive file this large', + driveReadOnly: 'The MSD directory filesystem is read-only', + drivePermissionDenied: 'One-KVM does not have permission to write to the MSD directory', driveConnectedBlocked: 'Disconnect the drive before editing files', driveConnectedFilesHidden: 'Drive is connected to target machine — file list unavailable', uploadFailed: 'File upload failed', diff --git a/web/src/i18n/zh-CN.ts b/web/src/i18n/zh-CN.ts index 13aa23fe..7d4da3b5 100644 --- a/web/src/i18n/zh-CN.ts +++ b/web/src/i18n/zh-CN.ts @@ -395,7 +395,10 @@ export default { driveSpaceUnknown: '无法读取 MSD 目录所在储存空间的剩余可用空间', driveSpaceUnavailable: 'MSD 目录所在储存空间不足,无法创建虚拟U盘', driveSpaceTooSmall: '剩余可用空间不足 {min} MB,无法创建虚拟U盘', - driveCreateFailed: '虚拟U盘创建失败,请检查 MSD 目录剩余空间', + driveCreateFailed: '虚拟U盘创建失败', + driveFileTooLarge: 'MSD 目录所在文件系统不支持这么大的单个虚拟盘文件', + driveReadOnly: 'MSD 目录所在文件系统为只读', + drivePermissionDenied: 'One-KVM 没有写入 MSD 目录的权限', driveConnectedBlocked: '请先断开连接,再操作文件', driveConnectedFilesHidden: '已连接到被控机,文件列表暂不可用', uploadFailed: '文件上传失败',