tuitbot_core/source/
local_fs.rs1use std::path::{Path, PathBuf};
8
9use async_trait::async_trait;
10use sha2::{Digest, Sha256};
11
12use super::{ContentSourceProvider, SourceError, SourceFile};
13use crate::automation::watchtower::matches_patterns;
14
15pub struct LocalFsProvider {
17 base_path: PathBuf,
18}
19
20impl LocalFsProvider {
21 pub fn new(base_path: PathBuf) -> Self {
22 Self { base_path }
23 }
24}
25
26#[async_trait]
27impl ContentSourceProvider for LocalFsProvider {
28 fn source_type(&self) -> &str {
29 "local_fs"
30 }
31
32 async fn scan_for_changes(
33 &self,
34 _since_cursor: Option<&str>,
35 patterns: &[String],
36 ) -> Result<Vec<SourceFile>, SourceError> {
37 let base = self.base_path.clone();
38 let patterns = patterns.to_vec();
39
40 let files = tokio::task::spawn_blocking(move || walk_and_hash(&base, &base, &patterns))
42 .await
43 .map_err(|e| SourceError::Io(std::io::Error::other(e)))??;
44
45 Ok(files)
46 }
47
48 async fn read_content(&self, file_id: &str) -> Result<String, SourceError> {
49 let full_path = self.base_path.join(file_id);
50 tokio::fs::read_to_string(&full_path)
51 .await
52 .map_err(SourceError::Io)
53 }
54}
55
56fn walk_and_hash(
58 base: &Path,
59 current: &Path,
60 patterns: &[String],
61) -> Result<Vec<SourceFile>, SourceError> {
62 let mut out = Vec::new();
63 let entries = std::fs::read_dir(current)?;
64
65 for entry in entries {
66 let entry = entry?;
67 let ft = entry.file_type()?;
68 let path = entry.path();
69
70 if ft.is_dir() {
71 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
72 if name.starts_with('.') {
73 continue;
74 }
75 }
76 out.extend(walk_and_hash(base, &path, patterns)?);
77 } else if ft.is_file() && matches_patterns(&path, patterns) {
78 if let Ok(rel) = path.strip_prefix(base) {
79 let content = std::fs::read(&path)?;
80 let hash = format!("{:x}", Sha256::digest(&content));
81 let modified = std::fs::metadata(&path)?
82 .modified()
83 .ok()
84 .map(|t| {
85 let dt: chrono::DateTime<chrono::Utc> = t.into();
86 dt.to_rfc3339()
87 })
88 .unwrap_or_default();
89
90 out.push(SourceFile {
91 provider_id: rel.to_string_lossy().to_string(),
92 display_name: path
93 .file_name()
94 .unwrap_or_default()
95 .to_string_lossy()
96 .to_string(),
97 content_hash: hash,
98 modified_at: modified,
99 });
100 }
101 }
102 }
103 Ok(out)
104}