use crate::utils::error::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs::{self, File};
use std::io::{BufReader, BufWriter};
use std::path::{Path, PathBuf};
use crate::graph::Graph;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Snapshot {
pub name: String,
pub created_at: DateTime<Utc>,
pub graph: GraphSnapshot,
pub files: Vec<FileSnapshot>,
pub templates: Vec<TemplateSnapshot>,
pub metadata: BTreeMap<String, String>,
}
impl Snapshot {
pub fn new(
name: String, graph: &Graph, files: Vec<(PathBuf, String)>,
templates: Vec<(PathBuf, String)>,
) -> Result<Self> {
let now = Utc::now();
let graph_snapshot = GraphSnapshot::from_graph(graph)?;
let file_snapshots = files
.into_iter()
.map(|(path, content)| FileSnapshot::new(path, content))
.collect::<Result<Vec<_>>>()?;
let template_snapshots = templates
.into_iter()
.map(|(path, content)| TemplateSnapshot::new(path, content))
.collect::<Result<Vec<_>>>()?;
Ok(Self {
name,
created_at: now,
graph: graph_snapshot,
files: file_snapshots,
templates: template_snapshots,
metadata: BTreeMap::new(),
})
}
pub fn is_compatible_with(&self, graph: &Graph) -> Result<bool> {
let current_hash = graph.compute_hash()?;
Ok(self.graph.hash == current_hash)
}
pub fn find_file(&self, path: &Path) -> Option<&FileSnapshot> {
self.files.iter().find(|f| f.path == path)
}
pub fn find_template(&self, path: &Path) -> Option<&TemplateSnapshot> {
self.templates.iter().find(|t| t.path == path)
}
pub fn add_metadata(&mut self, key: String, value: String) {
self.metadata.insert(key, value);
}
pub fn get_metadata(&self, key: &str) -> Option<&String> {
self.metadata.get(key)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphSnapshot {
pub hash: String,
pub triple_count: usize,
pub sources: Vec<String>,
pub loaded_at: DateTime<Utc>,
}
impl GraphSnapshot {
pub fn from_graph(graph: &Graph) -> Result<Self> {
let hash = graph.compute_hash()?;
let triple_count = graph.len();
Ok(Self {
hash,
triple_count,
sources: Vec::new(), loaded_at: Utc::now(),
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileSnapshot {
pub path: PathBuf,
pub hash: String,
pub size: u64,
pub modified_at: DateTime<Utc>,
pub generated_regions: Vec<Region>,
pub manual_regions: Vec<Region>,
}
impl FileSnapshot {
pub fn new(path: PathBuf, content: String) -> Result<Self> {
let metadata = fs::metadata(&path)?;
let hash = Self::compute_hash(&content);
Ok(Self {
path,
hash,
size: metadata.len(),
modified_at: metadata.modified()?.into(),
generated_regions: Self::detect_regions(&content),
manual_regions: Self::detect_regions(&content),
})
}
fn compute_hash(content: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
format!("sha256:{:x}", hasher.finalize())
}
fn detect_regions(_content: &str) -> Vec<Region> {
Vec::new() }
pub fn has_changed(&self, new_content: &str) -> bool {
let new_hash = Self::compute_hash(new_content);
self.hash != new_hash
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateSnapshot {
pub path: PathBuf,
pub hash: String,
pub queries: Vec<String>,
pub processed_at: DateTime<Utc>,
}
impl TemplateSnapshot {
pub fn new(path: PathBuf, content: String) -> Result<Self> {
let hash = Self::compute_hash(&content);
let queries = Self::extract_queries(&content);
Ok(Self {
path,
hash,
queries,
processed_at: Utc::now(),
})
}
fn compute_hash(content: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
format!("sha256:{:x}", hasher.finalize())
}
fn extract_queries(_content: &str) -> Vec<String> {
Vec::new() }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Region {
pub start: usize,
pub end: usize,
pub region_type: RegionType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RegionType {
Generated,
Manual,
}
pub struct SnapshotManager {
snapshot_dir: PathBuf,
}
impl SnapshotManager {
pub fn new(snapshot_dir: PathBuf) -> Result<Self> {
fs::create_dir_all(&snapshot_dir)?;
Ok(Self { snapshot_dir })
}
pub fn save(&self, snapshot: &Snapshot) -> Result<()> {
let file_path = self.snapshot_dir.join(format!("{}.json", snapshot.name));
let file = File::create(file_path)?;
let writer = BufWriter::new(file);
serde_json::to_writer_pretty(writer, snapshot)?;
Ok(())
}
pub fn load(&self, name: &str) -> Result<Snapshot> {
let file_path = self.snapshot_dir.join(format!("{}.json", name));
let file = File::open(file_path)?;
let reader = BufReader::new(file);
let snapshot = serde_json::from_reader(reader)?;
Ok(snapshot)
}
pub fn list(&self) -> Result<Vec<String>> {
let mut snapshots = Vec::new();
let entries = fs::read_dir(&self.snapshot_dir)?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
snapshots.push(name.to_string());
}
}
snapshots.sort();
Ok(snapshots)
}
pub fn delete(&self, name: &str) -> Result<()> {
let file_path = self.snapshot_dir.join(format!("{}.json", name));
if file_path.exists() {
fs::remove_file(file_path)?;
}
Ok(())
}
pub fn exists(&self, name: &str) -> bool {
let file_path = self.snapshot_dir.join(format!("{}.json", name));
file_path.exists()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::Graph;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_snapshot_creation() {
let graph = Graph::new().unwrap();
graph
.insert_turtle("@prefix : <http://example.org/> . :test a :Class .")
.unwrap();
let temp_dir = tempdir().unwrap();
let test_file = temp_dir.path().join("test.txt");
let test_template = temp_dir.path().join("test.tmpl");
fs::write(&test_file, "test content").unwrap();
fs::write(&test_template, "template content").unwrap();
let files = vec![(test_file, "test content".to_string())];
let templates = vec![(test_template, "template content".to_string())];
let snapshot =
Snapshot::new("test_snapshot".to_string(), &graph, files, templates).unwrap();
assert_eq!(snapshot.name, "test_snapshot");
assert_eq!(snapshot.files.len(), 1);
assert_eq!(snapshot.templates.len(), 1);
assert!(!snapshot.graph.hash.is_empty());
}
#[test]
fn test_snapshot_manager() {
let temp_dir = tempdir().unwrap();
let manager = SnapshotManager::new(temp_dir.path().to_path_buf()).unwrap();
let graph = Graph::new().unwrap();
graph
.insert_turtle("@prefix : <http://example.org/> . :test a :Class .")
.unwrap();
let snapshot = Snapshot::new("manager_test".to_string(), &graph, vec![], vec![]).unwrap();
manager.save(&snapshot).unwrap();
assert!(manager.exists("manager_test"));
let loaded = manager.load("manager_test").unwrap();
assert_eq!(loaded.name, snapshot.name);
let list = manager.list().unwrap();
assert!(list.contains(&"manager_test".to_string()));
manager.delete("manager_test").unwrap();
assert!(!manager.exists("manager_test"));
}
#[test]
fn test_file_snapshot() {
let temp_dir = tempdir().unwrap();
let file_path = temp_dir.path().join("test.txt");
fs::write(&file_path, "test content").unwrap();
let snapshot = FileSnapshot::new(file_path.clone(), "test content".to_string()).unwrap();
assert_eq!(snapshot.path, file_path);
assert!(!snapshot.hash.is_empty());
assert!(snapshot.size > 0);
assert!(!snapshot.has_changed("test content"));
assert!(snapshot.has_changed("different content"));
}
#[test]
fn test_template_snapshot() {
let temp_dir = tempdir().unwrap();
let template_path = temp_dir.path().join("test.tmpl");
let snapshot = TemplateSnapshot::new(
template_path.clone(),
"SELECT ?s WHERE { ?s ?p ?o }".to_string(),
)
.unwrap();
assert_eq!(snapshot.path, template_path);
assert!(!snapshot.hash.is_empty());
}
}