use std::fs::OpenOptions;
use std::fs::create_dir_all;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
use sha2::Digest;
use sha2::Sha256;
use tokio::fs;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::io::BufWriter;
use tracing::debug;
use tracing::error;
use crate::ConvertError;
use crate::FileError;
use crate::Result;
use crate::StorageError;
pub fn create_parent_dir_if_not_exist(path: &Path) -> Result<()> {
fn has_trailing_separator(path: &Path) -> bool {
let s = path.to_string_lossy();
!s.is_empty() && s.ends_with(std::path::MAIN_SEPARATOR)
}
let dir_to_create = if has_trailing_separator(path) {
path } else {
path.parent().unwrap_or(path) };
if !dir_to_create.exists() {
if let Err(e) = create_dir_all(dir_to_create) {
error!(?e, "create_parent_dir_if_not_exist failed.");
return Err(StorageError::PathError {
path: path.to_path_buf(),
source: e,
}
.into());
}
}
Ok(())
}
pub fn open_file_for_append(path: PathBuf) -> Result<std::fs::File> {
create_parent_dir_if_not_exist(&path)?;
let log_file = match OpenOptions::new().append(true).create(true).open(&path) {
Ok(f) => f,
Err(e) => {
return Err(StorageError::PathError { path, source: e }.into());
}
};
Ok(log_file)
}
#[allow(dead_code)]
pub async fn write_into_file(
path: PathBuf,
buf: Vec<u8>,
) {
if let Some(parent) = path.parent() {
if let Err(e) = tokio::fs::create_dir_all(parent).await {
error!("failed to crate dir with error({})", e);
} else {
debug!("created successfully: {:?}", path);
}
}
let file = tokio::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.await
.unwrap();
let mut active_file = BufWriter::new(file);
active_file.write_all(&buf).await.unwrap();
active_file.flush().await.unwrap();
}
#[allow(dead_code)]
pub async fn delete_file<P: AsRef<Path>>(path: P) -> Result<()> {
let path = path.as_ref();
let display_path = path.display().to_string();
let canonical_path = match path.canonicalize() {
Ok(p) => p,
Err(e) => return Err(map_canonicalize_error(e, &display_path).into()),
};
let metadata = match fs::metadata(&canonical_path).await {
Ok(m) => m,
Err(e) => return Err(map_metadata_error(e, &display_path).into()),
};
if metadata.is_dir() {
return Err(FileError::IsDirectory(display_path).into());
}
match fs::remove_file(&canonical_path).await {
Ok(_) => Ok(()),
Err(e) => Err(map_remove_error(e, &display_path).into()),
}
}
fn map_canonicalize_error(
error: std::io::Error,
path: &str,
) -> FileError {
match error.kind() {
std::io::ErrorKind::NotFound => FileError::NotFound(path.to_string()),
std::io::ErrorKind::PermissionDenied => FileError::PermissionDenied(path.to_string()),
_ => FileError::InvalidPath(format!("{path}: {error}")),
}
}
fn map_metadata_error(
error: std::io::Error,
path: &str,
) -> FileError {
match error.kind() {
std::io::ErrorKind::PermissionDenied => FileError::PermissionDenied(path.to_string()),
std::io::ErrorKind::NotFound => FileError::NotFound(path.to_string()),
_ => FileError::UnknownIo(format!("{path}: {error}")),
}
}
fn map_remove_error(
error: std::io::Error,
path: &str,
) -> FileError {
match error.kind() {
std::io::ErrorKind::PermissionDenied => FileError::PermissionDenied(path.to_string()),
std::io::ErrorKind::NotFound => FileError::NotFound(path.to_string()),
std::io::ErrorKind::Other if is_file_busy(&error) => FileError::FileBusy(path.to_string()),
_ => FileError::UnknownIo(format!("{path}: {error}")),
}
}
fn is_file_busy(error: &std::io::Error) -> bool {
#[cfg(windows)]
{
error.raw_os_error() == Some(32) }
#[cfg(unix)]
{
error.raw_os_error() == Some(16) }
#[cfg(not(any(windows, unix)))]
{
false
}
}
pub fn validate_checksum(
data: &[u8],
expected: &[u8],
) -> bool {
let mut hasher = crc32fast::Hasher::new();
hasher.update(data);
hasher.finalize().to_be_bytes() == expected
}
pub fn convert_vec_checksum(checksum: Vec<u8>) -> Result<[u8; 32]> {
match checksum.len() {
32 => checksum.try_into().map_err(|_| {
ConvertError::ConversionFailure("Conversion failed despite correct length".to_string())
.into()
}),
0 => Err(
ConvertError::ConversionFailure("Empty checksum vector provided".to_string()).into(),
),
len => Err(ConvertError::ConversionFailure(format!(
"Invalid checksum length: expected 32 bytes, got {len} bytes"
))
.into()),
}
}
#[allow(dead_code)]
pub async fn compute_checksum_from_file_path(file_path: &Path) -> Result<[u8; 32]> {
let mut file = tokio::fs::File::open(file_path).await.map_err(StorageError::IoError)?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 4096];
loop {
let bytes_read = file.read(&mut buffer).await.map_err(StorageError::IoError)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(hasher.finalize().into())
}
pub(crate) fn validate_compressed_format(path: &Path) -> Result<()> {
if !path.exists() {
return Err(FileError::NotFound(path.display().to_string()).into());
}
if let Ok(metadata) = std::fs::metadata(path) {
if metadata.len() < 10 {
return Err(FileError::TooSmall(metadata.len()).into());
}
}
let ext = path
.extension()
.and_then(|s| s.to_str())
.ok_or_else(|| FileError::InvalidExt("Invalid file extension".into()))?;
if !matches!(ext.to_lowercase().as_str(), "gz" | "tgz" | "snap") {
return Err(FileError::InvalidExt(format!("Invalid compression extension: {ext}")).into());
}
let mut file = std::fs::File::open(path).map_err(StorageError::IoError)?;
let mut header = [0u8; 2];
file.read_exact(&mut header).map_err(StorageError::IoError)?;
if header != [0x1f, 0x8b] {
return Err(FileError::InvalidGzipHeader("Invalid GZIP header".to_string()).into());
}
Ok(())
}
#[allow(dead_code)]
pub(super) async fn is_dir(path: &Path) -> Result<bool> {
let metadata = fs::metadata(path).await.map_err(StorageError::IoError)?;
Ok(metadata.is_dir())
}