use crate::{DevToolsError, Result};
use colored::Colorize;
use comfy_table::{Cell, Row, Table};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct FileInspector {
path: PathBuf,
info: FileInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileInfo {
pub path: String,
pub size: u64,
pub extension: Option<String>,
pub format: Option<FileFormat>,
pub readable: bool,
pub writable: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FileFormat {
GeoTiff,
GeoJson,
Shapefile,
Zarr,
NetCdf,
Hdf5,
GeoParquet,
FlatGeobuf,
Unknown,
}
impl FileFormat {
pub fn description(&self) -> &str {
match self {
Self::GeoTiff => "GeoTIFF raster format",
Self::GeoJson => "GeoJSON vector format",
Self::Shapefile => "ESRI Shapefile",
Self::Zarr => "Zarr array storage",
Self::NetCdf => "NetCDF scientific data",
Self::Hdf5 => "HDF5 hierarchical data",
Self::GeoParquet => "GeoParquet columnar format",
Self::FlatGeobuf => "FlatGeobuf binary format",
Self::Unknown => "Unknown format",
}
}
}
impl FileInspector {
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref().to_path_buf();
if !path.exists() {
return Err(DevToolsError::Inspector(format!(
"File does not exist: {}",
path.display()
)));
}
let metadata = std::fs::metadata(&path)?;
let size = metadata.len();
let extension = path
.extension()
.and_then(|s| s.to_str())
.map(|s| s.to_string());
let format = Self::detect_format(&path, extension.as_deref())?;
let info = FileInfo {
path: path.display().to_string(),
size,
extension,
format: Some(format),
readable: metadata.permissions().readonly(),
writable: !metadata.permissions().readonly(),
};
Ok(Self { path, info })
}
fn detect_format(path: &Path, extension: Option<&str>) -> Result<FileFormat> {
if let Some(ext) = extension {
match ext.to_lowercase().as_str() {
"tif" | "tiff" | "gtiff" => return Ok(FileFormat::GeoTiff),
"json" | "geojson" => return Ok(FileFormat::GeoJson),
"shp" => return Ok(FileFormat::Shapefile),
"zarr" => return Ok(FileFormat::Zarr),
"nc" | "nc4" => return Ok(FileFormat::NetCdf),
"h5" | "hdf5" => return Ok(FileFormat::Hdf5),
"parquet" | "geoparquet" => return Ok(FileFormat::GeoParquet),
"fgb" => return Ok(FileFormat::FlatGeobuf),
_ => {}
}
}
if let Ok(mut file) = std::fs::File::open(path) {
use std::io::Read;
let mut magic = [0u8; 8];
if file.read_exact(&mut magic).is_ok() {
if magic[0..2] == [0x49, 0x49] || magic[0..2] == [0x4D, 0x4D] {
return Ok(FileFormat::GeoTiff);
}
if magic[0] == b'{' {
return Ok(FileFormat::GeoJson);
}
if magic[0..4] == [0x89, 0x48, 0x44, 0x46] {
return Ok(FileFormat::Hdf5);
}
}
}
Ok(FileFormat::Unknown)
}
pub fn info(&self) -> &FileInfo {
&self.info
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn summary(&self) -> String {
let mut report = String::new();
report.push_str(&format!("\n{}\n", "File Inspection".bold()));
report.push_str(&format!("{}\n\n", "=".repeat(60)));
let mut table = Table::new();
table.add_row(Row::from(vec![
Cell::new("Path"),
Cell::new(&self.info.path),
]));
table.add_row(Row::from(vec![
Cell::new("Size"),
Cell::new(format_size(self.info.size)),
]));
if let Some(ref ext) = self.info.extension {
table.add_row(Row::from(vec![Cell::new("Extension"), Cell::new(ext)]));
}
if let Some(format) = self.info.format {
table.add_row(Row::from(vec![
Cell::new("Format"),
Cell::new(format!("{:?}", format)),
]));
table.add_row(Row::from(vec![
Cell::new("Description"),
Cell::new(format.description()),
]));
}
table.add_row(Row::from(vec![
Cell::new("Readable"),
Cell::new(if self.info.readable { "Yes" } else { "No" }),
]));
table.add_row(Row::from(vec![
Cell::new("Writable"),
Cell::new(if self.info.writable { "Yes" } else { "No" }),
]));
report.push_str(&table.to_string());
report.push('\n');
report
}
pub fn export_json(&self) -> Result<String> {
Ok(serde_json::to_string_pretty(&self.info)?)
}
}
fn format_size(bytes: u64) -> String {
if bytes < 1024 {
format!("{} B", bytes)
} else if bytes < 1024 * 1024 {
format!("{:.2} KB", bytes as f64 / 1024.0)
} else if bytes < 1024 * 1024 * 1024 {
format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
} else {
format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_format_size() {
assert_eq!(format_size(512), "512 B");
assert_eq!(format_size(2048), "2.00 KB");
assert_eq!(format_size(2 * 1024 * 1024), "2.00 MB");
}
#[test]
fn test_file_inspector_creation() -> Result<()> {
let mut temp_file = NamedTempFile::new()?;
temp_file.write_all(b"test data")?;
let inspector = FileInspector::new(temp_file.path())?;
assert_eq!(inspector.info().size, 9);
Ok(())
}
#[test]
fn test_file_inspector_nonexistent() {
let result = FileInspector::new("/nonexistent/file.tif");
assert!(result.is_err());
}
#[test]
fn test_format_detection_by_extension() -> Result<()> {
let mut temp_file = NamedTempFile::with_suffix(".tif")?;
temp_file.write_all(b"II\x2a\x00")?;
let inspector = FileInspector::new(temp_file.path())?;
assert_eq!(inspector.info().format, Some(FileFormat::GeoTiff));
Ok(())
}
#[test]
fn test_file_info_export() -> Result<()> {
let mut temp_file = NamedTempFile::new()?;
temp_file.write_all(b"test")?;
let inspector = FileInspector::new(temp_file.path())?;
let json = inspector.export_json()?;
assert!(json.contains("path"));
assert!(json.contains("size"));
Ok(())
}
}