1use crate::{DevToolsError, Result};
6use colored::Colorize;
7use comfy_table::{Cell, Row, Table};
8use serde::{Deserialize, Serialize};
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone)]
13pub struct FileInspector {
14 path: PathBuf,
16 info: FileInfo,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct FileInfo {
23 pub path: String,
25 pub size: u64,
27 pub extension: Option<String>,
29 pub format: Option<FileFormat>,
31 pub readable: bool,
33 pub writable: bool,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39pub enum FileFormat {
40 GeoTiff,
42 GeoJson,
44 Shapefile,
46 Zarr,
48 NetCdf,
50 Hdf5,
52 GeoParquet,
54 FlatGeobuf,
56 Unknown,
58}
59
60impl FileFormat {
61 pub fn description(&self) -> &str {
63 match self {
64 Self::GeoTiff => "GeoTIFF raster format",
65 Self::GeoJson => "GeoJSON vector format",
66 Self::Shapefile => "ESRI Shapefile",
67 Self::Zarr => "Zarr array storage",
68 Self::NetCdf => "NetCDF scientific data",
69 Self::Hdf5 => "HDF5 hierarchical data",
70 Self::GeoParquet => "GeoParquet columnar format",
71 Self::FlatGeobuf => "FlatGeobuf binary format",
72 Self::Unknown => "Unknown format",
73 }
74 }
75}
76
77impl FileInspector {
78 pub fn new(path: impl AsRef<Path>) -> Result<Self> {
80 let path = path.as_ref().to_path_buf();
81
82 if !path.exists() {
83 return Err(DevToolsError::Inspector(format!(
84 "File does not exist: {}",
85 path.display()
86 )));
87 }
88
89 let metadata = std::fs::metadata(&path)?;
90 let size = metadata.len();
91 let extension = path
92 .extension()
93 .and_then(|s| s.to_str())
94 .map(|s| s.to_string());
95
96 let format = Self::detect_format(&path, extension.as_deref())?;
97
98 let info = FileInfo {
99 path: path.display().to_string(),
100 size,
101 extension,
102 format: Some(format),
103 readable: metadata.permissions().readonly(),
104 writable: !metadata.permissions().readonly(),
105 };
106
107 Ok(Self { path, info })
108 }
109
110 fn detect_format(path: &Path, extension: Option<&str>) -> Result<FileFormat> {
112 if let Some(ext) = extension {
114 match ext.to_lowercase().as_str() {
115 "tif" | "tiff" | "gtiff" => return Ok(FileFormat::GeoTiff),
116 "json" | "geojson" => return Ok(FileFormat::GeoJson),
117 "shp" => return Ok(FileFormat::Shapefile),
118 "zarr" => return Ok(FileFormat::Zarr),
119 "nc" | "nc4" => return Ok(FileFormat::NetCdf),
120 "h5" | "hdf5" => return Ok(FileFormat::Hdf5),
121 "parquet" | "geoparquet" => return Ok(FileFormat::GeoParquet),
122 "fgb" => return Ok(FileFormat::FlatGeobuf),
123 _ => {}
124 }
125 }
126
127 if let Ok(mut file) = std::fs::File::open(path) {
129 use std::io::Read;
130 let mut magic = [0u8; 8];
131 if file.read_exact(&mut magic).is_ok() {
132 if magic[0..2] == [0x49, 0x49] || magic[0..2] == [0x4D, 0x4D] {
134 return Ok(FileFormat::GeoTiff);
135 }
136 if magic[0] == b'{' {
138 return Ok(FileFormat::GeoJson);
139 }
140 if magic[0..4] == [0x89, 0x48, 0x44, 0x46] {
142 return Ok(FileFormat::Hdf5);
143 }
144 }
145 }
146
147 Ok(FileFormat::Unknown)
148 }
149
150 pub fn info(&self) -> &FileInfo {
152 &self.info
153 }
154
155 pub fn path(&self) -> &Path {
157 &self.path
158 }
159
160 pub fn summary(&self) -> String {
162 let mut report = String::new();
163 report.push_str(&format!("\n{}\n", "File Inspection".bold()));
164 report.push_str(&format!("{}\n\n", "=".repeat(60)));
165
166 let mut table = Table::new();
167 table.add_row(Row::from(vec![
168 Cell::new("Path"),
169 Cell::new(&self.info.path),
170 ]));
171 table.add_row(Row::from(vec![
172 Cell::new("Size"),
173 Cell::new(format_size(self.info.size)),
174 ]));
175 if let Some(ref ext) = self.info.extension {
176 table.add_row(Row::from(vec![Cell::new("Extension"), Cell::new(ext)]));
177 }
178 if let Some(format) = self.info.format {
179 table.add_row(Row::from(vec![
180 Cell::new("Format"),
181 Cell::new(format!("{:?}", format)),
182 ]));
183 table.add_row(Row::from(vec![
184 Cell::new("Description"),
185 Cell::new(format.description()),
186 ]));
187 }
188 table.add_row(Row::from(vec![
189 Cell::new("Readable"),
190 Cell::new(if self.info.readable { "Yes" } else { "No" }),
191 ]));
192 table.add_row(Row::from(vec![
193 Cell::new("Writable"),
194 Cell::new(if self.info.writable { "Yes" } else { "No" }),
195 ]));
196
197 report.push_str(&table.to_string());
198 report.push('\n');
199
200 report
201 }
202
203 pub fn export_json(&self) -> Result<String> {
205 Ok(serde_json::to_string_pretty(&self.info)?)
206 }
207}
208
209fn format_size(bytes: u64) -> String {
211 if bytes < 1024 {
212 format!("{} B", bytes)
213 } else if bytes < 1024 * 1024 {
214 format!("{:.2} KB", bytes as f64 / 1024.0)
215 } else if bytes < 1024 * 1024 * 1024 {
216 format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
217 } else {
218 format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use std::io::Write;
226 use tempfile::NamedTempFile;
227
228 #[test]
229 fn test_format_size() {
230 assert_eq!(format_size(512), "512 B");
231 assert_eq!(format_size(2048), "2.00 KB");
232 assert_eq!(format_size(2 * 1024 * 1024), "2.00 MB");
233 }
234
235 #[test]
236 fn test_file_inspector_creation() -> Result<()> {
237 let mut temp_file = NamedTempFile::new()?;
238 temp_file.write_all(b"test data")?;
239
240 let inspector = FileInspector::new(temp_file.path())?;
241 assert_eq!(inspector.info().size, 9);
242
243 Ok(())
244 }
245
246 #[test]
247 fn test_file_inspector_nonexistent() {
248 let result = FileInspector::new("/nonexistent/file.tif");
249 assert!(result.is_err());
250 }
251
252 #[test]
253 fn test_format_detection_by_extension() -> Result<()> {
254 let mut temp_file = NamedTempFile::with_suffix(".tif")?;
255 temp_file.write_all(b"II\x2a\x00")?; let inspector = FileInspector::new(temp_file.path())?;
258 assert_eq!(inspector.info().format, Some(FileFormat::GeoTiff));
259
260 Ok(())
261 }
262
263 #[test]
264 fn test_file_info_export() -> Result<()> {
265 let mut temp_file = NamedTempFile::new()?;
266 temp_file.write_all(b"test")?;
267
268 let inspector = FileInspector::new(temp_file.path())?;
269 let json = inspector.export_json()?;
270 assert!(json.contains("path"));
271 assert!(json.contains("size"));
272
273 Ok(())
274 }
275}