claude_utils/file_manager/
mod.rs1use sha2::{Digest, Sha256};
2use std::collections::HashMap;
3use std::path::{Path, PathBuf};
4use std::sync::{Arc, Mutex};
5use std::time::{Duration, SystemTime};
6use tokio::fs;
7use tracing::{error, info, warn};
8
9use crate::Result;
10
11#[derive(Debug, Clone)]
12pub struct StagedFile {
13 pub path: PathBuf,
14 pub size: usize,
15 pub format: String,
16 pub created_at: SystemTime,
17 pub thumbnail_path: Option<PathBuf>,
18}
19
20#[derive(Debug, Clone)]
21pub struct FileManagerConfig {
22 pub staging_dir: PathBuf,
23 pub cleanup_interval: Duration,
24 pub max_file_age: Duration,
25}
26
27impl Default for FileManagerConfig {
28 fn default() -> Self {
29 let staging_dir = std::env::temp_dir().join(crate::STAGING_DIR_NAME);
30
31 Self {
32 staging_dir,
33 cleanup_interval: Duration::from_secs(crate::CLEANUP_INTERVAL_MINS * 60),
34 max_file_age: Duration::from_secs(crate::CLEANUP_INTERVAL_MINS * 60),
35 }
36 }
37}
38
39pub struct FileManager {
40 config: FileManagerConfig,
41 cache: Arc<Mutex<HashMap<String, StagedFile>>>,
42}
43
44impl FileManager {
45 pub async fn new(config: FileManagerConfig) -> Result<Self> {
46 fs::create_dir_all(&config.staging_dir).await?;
48
49 let manager = Self {
50 config,
51 cache: Arc::new(Mutex::new(HashMap::new())),
52 };
53
54 manager.start_cleanup_task();
56
57 Ok(manager)
58 }
59
60 pub async fn stage_image(&self, data: &[u8], format: &str) -> Result<StagedFile> {
61 let hash = self.calculate_hash(data);
63 let filename = format!("clip-{}.{}", &hash[..8], format);
64 let file_path = self.config.staging_dir.join(&filename);
65
66 if let Some(staged) = self.get_from_cache(&hash) {
68 if file_path.exists() {
69 info!("Using cached file: {}", file_path.display());
70 return Ok(staged);
71 }
72 }
73
74 fs::write(&file_path, data).await?;
76 info!(
77 "Staged file: {} ({} bytes)",
78 file_path.display(),
79 data.len()
80 );
81
82 let thumbnail_path = self.generate_thumbnail(&file_path, data, format).await?;
84
85 let staged_file = StagedFile {
86 path: file_path,
87 size: data.len(),
88 format: format.to_string(),
89 created_at: SystemTime::now(),
90 thumbnail_path,
91 };
92
93 self.update_cache(hash, staged_file.clone());
95
96 Ok(staged_file)
97 }
98
99 pub async fn stage_text(&self, text: &str) -> Result<StagedFile> {
100 let data = text.as_bytes();
101 let hash = self.calculate_hash(data);
102 let filename = format!("clip-{}.txt", &hash[..8]);
103 let file_path = self.config.staging_dir.join(&filename);
104
105 if let Some(staged) = self.get_from_cache(&hash) {
107 if file_path.exists() {
108 return Ok(staged);
109 }
110 }
111
112 fs::write(&file_path, text).await?;
114
115 let staged_file = StagedFile {
116 path: file_path,
117 size: data.len(),
118 format: "txt".to_string(),
119 created_at: SystemTime::now(),
120 thumbnail_path: None,
121 };
122
123 self.update_cache(hash, staged_file.clone());
124
125 Ok(staged_file)
126 }
127
128 async fn generate_thumbnail(
129 &self,
130 file_path: &Path,
131 data: &[u8],
132 format: &str,
133 ) -> Result<Option<PathBuf>> {
134 use image::imageops::FilterType;
135
136 if !["png", "jpg", "jpeg", "gif", "webp", "bmp"].contains(&format) {
138 return Ok(None);
139 }
140
141 let thumb_path = file_path.with_extension("thumb.png");
142
143 match image::load_from_memory(data) {
145 Ok(img) => {
146 let thumbnail = img.resize(256, 256, FilterType::Lanczos3);
147
148 match thumbnail.save(&thumb_path) {
150 Ok(_) => {
151 info!("Generated thumbnail: {}", thumb_path.display());
152 Ok(Some(thumb_path))
153 }
154 Err(e) => {
155 warn!("Failed to save thumbnail: {}", e);
156 Ok(None)
157 }
158 }
159 }
160 Err(e) => {
161 warn!("Failed to generate thumbnail: {}", e);
162 Ok(None)
163 }
164 }
165 }
166
167 fn calculate_hash(&self, data: &[u8]) -> String {
168 let mut hasher = Sha256::new();
169 hasher.update(data);
170 format!("{:x}", hasher.finalize())
171 }
172
173 fn get_from_cache(&self, hash: &str) -> Option<StagedFile> {
174 self.cache.lock().ok()?.get(hash).cloned()
175 }
176
177 fn update_cache(&self, hash: String, file: StagedFile) {
178 if let Ok(mut cache) = self.cache.lock() {
179 cache.insert(hash, file);
180 }
181 }
182
183 fn start_cleanup_task(&self) {
184 let cache = self.cache.clone();
185 let staging_dir = self.config.staging_dir.clone();
186 let max_age = self.config.max_file_age;
187 let interval = self.config.cleanup_interval;
188
189 tokio::spawn(async move {
190 let mut interval_timer = tokio::time::interval(interval);
191
192 loop {
193 interval_timer.tick().await;
194
195 info!("Running cleanup task");
196
197 match fs::read_dir(&staging_dir).await {
199 Ok(mut entries) => {
200 while let Ok(Some(entry)) = entries.next_entry().await {
201 if let Ok(metadata) = entry.metadata().await {
202 if let Ok(modified) = metadata.modified() {
203 if let Ok(age) = modified.elapsed() {
204 if age > max_age {
205 let path = entry.path();
206 match fs::remove_file(&path).await {
207 Ok(_) => {
208 info!("Cleaned up old file: {}", path.display())
209 }
210 Err(e) => warn!("Failed to remove file: {}", e),
211 }
212 }
213 }
214 }
215 }
216 }
217 }
218 Err(e) => error!("Failed to read staging directory: {}", e),
219 }
220
221 if let Ok(mut cache_guard) = cache.lock() {
223 let _now = SystemTime::now();
224 cache_guard.retain(|_, file| {
225 file.created_at
226 .elapsed()
227 .map(|age| age < max_age)
228 .unwrap_or(false)
229 });
230 }
231 }
232 });
233 }
234
235 pub fn get_staging_dir(&self) -> &Path {
236 &self.config.staging_dir
237 }
238}