// IO error // A wrapper around the tokio IO functions that adds the path to the error message, instead of the uninformative std::io::Error. use std::{io::Write, path::Path}; use tempfile::NamedTempFile; use tokio::task::spawn_blocking; #[derive(Debug, thiserror::Error)] pub enum IOError { #[error("{source}, path: {path}")] IOPathError { #[source] source: std::io::Error, path: String, }, #[error(transparent)] IOError(#[from] std::io::Error), } impl IOError { pub fn from(source: std::io::Error) -> Self { Self::IOError(source) } pub fn with_path( source: std::io::Error, path: impl AsRef, ) -> Self { let path = path.as_ref(); Self::IOPathError { source, path: path.to_string_lossy().to_string(), } } } pub fn canonicalize( path: impl AsRef, ) -> Result { let path = path.as_ref(); dunce::canonicalize(path).map_err(|e| IOError::IOPathError { source: e, path: path.to_string_lossy().to_string(), }) } pub async fn read_dir( path: impl AsRef, ) -> Result { let path = path.as_ref(); tokio::fs::read_dir(path) .await .map_err(|e| IOError::IOPathError { source: e, path: path.to_string_lossy().to_string(), }) } pub async fn create_dir( path: impl AsRef, ) -> Result<(), IOError> { let path = path.as_ref(); tokio::fs::create_dir(path) .await .map_err(|e| IOError::IOPathError { source: e, path: path.to_string_lossy().to_string(), }) } pub async fn create_dir_all( path: impl AsRef, ) -> Result<(), IOError> { let path = path.as_ref(); tokio::fs::create_dir_all(path) .await .map_err(|e| IOError::IOPathError { source: e, path: path.to_string_lossy().to_string(), }) } pub async fn remove_dir_all( path: impl AsRef, ) -> Result<(), IOError> { let path = path.as_ref(); tokio::fs::remove_dir_all(path) .await .map_err(|e| IOError::IOPathError { source: e, path: path.to_string_lossy().to_string(), }) } /// Reads a text file to a string, automatically detecting its encoding and /// substituting any invalid characters with the Unicode replacement character. /// /// This function is best suited for reading Minecraft instance files, whose /// encoding may vary depending on the platform, launchers, client versions /// (older Minecraft versions tended to rely on the system's default codepage /// more on Windows platforms), and mods used, while not being highly sensitive /// to occasional occurrences of mojibake or character replacements. pub async fn read_any_encoding_to_string( path: impl AsRef, ) -> Result<(String, &'static encoding_rs::Encoding), IOError> { let path = path.as_ref(); let file_bytes = tokio::fs::read(path) .await .map_err(|e| IOError::IOPathError { source: e, path: path.to_string_lossy().to_string(), })?; let file_encoding = { let mut encoding_detector = chardetng::EncodingDetector::new(); encoding_detector.feed(&file_bytes, true); encoding_detector.guess(None, true) }; let (file_string, actual_file_encoding, _) = file_encoding.decode(&file_bytes); Ok((file_string.to_string(), actual_file_encoding)) } pub async fn read( path: impl AsRef, ) -> Result, IOError> { let path = path.as_ref(); tokio::fs::read(path) .await .map_err(|e| IOError::IOPathError { source: e, path: path.to_string_lossy().to_string(), }) } pub async fn write( path: impl AsRef, data: impl AsRef<[u8]>, ) -> Result<(), IOError> { let path = path.as_ref().to_owned(); let data = data.as_ref().to_owned(); spawn_blocking(move || { let cloned_path = path.clone(); sync_write(data, path).map_err(|e| IOError::IOPathError { source: e, path: cloned_path.to_string_lossy().to_string(), }) }) .await .map_err(|_| std::io::Error::other("background task failed"))??; Ok(()) } fn sync_write( data: impl AsRef<[u8]>, path: impl AsRef, ) -> Result<(), std::io::Error> { let mut tempfile = NamedTempFile::new_in(path.as_ref().parent().ok_or_else(|| { std::io::Error::other( "could not get parent directory for temporary file", ) })?)?; tempfile.write_all(data.as_ref())?; let tmp_path = tempfile.into_temp_path(); let path = path.as_ref(); tmp_path.persist(path)?; std::io::Result::Ok(()) } pub fn is_same_disk(old_dir: &Path, new_dir: &Path) -> Result { #[cfg(unix)] { use std::os::unix::fs::MetadataExt; Ok(old_dir.metadata()?.dev() == new_dir.metadata()?.dev()) } #[cfg(windows)] { let old_dir = canonicalize(old_dir)?; let new_dir = canonicalize(new_dir)?; let old_component = old_dir.components().next(); let new_component = new_dir.components().next(); match (old_component, new_component) { ( Some(std::path::Component::Prefix(old)), Some(std::path::Component::Prefix(new)), ) => Ok(old.as_os_str() == new.as_os_str()), _ => Ok(false), } } } pub async fn rename_or_move( from: impl AsRef, to: impl AsRef, ) -> Result<(), IOError> { let from = from.as_ref(); let to = to.as_ref(); if to .parent() .map_or(Ok(false), |to_dir| is_same_disk(from, to_dir))? { tokio::fs::rename(from, to) .await .map_err(|e| IOError::IOPathError { source: e, path: from.to_string_lossy().to_string(), }) } else { move_recursive(from, to).await } } #[async_recursion::async_recursion] async fn move_recursive(from: &Path, to: &Path) -> Result<(), IOError> { if from.is_file() { copy(from, to).await?; remove_file(from).await?; return Ok(()); } create_dir(to).await?; let mut dir = read_dir(from).await?; while let Some(entry) = dir.next_entry().await? { let new_path = to.join(entry.file_name()); move_recursive(&entry.path(), &new_path).await?; } Ok(()) } pub async fn copy( from: impl AsRef, to: impl AsRef, ) -> Result { let from: &Path = from.as_ref(); let to = to.as_ref(); tokio::fs::copy(from, to) .await .map_err(|e| IOError::IOPathError { source: e, path: from.to_string_lossy().to_string(), }) } pub async fn remove_file( path: impl AsRef, ) -> Result<(), IOError> { let path = path.as_ref(); tokio::fs::remove_file(path) .await .map_err(|e| IOError::IOPathError { source: e, path: path.to_string_lossy().to_string(), }) } pub async fn open_file( path: impl AsRef, ) -> Result { let path = path.as_ref(); tokio::fs::File::open(path) .await .map_err(|e| IOError::IOPathError { source: e, path: path.to_string_lossy().to_string(), }) } pub async fn remove_dir( path: impl AsRef, ) -> Result<(), IOError> { let path = path.as_ref(); tokio::fs::remove_dir(path) .await .map_err(|e| IOError::IOPathError { source: e, path: path.to_string_lossy().to_string(), }) } pub async fn metadata( path: impl AsRef, ) -> Result { let path = path.as_ref(); tokio::fs::metadata(path) .await .map_err(|e| IOError::IOPathError { source: e, path: path.to_string_lossy().to_string(), }) } /// Gets a resource file from the executable. Returns `theseus::Result<(TempDir, PathBuf)>`. #[macro_export] macro_rules! get_resource_file { (directory: $relative_dir:expr, file: $file_name:expr) => { 'get_resource_file: { let dir = match tempfile::tempdir() { Ok(dir) => dir, Err(e) => { break 'get_resource_file $crate::Result::Err( $crate::util::io::IOError::from(e).into(), ); } }; let path = dir.path().join($file_name); if let Err(e) = $crate::util::io::write( &path, include_bytes!(concat!($relative_dir, "/", $file_name)), ) .await { break 'get_resource_file $crate::Result::Err(e.into()); } let path = match $crate::util::io::canonicalize(path) { Ok(path) => path, Err(e) => { break 'get_resource_file $crate::Result::Err(e.into()); } }; $crate::Result::Ok((dir, path)) } }; ($relative_dir:literal / $file_name:literal) => { get_resource_file!(directory: $relative_dir, file: $file_name) }; (env $dir_env_name:literal / $file_name:literal) => { get_resource_file!(directory: env!($dir_env_name), file: $file_name) }; }