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 readable = std::fs::File::open(&path).is_ok();
102
103 let info = FileInfo {
104 path: path.display().to_string(),
105 size,
106 extension,
107 format: Some(format),
108 readable,
109 writable: !metadata.permissions().readonly(),
110 };
111
112 Ok(Self { path, info })
113 }
114
115 fn detect_format(path: &Path, extension: Option<&str>) -> Result<FileFormat> {
117 if let Some(ext) = extension {
119 match ext.to_lowercase().as_str() {
120 "tif" | "tiff" | "gtiff" => return Ok(FileFormat::GeoTiff),
121 "json" | "geojson" => return Ok(FileFormat::GeoJson),
122 "shp" => return Ok(FileFormat::Shapefile),
123 "zarr" => return Ok(FileFormat::Zarr),
124 "nc" | "nc4" => return Ok(FileFormat::NetCdf),
125 "h5" | "hdf5" => return Ok(FileFormat::Hdf5),
126 "parquet" | "geoparquet" => return Ok(FileFormat::GeoParquet),
127 "fgb" => return Ok(FileFormat::FlatGeobuf),
128 _ => {}
129 }
130 }
131
132 if let Ok(mut file) = std::fs::File::open(path) {
134 use std::io::Read;
135 let mut magic = [0u8; 8];
136 if file.read_exact(&mut magic).is_ok() {
137 if magic[0..2] == [0x49, 0x49] || magic[0..2] == [0x4D, 0x4D] {
139 return Ok(FileFormat::GeoTiff);
140 }
141 if magic[0] == b'{' {
143 return Ok(FileFormat::GeoJson);
144 }
145 if magic[0..4] == [0x89, 0x48, 0x44, 0x46] {
147 return Ok(FileFormat::Hdf5);
148 }
149 }
150 }
151
152 Ok(FileFormat::Unknown)
153 }
154
155 pub fn info(&self) -> &FileInfo {
157 &self.info
158 }
159
160 pub fn path(&self) -> &Path {
162 &self.path
163 }
164
165 pub fn summary(&self) -> String {
167 let mut report = String::new();
168 report.push_str(&format!("\n{}\n", "File Inspection".bold()));
169 report.push_str(&format!("{}\n\n", "=".repeat(60)));
170
171 let mut table = Table::new();
172 table.add_row(Row::from(vec![
173 Cell::new("Path"),
174 Cell::new(&self.info.path),
175 ]));
176 table.add_row(Row::from(vec![
177 Cell::new("Size"),
178 Cell::new(format_size(self.info.size)),
179 ]));
180 if let Some(ref ext) = self.info.extension {
181 table.add_row(Row::from(vec![Cell::new("Extension"), Cell::new(ext)]));
182 }
183 if let Some(format) = self.info.format {
184 table.add_row(Row::from(vec![
185 Cell::new("Format"),
186 Cell::new(format!("{:?}", format)),
187 ]));
188 table.add_row(Row::from(vec![
189 Cell::new("Description"),
190 Cell::new(format.description()),
191 ]));
192 }
193 table.add_row(Row::from(vec![
194 Cell::new("Readable"),
195 Cell::new(if self.info.readable { "Yes" } else { "No" }),
196 ]));
197 table.add_row(Row::from(vec![
198 Cell::new("Writable"),
199 Cell::new(if self.info.writable { "Yes" } else { "No" }),
200 ]));
201
202 report.push_str(&table.to_string());
203 report.push('\n');
204
205 report
206 }
207
208 pub fn export_json(&self) -> Result<String> {
210 Ok(serde_json::to_string_pretty(&self.info)?)
211 }
212}
213
214fn format_size(bytes: u64) -> String {
216 if bytes < 1024 {
217 format!("{} B", bytes)
218 } else if bytes < 1024 * 1024 {
219 format!("{:.2} KB", bytes as f64 / 1024.0)
220 } else if bytes < 1024 * 1024 * 1024 {
221 format!("{:.2} MB", bytes as f64 / (1024.0 * 1024.0))
222 } else {
223 format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use std::io::Write;
231 use tempfile::NamedTempFile;
232
233 #[test]
234 fn test_format_size() {
235 assert_eq!(format_size(512), "512 B");
236 assert_eq!(format_size(2048), "2.00 KB");
237 assert_eq!(format_size(2 * 1024 * 1024), "2.00 MB");
238 }
239
240 #[test]
241 fn test_file_inspector_creation() -> Result<()> {
242 let mut temp_file = NamedTempFile::new()?;
243 temp_file.write_all(b"test data")?;
244
245 let inspector = FileInspector::new(temp_file.path())?;
246 assert_eq!(inspector.info().size, 9);
247
248 Ok(())
249 }
250
251 #[test]
252 fn test_file_inspector_nonexistent() {
253 let result = FileInspector::new("/nonexistent/file.tif");
254 assert!(result.is_err());
255 }
256
257 #[test]
258 fn test_format_detection_by_extension() -> Result<()> {
259 let mut temp_file = NamedTempFile::with_suffix(".tif")?;
260 temp_file.write_all(b"II\x2a\x00")?; let inspector = FileInspector::new(temp_file.path())?;
263 assert_eq!(inspector.info().format, Some(FileFormat::GeoTiff));
264
265 Ok(())
266 }
267
268 #[test]
269 fn test_readable_reports_true_for_writable_file() -> Result<()> {
270 let mut temp_file = NamedTempFile::new()?;
276 temp_file.write_all(b"test data")?;
277
278 let inspector = FileInspector::new(temp_file.path())?;
279 assert!(
280 inspector.info().readable,
281 "a readable temp file must report readable: true"
282 );
283 assert!(
284 inspector.info().writable,
285 "a writable temp file must report writable: true"
286 );
287
288 Ok(())
289 }
290
291 #[test]
292 fn test_file_info_export() -> Result<()> {
293 let mut temp_file = NamedTempFile::new()?;
294 temp_file.write_all(b"test")?;
295
296 let inspector = FileInspector::new(temp_file.path())?;
297 let json = inspector.export_json()?;
298 assert!(json.contains("path"));
299 assert!(json.contains("size"));
300
301 Ok(())
302 }
303}