use crate::error::{QuantumLogError, Result};
use std::fs::{self, File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
pub struct FileTools;
impl FileTools {
pub fn ensure_directory_exists<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
if !path.exists() {
fs::create_dir_all(path).map_err(|e| QuantumLogError::IoError { source: e })?
} else if !path.is_dir() {
return Err(QuantumLogError::IoError {
source: std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("路径存在但不是目录: {}", path.display()),
),
});
}
Ok(())
}
pub fn create_file_safe<P: AsRef<Path>>(file_path: P, create_parents: bool) -> Result<File> {
let file_path = file_path.as_ref();
if create_parents {
if let Some(parent) = file_path.parent() {
Self::ensure_directory_exists(parent)?;
}
}
File::create(file_path).map_err(|e| QuantumLogError::IoError { source: e })
}
pub fn open_file_append<P: AsRef<Path>>(
file_path: P,
create_if_not_exists: bool,
) -> Result<File> {
let file_path = file_path.as_ref();
let mut options = OpenOptions::new();
options.append(true);
if create_if_not_exists {
options.create(true);
if let Some(parent) = file_path.parent() {
Self::ensure_directory_exists(parent)?;
}
}
options
.open(file_path)
.map_err(|e| QuantumLogError::IoError { source: e })
}
pub fn get_file_size<P: AsRef<Path>>(file_path: P) -> Result<u64> {
let file_path = file_path.as_ref();
let metadata =
fs::metadata(file_path).map_err(|e| QuantumLogError::IoError { source: e })?;
Ok(metadata.len())
}
pub fn is_file_writable<P: AsRef<Path>>(file_path: P) -> bool {
let file_path = file_path.as_ref();
if !file_path.exists() {
if let Some(parent) = file_path.parent() {
return Self::is_directory_writable(parent);
}
return false;
}
OpenOptions::new().append(true).open(file_path).is_ok()
}
pub fn is_directory_writable<P: AsRef<Path>>(dir_path: P) -> bool {
let dir_path = dir_path.as_ref();
if !dir_path.exists() || !dir_path.is_dir() {
return false;
}
let temp_file = dir_path.join(".quantum_log_write_test");
let result = File::create(&temp_file).is_ok();
if temp_file.exists() {
let _ = fs::remove_file(&temp_file);
}
result
}
pub fn file_exists<P: AsRef<Path>>(file_path: P) -> bool {
file_path.as_ref().exists()
}
pub fn remove_file_safe<P: AsRef<Path>>(file_path: P) -> Result<()> {
let file_path = file_path.as_ref();
if file_path.exists() {
fs::remove_file(file_path).map_err(|e| QuantumLogError::IoError { source: e })?;
}
Ok(())
}
pub fn safe_remove_file<P: AsRef<Path>>(file_path: P) -> Result<()> {
Self::remove_file_safe(file_path)
}
pub fn canonicalize_path<P: AsRef<Path>>(file_path: P) -> Result<PathBuf> {
let file_path = file_path.as_ref();
file_path
.canonicalize()
.map_err(|e| QuantumLogError::IoError {
source: std::io::Error::other(format!(
"规范化路径失败: {}: {}",
file_path.display(),
e
)),
})
}
pub fn create_buffered_writer(file: File, buffer_size: Option<usize>) -> BufWriter<File> {
match buffer_size {
Some(size) => BufWriter::with_capacity(size, file),
None => BufWriter::new(file),
}
}
pub fn write_to_file<P: AsRef<Path>>(file_path: P, data: &[u8], append: bool) -> Result<usize> {
let file_path = file_path.as_ref();
if let Some(parent) = file_path.parent() {
Self::ensure_directory_exists(parent)?;
}
let mut options = OpenOptions::new();
if append {
options.append(true).create(true);
} else {
options.write(true).create(true).truncate(true);
}
let mut file = options
.open(file_path)
.map_err(|e| QuantumLogError::IoError {
source: std::io::Error::new(
e.kind(),
format!("打开文件失败: {}: {}", file_path.display(), e),
),
})?;
file.write_all(data).map_err(|e| QuantumLogError::IoError {
source: std::io::Error::new(
e.kind(),
format!("写入文件失败: {}: {}", file_path.display(), e),
),
})?;
file.flush().map_err(|e| QuantumLogError::IoError {
source: std::io::Error::new(
e.kind(),
format!("刷新文件失败: {}: {}", file_path.display(), e),
),
})?;
Ok(data.len())
}
pub fn get_file_extension<P: AsRef<Path>>(file_path: P) -> Option<String> {
file_path
.as_ref()
.extension()
.and_then(|ext| ext.to_str())
.map(|s| s.to_lowercase())
}
pub fn generate_timestamped_filename(base_name: &str, extension: &str) -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
format!(
"{}_{}_{}.{}",
base_name,
timestamp,
std::process::id(),
extension
)
}
#[cfg(unix)]
pub fn has_sufficient_disk_space<P: AsRef<Path>>(path: P, required_bytes: u64) -> bool {
use std::os::unix::fs::MetadataExt;
if let Ok(metadata) = fs::metadata(path) {
true
} else {
false
}
}
#[cfg(windows)]
pub fn has_sufficient_disk_space<P: AsRef<Path>>(_path: P, _required_bytes: u64) -> bool {
true
}
#[cfg(not(any(unix, windows)))]
pub fn has_sufficient_disk_space<P: AsRef<Path>>(_path: P, _required_bytes: u64) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::fs;
#[test]
fn test_ensure_directory_exists() {
let temp_dir = env::temp_dir().join("quantum_log_test_dir");
let _ = fs::remove_dir_all(&temp_dir);
assert!(FileTools::ensure_directory_exists(&temp_dir).is_ok());
assert!(temp_dir.exists());
assert!(temp_dir.is_dir());
assert!(FileTools::ensure_directory_exists(&temp_dir).is_ok());
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_create_file_safe() {
let temp_dir = env::temp_dir().join("quantum_log_test_create");
let test_file = temp_dir.join("test.txt");
let _ = fs::remove_dir_all(&temp_dir);
let file = FileTools::create_file_safe(&test_file, true);
assert!(file.is_ok());
assert!(test_file.exists());
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_file_size() {
let temp_file = env::temp_dir().join("quantum_log_size_test.txt");
let test_data = b"Hello, QuantumLog!";
fs::write(&temp_file, test_data).unwrap();
let size = FileTools::get_file_size(&temp_file);
assert!(size.is_ok());
assert_eq!(size.unwrap(), test_data.len() as u64);
let _ = fs::remove_file(&temp_file);
}
#[test]
fn test_write_to_file() {
let temp_file = env::temp_dir().join("quantum_log_write_test.txt");
let test_data = b"Test data for QuantumLog";
let _ = fs::remove_file(&temp_file);
let result = FileTools::write_to_file(&temp_file, test_data, false);
assert!(result.is_ok());
assert_eq!(result.unwrap(), test_data.len());
let content = fs::read(&temp_file).unwrap();
assert_eq!(content, test_data);
let append_data = b" - Appended";
let result = FileTools::write_to_file(&temp_file, append_data, true);
assert!(result.is_ok());
let content = fs::read(&temp_file).unwrap();
let expected = [&test_data[..], &append_data[..]].concat();
assert_eq!(content, expected);
let _ = fs::remove_file(&temp_file);
}
#[test]
fn test_file_extension() {
assert_eq!(
FileTools::get_file_extension("test.txt"),
Some("txt".to_string())
);
assert_eq!(
FileTools::get_file_extension("test.LOG"),
Some("log".to_string())
);
assert_eq!(FileTools::get_file_extension("test"), None);
assert_eq!(FileTools::get_file_extension(".hidden"), None);
}
#[test]
fn test_timestamped_filename() {
let filename = FileTools::generate_timestamped_filename("quantum_log", "log");
assert!(filename.starts_with("quantum_log_"));
assert!(filename.ends_with(".log"));
assert!(filename.contains(&std::process::id().to_string()));
}
#[test]
fn test_is_file_writable() {
let temp_dir = env::temp_dir();
let test_file = temp_dir.join("quantum_log_writable_test.txt");
let _ = fs::remove_file(&test_file);
let writable = FileTools::is_file_writable(&test_file);
assert!(writable);
fs::write(&test_file, b"test").unwrap();
assert!(FileTools::is_file_writable(&test_file));
let _ = fs::remove_file(&test_file);
}
}