use anyhow::{Context, Result};
use serde::Serialize;
use serde_json::Value;
use std::path::PathBuf;
use std::time::SystemTime;
#[derive(Debug, Clone)]
pub struct FileTracker {
workspace_root: PathBuf,
tracked_patterns: Vec<String>,
}
impl FileTracker {
pub fn new(workspace_root: PathBuf) -> Self {
Self {
workspace_root,
tracked_patterns: vec![
"*.pdf".to_string(),
"*.xlsx".to_string(),
"*.csv".to_string(),
"*.docx".to_string(),
"*.png".to_string(),
"*.jpg".to_string(),
"*.json".to_string(),
"*.xml".to_string(),
],
}
}
pub fn add_pattern(&mut self, pattern: String) {
self.tracked_patterns.push(pattern);
}
pub async fn detect_new_files(&self, since: SystemTime) -> Result<Vec<TrackedFile>> {
let mut new_files = Vec::new();
for pattern in &self.tracked_patterns {
let matches = self.find_files_matching_pattern(pattern).await?;
for file_path in matches {
if let Ok(metadata) = tokio::fs::metadata(&file_path).await
&& let Ok(modified) = metadata.modified()
&& modified >= since
&& metadata.is_file()
{
new_files.push(TrackedFile {
absolute_path: file_path,
size: metadata.len(),
modified,
});
}
}
}
Ok(new_files)
}
pub async fn verify_file_exists(&self, filename: &str) -> Result<Option<TrackedFile>> {
let file_path = self.workspace_root.join(filename);
match tokio::fs::metadata(&file_path).await {
Ok(metadata) if metadata.is_file() => {
let modified = metadata.modified().unwrap_or_else(|_| SystemTime::now());
Ok(Some(TrackedFile {
absolute_path: file_path,
size: metadata.len(),
modified,
}))
}
Ok(_) => Ok(None), Err(_) => Ok(None), }
}
pub fn get_absolute_path(&self, filename: &str) -> PathBuf {
self.workspace_root.join(filename)
}
pub async fn find_files_matching_pattern(&self, pattern: &str) -> Result<Vec<PathBuf>> {
use tokio::task;
let workspace = self.workspace_root.clone();
let pattern = pattern.to_string();
task::spawn_blocking(move || {
let mut files = Vec::new();
if let Ok(glob_pattern) = glob::glob(&format!("{}/{}", workspace.display(), pattern)) {
for path in glob_pattern.flatten() {
files.push(path);
}
}
Ok(files)
})
.await
.context("Failed to spawn file search task")?
}
pub fn generate_verification_code(&self, filename: &str) -> String {
format!(
r#"
import os
import sys
file_path = os.path.abspath('{filename}')
exists = os.path.exists(file_path)
is_file = os.path.isfile(file_path) if exists else False
size = os.path.getsize(file_path) if exists else 0
print(f"FILE_VERIFICATION: {{file_path}} {{exists}} {{is_file}} {{size}}")
sys.exit(0 if exists and is_file else 1)
"#,
filename = filename
)
}
pub fn generate_file_summary(&self, files: &[TrackedFile]) -> String {
if files.is_empty() {
return "No generated files detected.".to_string();
}
let mut summary = String::from("Generated files:\n");
for file in files {
summary.push_str(&format!(
" - {} ({} bytes)\n",
file.absolute_path.display(),
file.size
));
}
summary
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TrackedFile {
pub absolute_path: PathBuf,
pub size: u64,
#[serde(with = "system_time_serde")]
pub modified: SystemTime,
}
mod system_time_serde {
use serde::Serializer;
use std::time::{SystemTime, UNIX_EPOCH};
pub fn serialize<S>(time: &SystemTime, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let duration = time
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0));
serializer.serialize_u64(duration.as_secs())
}
}
impl TrackedFile {
pub fn to_json(&self) -> Value {
serde_json::json!({
"absolute_path": self.absolute_path.display().to_string(),
"size": self.size,
"modified": format!("{:?}", self.modified),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio;
#[tokio::test]
async fn test_file_tracker_basic() {
let tracker = FileTracker::new(PathBuf::from("/test"));
assert!(!tracker.tracked_patterns.is_empty());
}
#[tokio::test]
async fn test_verification_code_generation() {
let tracker = FileTracker::new(PathBuf::from("/workspace"));
let code = tracker.generate_verification_code("test.pdf");
assert!(code.contains("test.pdf"));
assert!(code.contains("FILE_VERIFICATION"));
}
#[test]
fn test_file_summary_generation() {
let tracker = FileTracker::new(PathBuf::from("/workspace"));
let files = vec![TrackedFile {
absolute_path: PathBuf::from("/workspace/test.pdf"),
size: 1024,
modified: SystemTime::now(),
}];
let summary = tracker.generate_file_summary(&files);
assert!(summary.contains("test.pdf"));
assert!(summary.contains("1024 bytes"));
}
}