fix: 修复 MSD ISO/FLASH 挂载识别错误;完善错误提示

This commit is contained in:
mofeng-git
2026-07-26 22:59:27 +08:00
parent 27c8da9a75
commit 376dc97134
24 changed files with 1703 additions and 522 deletions

View File

@@ -11,7 +11,7 @@ use super::types::{
DiskMode, DownloadProgress, DownloadStatus, DriveInfo, ImageInfo, MountedMedia,
MountedMediaKind, MsdState,
};
use crate::error::{AppError, Result};
use crate::error::{AppError, MsdErrorCode, Result};
use crate::otg::{MsdFunction, MsdLunConfig, OtgService};
pub struct MsdController {
@@ -70,9 +70,11 @@ impl MsdController {
}
info!("Fetching MSD function from OtgService");
let msd_func = self.otg_service.msd_function().await.ok_or_else(|| {
AppError::Internal("MSD function is not active in OtgService".to_string())
})?;
let msd_func = self
.otg_service
.msd_function()
.await
.ok_or_else(|| AppError::from(MsdErrorCode::MsdUnavailable))?;
*self.msd_function.write().await = Some(msd_func);
@@ -148,7 +150,7 @@ impl MsdController {
read_only: bool,
requested_lun: Option<u8>,
) -> Result<()> {
let _op_guard = self.operation_lock.write().await;
let _op_guard = self.try_operation()?;
let mut state = self.state.write().await;
let previous_state = state.clone();
@@ -159,7 +161,7 @@ impl MsdController {
self.monitor
.report_error(&error_msg, "image_not_found")
.await;
return Err(AppError::Internal(error_msg));
return Err(MsdErrorCode::MsdResourceNotFound.into());
}
if state
@@ -167,7 +169,7 @@ impl MsdController {
.iter()
.any(|media| media.kind == MountedMediaKind::Image && media.id == image.id)
{
return Err(AppError::BadRequest("Image is already mounted".to_string()));
return Err(MsdErrorCode::MsdMediaAlreadyMounted.into());
}
let lun = Self::select_lun(&state, requested_lun)?;
@@ -195,19 +197,17 @@ impl MsdController {
}
pub async fn mount_drive(&self) -> Result<()> {
let _op_guard = self.operation_lock.write().await;
let _op_guard = self.try_operation()?;
let mut state = self.state.write().await;
let previous_state = state.clone();
self.assert_available(&state).await?;
if !self.drive_path.exists() {
let err =
AppError::Internal("Virtual drive not initialized. Call init first.".to_string());
self.monitor
.report_error("Virtual drive not initialized", "drive_not_found")
.await;
return Err(err);
return Err(MsdErrorCode::MsdDriveNotInitialized.into());
}
let drive_info = state.drive_info.clone().or_else(|| {
@@ -230,15 +230,13 @@ impl MsdController {
.iter()
.any(|media| media.kind == MountedMediaKind::Drive)
{
return Err(AppError::BadRequest(
"Virtual drive is already mounted".to_string(),
));
return Err(MsdErrorCode::MsdMediaAlreadyMounted.into());
}
let drive_info = drive_info
.ok_or_else(|| AppError::Internal("Virtual drive info is unavailable".to_string()))?;
let drive_info =
drive_info.ok_or_else(|| AppError::from(MsdErrorCode::MsdDriveNotInitialized))?;
let lun = Self::lowest_free_lun(&state)
.ok_or_else(|| AppError::BadRequest("Media slots are full".to_string()))?;
.ok_or_else(|| AppError::from(MsdErrorCode::MsdMediaSlotsFull))?;
let media = MountedMedia::drive(lun, &drive_info);
if let Err(e) = self.configure_media(&media).await {
@@ -265,7 +263,7 @@ impl MsdController {
self.monitor
.report_error("MSD not available", "not_available")
.await;
return Err(AppError::Internal("MSD not available".to_string()));
return Err(MsdErrorCode::MsdUnavailable.into());
}
Ok(())
}
@@ -286,20 +284,14 @@ impl MsdController {
fn select_lun(state: &MsdState, requested_lun: Option<u8>) -> Result<u8> {
let Some(lun) = requested_lun else {
return Self::lowest_free_lun(state)
.ok_or_else(|| AppError::BadRequest("Media slots are full".to_string()));
.ok_or_else(|| AppError::from(MsdErrorCode::MsdMediaSlotsFull));
};
if lun >= state.disk_mode.capacity() {
return Err(AppError::BadRequest(format!(
"Media slot {} is outside the current disk mode capacity",
lun + 1
)));
return Err(MsdErrorCode::MsdInvalidRequest.into());
}
if state.mounted_media.iter().any(|media| media.lun == lun) {
return Err(AppError::BadRequest(format!(
"Media slot {} is already occupied",
lun + 1
)));
return Err(MsdErrorCode::MsdMediaSlotsFull.into());
}
Ok(lun)
}
@@ -310,7 +302,7 @@ impl MsdController {
}
pub async fn set_disk_mode(&self, disk_mode: DiskMode) -> Result<bool> {
let _op_guard = self.operation_lock.write().await;
let _op_guard = self.try_operation()?;
let previous_state = {
let mut state = self.state.write().await;
self.assert_available(&state).await?;
@@ -327,9 +319,10 @@ impl MsdController {
self.otg_service
.set_msd_lun_capacity(disk_mode.capacity())
.await?;
self.otg_service.msd_function().await.ok_or_else(|| {
AppError::Internal("MSD function missing after OTG rebuild".to_string())
})
self.otg_service
.msd_function()
.await
.ok_or_else(|| AppError::from(MsdErrorCode::MsdOperationFailed))
}
.await;
@@ -349,7 +342,7 @@ impl MsdController {
.report_error(&error_msg, "disk_mode_rollback_failed")
.await;
self.mark_device_info_dirty().await;
return Err(AppError::Internal(error_msg));
return Err(MsdErrorCode::MsdOperationFailed.into());
}
let mut state = self.state.write().await;
@@ -360,7 +353,7 @@ impl MsdController {
.report_error(&error_msg, "disk_mode_switch_failed")
.await;
self.mark_device_info_dirty().await;
return Err(AppError::Internal(error_msg));
return Err(MsdErrorCode::MsdOperationFailed.into());
}
};
*self.msd_function.write().await = Some(msd_function);
@@ -397,7 +390,7 @@ impl MsdController {
where
F: Fn(&MountedMedia) -> bool,
{
let _op_guard = self.operation_lock.write().await;
let _op_guard = self.try_operation()?;
let mut state = self.state.write().await;
let Some(index) = state.mounted_media.iter().position(predicate) else {
@@ -419,25 +412,22 @@ impl MsdController {
}
async fn configure_media(&self, media: &MountedMedia) -> Result<()> {
let gadget_path = self.active_gadget_path().await?;
let msd_hold = self.msd_function.read().await;
let Some(ref msd) = *msd_hold else {
self.monitor
.report_error("MSD function not initialized", "not_initialized")
.await;
return Err(AppError::Internal(
"MSD function not initialized".to_string(),
));
};
if let Err(e) = msd
.configure_lun_async(&gadget_path, media.lun, &Self::media_config(media))
if let Err(e) = self
.otg_service
.configure_msd_lun(media.lun, &Self::media_config(media))
.await
{
let error_msg = format!("Failed to configure LUN {}: {}", media.lun, e);
self.monitor
.report_error(&error_msg, "configfs_error")
.await;
return Err(e);
return Err(match e {
AppError::Msd(error) => AppError::Msd(error),
error => {
warn!(%error, "Unclassified MSD media configuration failure");
MsdErrorCode::MsdOperationFailed.into()
}
});
}
Ok(())
}
@@ -447,7 +437,7 @@ impl MsdController {
let msd_hold = self.msd_function.read().await;
let msd = msd_hold
.as_ref()
.ok_or_else(|| AppError::Internal("MSD function not initialized".to_string()))?;
.ok_or_else(|| AppError::from(MsdErrorCode::MsdUnavailable))?;
msd.disconnect_lun_async(&gadget_path, lun).await
}
@@ -455,9 +445,11 @@ impl MsdController {
self.otg_service
.set_msd_lun_capacity(previous_state.disk_mode.capacity())
.await?;
let msd_function = self.otg_service.msd_function().await.ok_or_else(|| {
AppError::Internal("MSD function missing after OTG rollback".to_string())
})?;
let msd_function = self
.otg_service
.msd_function()
.await
.ok_or_else(|| AppError::from(MsdErrorCode::MsdOperationFailed))?;
*self.msd_function.write().await = Some(msd_function);
for media in &previous_state.mounted_media {
self.configure_media(media).await?;
@@ -473,7 +465,7 @@ impl MsdController {
}
pub async fn disconnect(&self) -> Result<()> {
let _op_guard = self.operation_lock.write().await;
let _op_guard = self.try_operation()?;
let mut state = self.state.write().await;
if state.mounted_media.is_empty() {
@@ -488,10 +480,13 @@ impl MsdController {
for prior in &disconnected {
if let Err(restore_error) = self.configure_media(prior).await {
state.available = false;
return Err(AppError::Internal(format!(
"Failed to disconnect LUN {}: {error}; restore failed: {restore_error}",
media.lun
)));
warn!(
lun = media.lun,
disconnect_error = %error,
%restore_error,
"Failed to restore MSD media after disconnect failure"
);
return Err(MsdErrorCode::MsdDisconnectFailed.into());
}
}
return Err(error);
@@ -520,16 +515,14 @@ impl MsdController {
}
pub async fn delete_image(&self, image_id: &str) -> Result<()> {
let _op_guard = self.operation_lock.write().await;
let _op_guard = self.try_operation()?;
let state = self.state.read().await;
if state
.mounted_media
.iter()
.any(|media| media.kind == MountedMediaKind::Image && media.id == image_id)
{
return Err(AppError::BadRequest(
"Cannot delete image while it is mounted".to_string(),
));
return Err(MsdErrorCode::MsdMediaInUse.into());
}
ImageManager::new(self.images_path.clone()).delete(image_id)
@@ -540,6 +533,12 @@ impl MsdController {
url: String,
filename: Option<String>,
) -> Result<DownloadProgress> {
let parsed_url =
reqwest::Url::parse(&url).map_err(|_| AppError::from(MsdErrorCode::MsdInvalidUrl))?;
if !matches!(parsed_url.scheme(), "http" | "https") {
return Err(MsdErrorCode::MsdInvalidUrl.into());
}
let download_id = uuid::Uuid::new_v4().to_string();
let cancel_token = CancellationToken::new();
@@ -560,7 +559,7 @@ impl MsdController {
total_bytes: None,
progress_pct: None,
status: DownloadStatus::Started,
error: None,
error_code: None,
};
self.publish_event(crate::events::SystemEvent::MsdDownloadProgress {
@@ -571,6 +570,7 @@ impl MsdController {
total_bytes: None,
progress_pct: None,
status: "started".to_string(),
error_code: None,
})
.await;
@@ -600,6 +600,7 @@ impl MsdController {
total_bytes: total,
progress_pct,
status: "in_progress".to_string(),
error_code: None,
});
}
};
@@ -624,11 +625,16 @@ impl MsdController {
total_bytes: Some(image_info.size),
progress_pct: Some(100.0),
status: "completed".to_string(),
error_code: None,
});
}
}
Err(e) => {
warn!("Download failed: {}", e);
warn!(error = %e, "MSD image download failed");
let code = match e {
AppError::Msd(error) => error.code(),
_ => MsdErrorCode::MsdOperationFailed,
};
if let Some(ref bus) = events {
bus.publish(crate::events::SystemEvent::MsdDownloadProgress {
download_id: download_id_clone,
@@ -637,7 +643,8 @@ impl MsdController {
bytes_downloaded: 0,
total_bytes: None,
progress_pct: None,
status: format!("failed: {}", e),
status: "failed".to_string(),
error_code: Some(code.as_str().to_string()),
});
}
}
@@ -655,10 +662,7 @@ impl MsdController {
info!("Download cancelled: {}", download_id);
Ok(())
} else {
Err(AppError::NotFound(format!(
"Download not found: {}",
download_id
)))
Err(MsdErrorCode::MsdResourceNotFound.into())
}
}
@@ -666,7 +670,13 @@ impl MsdController {
self.otg_service
.gadget_path()
.await
.ok_or_else(|| AppError::Internal("OTG gadget path is not available".to_string()))
.ok_or_else(|| AppError::from(MsdErrorCode::MsdUnavailable))
}
fn try_operation(&self) -> Result<tokio::sync::RwLockWriteGuard<'_, ()>> {
self.operation_lock
.try_write()
.map_err(|_| MsdErrorCode::MsdOperationInProgress.into())
}
pub async fn shutdown(&self) -> Result<()> {
@@ -712,6 +722,18 @@ mod tests {
assert!(controller.drive_path.ends_with("ventoy.img"));
}
#[tokio::test]
async fn concurrent_operations_have_a_stable_error_code() {
let temp_dir = TempDir::new().unwrap();
let controller = MsdController::new(Arc::new(OtgService::new()), temp_dir.path());
let _guard = controller.operation_lock.write().await;
assert!(matches!(
controller.try_operation().unwrap_err(),
AppError::Msd(error) if error.code() == MsdErrorCode::MsdOperationInProgress
));
}
#[tokio::test]
async fn test_state_default() {
let temp_dir = TempDir::new().unwrap();
@@ -783,14 +805,14 @@ mod tests {
.push(MountedMedia::image(3, &image, false, true));
assert_eq!(MsdController::select_lun(&state, Some(5)).unwrap(), 5);
assert!(MsdController::select_lun(&state, Some(3))
.unwrap_err()
.to_string()
.contains("already occupied"));
assert!(MsdController::select_lun(&state, Some(8))
.unwrap_err()
.to_string()
.contains("outside"));
assert!(matches!(
MsdController::select_lun(&state, Some(3)).unwrap_err(),
AppError::Msd(error) if error.code() == MsdErrorCode::MsdMediaSlotsFull
));
assert!(matches!(
MsdController::select_lun(&state, Some(8)).unwrap_err(),
AppError::Msd(error) if error.code() == MsdErrorCode::MsdInvalidRequest
));
}
#[test]

View File

@@ -6,10 +6,10 @@ use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use time::OffsetDateTime;
use tokio::io::AsyncWriteExt;
use tracing::info;
use tracing::{info, warn};
use super::types::ImageInfo;
use crate::error::{AppError, Result};
use crate::error::{AppError, MsdErrorCode, Result};
const MAX_IMAGE_SIZE: u64 = 32 * 1024 * 1024 * 1024;
@@ -28,7 +28,7 @@ impl ImageManager {
pub fn ensure_dir(&self) -> Result<()> {
fs::create_dir_all(&self.images_path)
.map_err(|e| AppError::Internal(format!("Failed to create images directory: {}", e)))?;
.map_err(|error| storage_io_error("create images directory", error))?;
Ok(())
}
@@ -38,11 +38,9 @@ impl ImageManager {
let mut images = Vec::new();
for entry in fs::read_dir(&self.images_path)
.map_err(|e| AppError::Internal(format!("Failed to read images directory: {}", e)))?
.map_err(|error| storage_io_error("read images directory", error))?
{
let entry = entry.map_err(|e| {
AppError::Internal(format!("Failed to read directory entry: {}", e))
})?;
let entry = entry.map_err(|error| storage_io_error("read image entry", error))?;
let path = entry.path();
if path.is_file() {
@@ -88,13 +86,13 @@ impl ImageManager {
return Ok(image);
}
}
Err(AppError::NotFound(format!("Image not found: {}", id)))
Err(MsdErrorCode::MsdResourceNotFound.into())
}
pub fn get_by_name(&self, name: &str) -> Result<ImageInfo> {
let path = self.images_path.join(name);
self.get_image_info(&path)
.ok_or_else(|| AppError::NotFound(format!("Image not found: {}", name)))
.ok_or_else(|| MsdErrorCode::MsdResourceNotFound.into())
}
#[cfg(test)]
@@ -103,30 +101,24 @@ impl ImageManager {
let name = sanitize_filename(name);
if name.is_empty() {
return Err(AppError::Internal("Invalid filename".to_string()));
return Err(MsdErrorCode::MsdInvalidRequest.into());
}
if data.len() as u64 > MAX_IMAGE_SIZE {
return Err(AppError::Internal(format!(
"Image too large. Maximum size: {} GB",
MAX_IMAGE_SIZE / 1024 / 1024 / 1024
)));
return Err(MsdErrorCode::MsdImageTooLarge.into());
}
let path = self.images_path.join(&name);
if path.exists() {
return Err(AppError::Internal(format!(
"Image already exists: {}",
name
)));
return Err(MsdErrorCode::MsdResourceAlreadyExists.into());
}
let mut file = fs::File::create(&path)
.map_err(|e| AppError::Internal(format!("Failed to create image file: {}", e)))?;
let mut file =
fs::File::create(&path).map_err(|error| storage_io_error("create image", error))?;
file.write_all(data).map_err(|e| {
file.write_all(data).map_err(|error| {
let _ = fs::remove_file(&path);
AppError::Internal(format!("Failed to write image data: {}", e))
storage_io_error("write image", error)
})?;
info!("Created image: {} ({} bytes)", name, data.len());
@@ -143,7 +135,7 @@ impl ImageManager {
let name = sanitize_filename(name);
if name.is_empty() {
return Err(AppError::Internal("Invalid filename".to_string()));
return Err(MsdErrorCode::MsdInvalidRequest.into());
}
let temp_name = format!(".upload_{}", uuid::Uuid::new_v4());
@@ -151,48 +143,41 @@ impl ImageManager {
let final_path = self.images_path.join(&name);
if final_path.exists() {
return Err(AppError::Internal(format!(
"Image already exists: {}",
name
)));
return Err(MsdErrorCode::MsdResourceAlreadyExists.into());
}
let mut file = tokio::fs::File::create(&temp_path)
.await
.map_err(|e| AppError::Internal(format!("Failed to create temp file: {}", e)))?;
.map_err(|error| storage_io_error("create image upload", error))?;
let mut bytes_written: u64 = 0;
while let Some(chunk) = field
.chunk()
.await
.map_err(|e| AppError::Internal(format!("Failed to read upload chunk: {}", e)))?
{
while let Some(chunk) = field.chunk().await.map_err(|error| {
warn!(%error, "Failed to read MSD image upload chunk");
AppError::from(MsdErrorCode::MsdOperationFailed)
})? {
bytes_written += chunk.len() as u64;
if bytes_written > MAX_IMAGE_SIZE {
drop(file);
let _ = tokio::fs::remove_file(&temp_path).await;
return Err(AppError::Internal(format!(
"Image too large. Maximum size: {} GB",
MAX_IMAGE_SIZE / 1024 / 1024 / 1024
)));
return Err(MsdErrorCode::MsdImageTooLarge.into());
}
file.write_all(&chunk)
.await
.map_err(|e| AppError::Internal(format!("Failed to write chunk: {}", e)))?;
.map_err(|error| storage_io_error("write image upload", error))?;
}
file.flush()
.await
.map_err(|e| AppError::Internal(format!("Failed to flush file: {}", e)))?;
.map_err(|error| storage_io_error("flush image upload", error))?;
drop(file);
tokio::fs::rename(&temp_path, &final_path)
.await
.map_err(|e| {
.map_err(|error| {
let _ = std::fs::remove_file(&temp_path);
AppError::Internal(format!("Failed to rename temp file: {}", e))
storage_io_error("commit image upload", error)
})?;
info!(
@@ -206,8 +191,7 @@ impl ImageManager {
pub fn delete(&self, id: &str) -> Result<()> {
let image = self.get(id)?;
fs::remove_file(&image.path)
.map_err(|e| AppError::Internal(format!("Failed to delete image: {}", e)))?;
fs::remove_file(&image.path).map_err(|error| storage_io_error("delete image", error))?;
info!("Deleted image: {}", image.name);
Ok(())
@@ -224,8 +208,11 @@ impl ImageManager {
{
self.ensure_dir()?;
let parsed_url = reqwest::Url::parse(url)
.map_err(|e| AppError::BadRequest(format!("Invalid URL: {}", e)))?;
let parsed_url =
reqwest::Url::parse(url).map_err(|_| AppError::from(MsdErrorCode::MsdInvalidUrl))?;
if !matches!(parsed_url.scheme(), "http" | "https") {
return Err(MsdErrorCode::MsdInvalidUrl.into());
}
info!("Starting download from: {}", url);
@@ -233,19 +220,17 @@ impl ImageManager {
.timeout(std::time::Duration::from_secs(3600))
.connect_timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| AppError::Internal(format!("Failed to create HTTP client: {}", e)))?;
.map_err(|error| remote_download_error("create HTTP client", error))?;
let head_response = client
.head(url)
.send()
.await
.map_err(|e| AppError::Internal(format!("Failed to connect: {}", e)))?;
.map_err(|error| remote_download_error("send HEAD request", error))?;
if !head_response.status().is_success() {
return Err(AppError::Internal(format!(
"Server returned error: {}",
head_response.status()
)));
warn!(status = %head_response.status(), "MSD image HEAD request failed");
return Err(MsdErrorCode::MsdRemoteDownloadFailed.into());
}
let total_size = head_response
@@ -256,11 +241,7 @@ impl ImageManager {
if let Some(size) = total_size {
if size > MAX_IMAGE_SIZE {
return Err(AppError::BadRequest(format!(
"File too large: {} bytes (max {} GB)",
size,
MAX_IMAGE_SIZE / 1024 / 1024 / 1024
)));
return Err(MsdErrorCode::MsdImageTooLarge.into());
}
}
@@ -284,17 +265,12 @@ impl ImageManager {
};
if final_filename.is_empty() {
return Err(AppError::BadRequest(
"Could not determine filename".to_string(),
));
return Err(MsdErrorCode::MsdInvalidRequest.into());
}
let final_path = self.images_path.join(&final_filename);
if final_path.exists() {
return Err(AppError::BadRequest(format!(
"Image already exists: {}",
final_filename
)));
return Err(MsdErrorCode::MsdResourceAlreadyExists.into());
}
let temp_filename = format!(".download_{}", uuid::Uuid::new_v4());
@@ -304,13 +280,11 @@ impl ImageManager {
.get(url)
.send()
.await
.map_err(|e| AppError::Internal(format!("Download failed: {}", e)))?;
.map_err(|error| remote_download_error("send GET request", error))?;
if !response.status().is_success() {
return Err(AppError::Internal(format!(
"Download failed: HTTP {}",
response.status()
)));
warn!(status = %response.status(), "MSD image GET request failed");
return Err(MsdErrorCode::MsdRemoteDownloadFailed.into());
}
let content_length = response
@@ -322,7 +296,7 @@ impl ImageManager {
let mut file = tokio::fs::File::create(&temp_path)
.await
.map_err(|e| AppError::Internal(format!("Failed to create temp file: {}", e)))?;
.map_err(|error| storage_io_error("create image download", error))?;
let mut stream = response.bytes_stream();
let mut downloaded: u64 = 0;
@@ -334,11 +308,11 @@ impl ImageManager {
while let Some(chunk_result) = stream.next().await {
let chunk =
chunk_result.map_err(|e| AppError::Internal(format!("Download error: {}", e)))?;
chunk_result.map_err(|error| remote_download_error("read response body", error))?;
file.write_all(&chunk).await.map_err(|e| {
file.write_all(&chunk).await.map_err(|error| {
let _ = std::fs::remove_file(&temp_path);
AppError::Internal(format!("Failed to write data: {}", e))
storage_io_error("write image download", error)
})?;
downloaded += chunk.len() as u64;
@@ -360,29 +334,29 @@ impl ImageManager {
file.flush()
.await
.map_err(|e| AppError::Internal(format!("Failed to flush file: {}", e)))?;
.map_err(|error| storage_io_error("flush image download", error))?;
drop(file);
let metadata = tokio::fs::metadata(&temp_path)
.await
.map_err(|e| AppError::Internal(format!("Failed to read file metadata: {}", e)))?;
.map_err(|error| storage_io_error("read downloaded image metadata", error))?;
if let Some(expected) = content_length {
if metadata.len() != expected {
let _ = tokio::fs::remove_file(&temp_path).await;
return Err(AppError::Internal(format!(
"Download incomplete: got {} bytes, expected {}",
metadata.len(),
expected
)));
warn!(
actual = metadata.len(),
expected, "MSD image download was incomplete"
);
return Err(MsdErrorCode::MsdDownloadIncomplete.into());
}
}
tokio::fs::rename(&temp_path, &final_path)
.await
.map_err(|e| {
.map_err(|error| {
let _ = std::fs::remove_file(&temp_path);
AppError::Internal(format!("Failed to move file: {}", e))
storage_io_error("commit image download", error)
})?;
info!(
@@ -395,6 +369,26 @@ impl ImageManager {
}
}
fn storage_io_error(operation: &'static str, error: std::io::Error) -> AppError {
warn!(operation, %error, "MSD storage operation failed");
#[cfg(unix)]
let code = match error.raw_os_error() {
Some(libc::EFBIG) => MsdErrorCode::MsdImageTooLarge,
Some(libc::ENOSPC) => MsdErrorCode::MsdStorageFull,
Some(libc::EROFS) => MsdErrorCode::MsdStorageReadOnly,
Some(libc::EACCES | libc::EPERM) => MsdErrorCode::MsdStoragePermissionDenied,
_ => MsdErrorCode::MsdOperationFailed,
};
#[cfg(not(unix))]
let code = MsdErrorCode::MsdOperationFailed;
code.into()
}
fn remote_download_error(operation: &'static str, error: reqwest::Error) -> AppError {
warn!(operation, %error, "MSD remote download failed");
MsdErrorCode::MsdRemoteDownloadFailed.into()
}
fn stable_image_id_from_filename(name: &str) -> String {
let mut hash: u64 = 0;
for (i, byte) in name.bytes().enumerate() {
@@ -490,4 +484,18 @@ mod tests {
assert!(manager.list().unwrap().is_empty());
}
#[test]
fn classifies_storage_io_errors() {
for (errno, expected) in [
(libc::EFBIG, MsdErrorCode::MsdImageTooLarge),
(libc::ENOSPC, MsdErrorCode::MsdStorageFull),
(libc::EROFS, MsdErrorCode::MsdStorageReadOnly),
(libc::EACCES, MsdErrorCode::MsdStoragePermissionDenied),
(libc::EPERM, MsdErrorCode::MsdStoragePermissionDenied),
] {
let error = storage_io_error("test", std::io::Error::from_raw_os_error(errno));
assert!(matches!(error, AppError::Msd(error) if error.code() == expected));
}
}
}

View File

@@ -14,4 +14,5 @@ pub use types::{
};
pub use ventoy_drive::{VentoyDrive, MIN_DRIVE_SIZE_MB};
pub use crate::error::{MsdError, MsdErrorCode};
pub use crate::otg::{MsdFunction, MsdLunConfig};

View File

@@ -235,7 +235,7 @@ pub struct DownloadProgress {
pub total_bytes: Option<u64>,
pub progress_pct: Option<f32>,
pub status: DownloadStatus,
pub error: Option<String>,
pub error_code: Option<String>,
}
#[cfg(test)]

View File

@@ -1,12 +1,12 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::info;
use tracing::{info, warn};
use ventoy_img::{FileInfo as VentoyFileInfo, VentoyError, VentoyImage};
use super::types::{DriveFile, DriveInfo};
use crate::error::{AppError, Result};
use crate::error::{AppError, MsdErrorCode, Result};
const STREAM_CHUNK_SIZE: usize = 64 * 1024;
@@ -44,10 +44,7 @@ impl VentoyDrive {
pub async fn init(&self, size_mb: u32) -> Result<DriveInfo> {
if size_mb < MIN_DRIVE_SIZE_MB {
return Err(AppError::BadRequest(format!(
"Drive size must be at least {} MB",
MIN_DRIVE_SIZE_MB
)));
return Err(MsdErrorCode::MsdDriveSizeInvalid.into());
}
let size_str = format!("{}M", size_mb);
let path = self.path.clone();
@@ -59,7 +56,7 @@ impl VentoyDrive {
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)))?;
.map_err(|error| drive_io_error("read initialized drive metadata", error))?;
Ok::<DriveInfo, AppError>(DriveInfo {
size: metadata.len(),
@@ -70,7 +67,7 @@ impl VentoyDrive {
})
})
.await
.map_err(|e| AppError::Internal(format!("Task join error: {}", e)))??;
.map_err(|error| task_error("initialize virtual drive", error))??;
info!("Ventoy drive created successfully");
Ok(info)
@@ -78,7 +75,7 @@ impl VentoyDrive {
pub async fn info(&self) -> Result<DriveInfo> {
if !self.exists() {
return Err(AppError::Internal("Drive not initialized".to_string()));
return Err(MsdErrorCode::MsdDriveNotInitialized.into());
}
let path = self.path.clone();
@@ -86,7 +83,7 @@ impl VentoyDrive {
tokio::task::spawn_blocking(move || {
let metadata = std::fs::metadata(&path)
.map_err(|e| AppError::Internal(format!("Failed to read drive metadata: {}", e)))?;
.map_err(|error| drive_io_error("read drive metadata", error))?;
let image = VentoyImage::open(&path).map_err(ventoy_to_app_error)?;
@@ -110,12 +107,12 @@ impl VentoyDrive {
})
})
.await
.map_err(|e| AppError::Internal(format!("Task join error: {}", e)))?
.map_err(|error| task_error("read virtual drive info", error))?
}
pub async fn list_files(&self, dir_path: &str) -> Result<Vec<DriveFile>> {
if !self.exists() {
return Err(AppError::Internal("Drive not initialized".to_string()));
return Err(MsdErrorCode::MsdDriveNotInitialized.into());
}
let path = self.path.clone();
@@ -138,7 +135,7 @@ impl VentoyDrive {
.collect())
})
.await
.map_err(|e| AppError::Internal(format!("Task join error: {}", e)))?
.map_err(|error| task_error("list virtual drive files", error))?
}
pub async fn write_file_from_multipart_field(
@@ -147,7 +144,7 @@ impl VentoyDrive {
mut field: axum::extract::multipart::Field<'_>,
) -> Result<u64> {
if !self.exists() {
return Err(AppError::Internal("Drive not initialized".to_string()));
return Err(MsdErrorCode::MsdDriveNotInitialized.into());
}
let temp_dir = self.path.parent().unwrap_or(Path::new("/tmp"));
@@ -156,24 +153,23 @@ impl VentoyDrive {
let mut temp_file = tokio::fs::File::create(&temp_path)
.await
.map_err(|e| AppError::Internal(format!("Failed to create temp file: {}", e)))?;
.map_err(|error| drive_io_error("create virtual drive upload", error))?;
let mut bytes_written: u64 = 0;
while let Some(chunk) = field
.chunk()
.await
.map_err(|e| AppError::Internal(format!("Failed to read upload chunk: {}", e)))?
{
while let Some(chunk) = field.chunk().await.map_err(|error| {
warn!(%error, "Failed to read virtual drive upload chunk");
AppError::from(MsdErrorCode::MsdOperationFailed)
})? {
bytes_written += chunk.len() as u64;
tokio::io::AsyncWriteExt::write_all(&mut temp_file, &chunk)
.await
.map_err(|e| AppError::Internal(format!("Failed to write chunk: {}", e)))?;
.map_err(|error| drive_io_error("write virtual drive upload", error))?;
}
tokio::io::AsyncWriteExt::flush(&mut temp_file)
.await
.map_err(|e| AppError::Internal(format!("Failed to flush temp file: {}", e)))?;
.map_err(|error| drive_io_error("flush virtual drive upload", error))?;
drop(temp_file);
let path = self.path.clone();
@@ -191,7 +187,7 @@ impl VentoyDrive {
Ok::<(), AppError>(())
})
.await
.map_err(|e| AppError::Internal(format!("Task join error: {}", e)))?;
.map_err(|error| task_error("write virtual drive file", error))?;
let _ = tokio::fs::remove_file(&temp_path).await;
@@ -202,7 +198,7 @@ impl VentoyDrive {
#[cfg(test)]
pub async fn read_file(&self, file_path: &str) -> Result<Vec<u8>> {
if !self.exists() {
return Err(AppError::Internal("Drive not initialized".to_string()));
return Err(MsdErrorCode::MsdDriveNotInitialized.into());
}
let path = self.path.clone();
@@ -215,12 +211,12 @@ impl VentoyDrive {
image.read_file(&file_path).map_err(ventoy_to_app_error)
})
.await
.map_err(|e| AppError::Internal(format!("Task join error: {}", e)))?
.map_err(|error| task_error("read virtual drive file", error))?
}
pub async fn get_file_info(&self, file_path: &str) -> Result<Option<DriveFile>> {
if !self.exists() {
return Err(AppError::Internal("Drive not initialized".to_string()));
return Err(MsdErrorCode::MsdDriveNotInitialized.into());
}
let path = self.path.clone();
@@ -234,7 +230,7 @@ impl VentoyDrive {
.map_err(ventoy_to_app_error)
})
.await
.map_err(|e| AppError::Internal(format!("Task join error: {}", e)))??;
.map_err(|error| task_error("read virtual drive file information", error))??;
Ok(info.map(|f| DriveFile {
name: f.name,
@@ -253,19 +249,16 @@ impl VentoyDrive {
tokio::sync::mpsc::Receiver<std::result::Result<bytes::Bytes, std::io::Error>>,
)> {
if !self.exists() {
return Err(AppError::Internal("Drive not initialized".to_string()));
return Err(MsdErrorCode::MsdDriveNotInitialized.into());
}
let file_info = self
.get_file_info(file_path)
.await?
.ok_or_else(|| AppError::NotFound(format!("File not found: {}", file_path)))?;
.ok_or_else(|| AppError::from(MsdErrorCode::MsdResourceNotFound))?;
if file_info.is_dir {
return Err(AppError::BadRequest(format!(
"'{}' is a directory",
file_path
)));
return Err(MsdErrorCode::MsdInvalidRequest.into());
}
let file_size = file_info.size;
@@ -300,7 +293,7 @@ impl VentoyDrive {
pub async fn mkdir(&self, dir_path: &str) -> Result<()> {
if !self.exists() {
return Err(AppError::Internal("Drive not initialized".to_string()));
return Err(MsdErrorCode::MsdDriveNotInitialized.into());
}
let path = self.path.clone();
@@ -315,12 +308,12 @@ impl VentoyDrive {
.map_err(ventoy_to_app_error)
})
.await
.map_err(|e| AppError::Internal(format!("Task join error: {}", e)))?
.map_err(|error| task_error("create virtual drive directory", error))?
}
pub async fn delete(&self, path_to_delete: &str) -> Result<()> {
if !self.exists() {
return Err(AppError::Internal("Drive not initialized".to_string()));
return Err(MsdErrorCode::MsdDriveNotInitialized.into());
}
let path = self.path.clone();
@@ -335,22 +328,23 @@ impl VentoyDrive {
.map_err(ventoy_to_app_error)
})
.await
.map_err(|e| AppError::Internal(format!("Task join error: {}", e)))?
.map_err(|error| task_error("delete virtual drive resource", error))?
}
}
fn ventoy_to_app_error(err: VentoyError) -> AppError {
warn!(%err, "Virtual drive filesystem operation failed");
match err {
VentoyError::Io(e) => AppError::Io(e),
VentoyError::InvalidSize(s) => AppError::BadRequest(format!("Invalid size: {}", s)),
VentoyError::SizeParseError(s) => AppError::BadRequest(format!("Size parse error: {}", s)),
VentoyError::FilesystemError(s) => AppError::Internal(format!("Filesystem error: {}", s)),
VentoyError::ImageError(s) => AppError::Internal(format!("Image error: {}", s)),
VentoyError::FileNotFound(s) => AppError::NotFound(format!("File not found: {}", s)),
VentoyError::ResourceNotFound(s) => {
AppError::Internal(format!("Resource not found: {}", s))
VentoyError::Io(error) => drive_io_error("access virtual drive", error),
VentoyError::InvalidSize(_) | VentoyError::SizeParseError(_) => {
MsdErrorCode::MsdDriveSizeInvalid.into()
}
VentoyError::FilesystemError(_)
| VentoyError::ImageError(_)
| VentoyError::PartitionError(_) => MsdErrorCode::MsdDriveFilesystemUnsupported.into(),
VentoyError::FileNotFound(_) | VentoyError::ResourceNotFound(_) => {
MsdErrorCode::MsdResourceNotFound.into()
}
VentoyError::PartitionError(s) => AppError::Internal(format!("Partition error: {}", s)),
}
}
@@ -361,21 +355,35 @@ fn drive_init_error(err: VentoyError) -> AppError {
#[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),
Some(libc::EFBIG) => MsdErrorCode::MsdDriveSizeInvalid.into(),
Some(libc::ENOSPC) => MsdErrorCode::MsdStorageFull.into(),
Some(libc::EROFS) => MsdErrorCode::MsdStorageReadOnly.into(),
Some(libc::EACCES | libc::EPERM) => MsdErrorCode::MsdStoragePermissionDenied.into(),
_ => drive_io_error("initialize virtual drive", error),
}
#[cfg(not(unix))]
AppError::Io(error)
drive_io_error("initialize virtual drive", error)
}
fn drive_io_error(operation: &'static str, error: std::io::Error) -> AppError {
warn!(operation, %error, "Virtual drive storage operation failed");
#[cfg(unix)]
let code = match error.raw_os_error() {
Some(libc::EFBIG) => MsdErrorCode::MsdImageTooLarge,
Some(libc::ENOSPC) => MsdErrorCode::MsdStorageFull,
Some(libc::EROFS) => MsdErrorCode::MsdStorageReadOnly,
Some(libc::EACCES | libc::EPERM) => MsdErrorCode::MsdStoragePermissionDenied,
_ => MsdErrorCode::MsdOperationFailed,
};
#[cfg(not(unix))]
let code = MsdErrorCode::MsdOperationFailed;
code.into()
}
fn task_error(operation: &'static str, error: tokio::task::JoinError) -> AppError {
warn!(operation, %error, "Virtual drive task failed");
MsdErrorCode::MsdOperationFailed.into()
}
fn ventoy_file_to_drive_file(info: VentoyFileInfo, parent_path: &str) -> DriveFile {
@@ -470,16 +478,35 @@ mod tests {
#[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"),
(libc::EFBIG, MsdErrorCode::MsdDriveSizeInvalid),
(libc::ENOSPC, MsdErrorCode::MsdStorageFull),
(libc::EROFS, MsdErrorCode::MsdStorageReadOnly),
(libc::EACCES, MsdErrorCode::MsdStoragePermissionDenied),
(libc::EPERM, MsdErrorCode::MsdStoragePermissionDenied),
] {
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)));
assert!(matches!(error, AppError::Msd(error) if error.code() == expected));
}
}
#[test]
fn classifies_ventoy_filesystem_and_resource_errors() {
for error in [
VentoyError::FilesystemError("details".into()),
VentoyError::ImageError("details".into()),
VentoyError::PartitionError("details".into()),
] {
assert!(matches!(
ventoy_to_app_error(error),
AppError::Msd(error) if error.code() == MsdErrorCode::MsdDriveFilesystemUnsupported
));
}
assert!(matches!(
ventoy_to_app_error(VentoyError::FileNotFound("details".into())),
AppError::Msd(error) if error.code() == MsdErrorCode::MsdResourceNotFound
));
}
fn init_ventoy_resources() -> bool {
static INIT: OnceLock<bool> = OnceLock::new();
*INIT.get_or_init(|| {