Skip to main content

oxigdal_dev_tools/
inspector.rs

1//! File inspection utilities
2//!
3//! This module provides tools for inspecting and analyzing geospatial file formats.
4
5use crate::{DevToolsError, Result};
6use colored::Colorize;
7use comfy_table::{Cell, Row, Table};
8use serde::{Deserialize, Serialize};
9use std::path::{Path, PathBuf};
10
11/// File inspector
12#[derive(Debug, Clone)]
13pub struct FileInspector {
14    /// File path
15    path: PathBuf,
16    /// File information
17    info: FileInfo,
18}
19
20/// File information
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct FileInfo {
23    /// File path
24    pub path: String,
25    /// File size in bytes
26    pub size: u64,
27    /// File extension
28    pub extension: Option<String>,
29    /// Detected format
30    pub format: Option<FileFormat>,
31    /// Is readable
32    pub readable: bool,
33    /// Is writable
34    pub writable: bool,
35}
36
37/// File format
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39pub enum FileFormat {
40    /// GeoTIFF
41    GeoTiff,
42    /// GeoJSON
43    GeoJson,
44    /// Shapefile
45    Shapefile,
46    /// Zarr
47    Zarr,
48    /// NetCDF
49    NetCdf,
50    /// HDF5
51    Hdf5,
52    /// GeoParquet
53    GeoParquet,
54    /// FlatGeobuf
55    FlatGeobuf,
56    /// Unknown format
57    Unknown,
58}
59
60impl FileFormat {
61    /// Get format description
62    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    /// Create a new file inspector
79    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    /// Detect file format
111    fn detect_format(path: &Path, extension: Option<&str>) -> Result<FileFormat> {
112        // First try by extension
113        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        // Try by magic bytes
128        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                // GeoTIFF: II or MM (little/big endian TIFF)
133                if magic[0..2] == [0x49, 0x49] || magic[0..2] == [0x4D, 0x4D] {
134                    return Ok(FileFormat::GeoTiff);
135                }
136                // GeoJSON: starts with '{'
137                if magic[0] == b'{' {
138                    return Ok(FileFormat::GeoJson);
139                }
140                // HDF5: magic number
141                if magic[0..4] == [0x89, 0x48, 0x44, 0x46] {
142                    return Ok(FileFormat::Hdf5);
143                }
144            }
145        }
146
147        Ok(FileFormat::Unknown)
148    }
149
150    /// Get file info
151    pub fn info(&self) -> &FileInfo {
152        &self.info
153    }
154
155    /// Get file path
156    pub fn path(&self) -> &Path {
157        &self.path
158    }
159
160    /// Generate summary report
161    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    /// Export info as JSON
204    pub fn export_json(&self) -> Result<String> {
205        Ok(serde_json::to_string_pretty(&self.info)?)
206    }
207}
208
209/// Format file size
210fn 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")?; // TIFF magic bytes
256
257        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}