use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
use tokio::fs;
use tracing::{error, info, warn};
use crate::Result;
#[derive(Debug, Clone)]
pub struct StagedFile {
pub path: PathBuf,
pub size: usize,
pub format: String,
pub created_at: SystemTime,
pub thumbnail_path: Option<PathBuf>,
}
#[derive(Debug, Clone)]
pub struct FileManagerConfig {
pub staging_dir: PathBuf,
pub cleanup_interval: Duration,
pub max_file_age: Duration,
}
impl Default for FileManagerConfig {
fn default() -> Self {
let staging_dir = std::env::temp_dir().join(crate::STAGING_DIR_NAME);
Self {
staging_dir,
cleanup_interval: Duration::from_secs(crate::CLEANUP_INTERVAL_MINS * 60),
max_file_age: Duration::from_secs(crate::CLEANUP_INTERVAL_MINS * 60),
}
}
}
pub struct FileManager {
config: FileManagerConfig,
cache: Arc<Mutex<HashMap<String, StagedFile>>>,
}
impl FileManager {
pub async fn new(config: FileManagerConfig) -> Result<Self> {
fs::create_dir_all(&config.staging_dir).await?;
let manager = Self {
config,
cache: Arc::new(Mutex::new(HashMap::new())),
};
manager.start_cleanup_task();
Ok(manager)
}
pub async fn stage_image(&self, data: &[u8], format: &str) -> Result<StagedFile> {
let hash = self.calculate_hash(data);
let filename = format!("clip-{}.{}", &hash[..8], format);
let file_path = self.config.staging_dir.join(&filename);
if let Some(staged) = self.get_from_cache(&hash) {
if file_path.exists() {
info!("Using cached file: {}", file_path.display());
return Ok(staged);
}
}
fs::write(&file_path, data).await?;
info!(
"Staged file: {} ({} bytes)",
file_path.display(),
data.len()
);
let thumbnail_path = self.generate_thumbnail(&file_path, data, format).await?;
let staged_file = StagedFile {
path: file_path,
size: data.len(),
format: format.to_string(),
created_at: SystemTime::now(),
thumbnail_path,
};
self.update_cache(hash, staged_file.clone());
Ok(staged_file)
}
pub async fn stage_text(&self, text: &str) -> Result<StagedFile> {
let data = text.as_bytes();
let hash = self.calculate_hash(data);
let filename = format!("clip-{}.txt", &hash[..8]);
let file_path = self.config.staging_dir.join(&filename);
if let Some(staged) = self.get_from_cache(&hash) {
if file_path.exists() {
return Ok(staged);
}
}
fs::write(&file_path, text).await?;
let staged_file = StagedFile {
path: file_path,
size: data.len(),
format: "txt".to_string(),
created_at: SystemTime::now(),
thumbnail_path: None,
};
self.update_cache(hash, staged_file.clone());
Ok(staged_file)
}
async fn generate_thumbnail(
&self,
file_path: &Path,
data: &[u8],
format: &str,
) -> Result<Option<PathBuf>> {
use image::imageops::FilterType;
if !["png", "jpg", "jpeg", "gif", "webp", "bmp"].contains(&format) {
return Ok(None);
}
let thumb_path = file_path.with_extension("thumb.png");
match image::load_from_memory(data) {
Ok(img) => {
let thumbnail = img.resize(256, 256, FilterType::Lanczos3);
match thumbnail.save(&thumb_path) {
Ok(_) => {
info!("Generated thumbnail: {}", thumb_path.display());
Ok(Some(thumb_path))
}
Err(e) => {
warn!("Failed to save thumbnail: {}", e);
Ok(None)
}
}
}
Err(e) => {
warn!("Failed to generate thumbnail: {}", e);
Ok(None)
}
}
}
fn calculate_hash(&self, data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
format!("{:x}", hasher.finalize())
}
fn get_from_cache(&self, hash: &str) -> Option<StagedFile> {
self.cache.lock().ok()?.get(hash).cloned()
}
fn update_cache(&self, hash: String, file: StagedFile) {
if let Ok(mut cache) = self.cache.lock() {
cache.insert(hash, file);
}
}
fn start_cleanup_task(&self) {
let cache = self.cache.clone();
let staging_dir = self.config.staging_dir.clone();
let max_age = self.config.max_file_age;
let interval = self.config.cleanup_interval;
tokio::spawn(async move {
let mut interval_timer = tokio::time::interval(interval);
loop {
interval_timer.tick().await;
info!("Running cleanup task");
match fs::read_dir(&staging_dir).await {
Ok(mut entries) => {
while let Ok(Some(entry)) = entries.next_entry().await {
if let Ok(metadata) = entry.metadata().await {
if let Ok(modified) = metadata.modified() {
if let Ok(age) = modified.elapsed() {
if age > max_age {
let path = entry.path();
match fs::remove_file(&path).await {
Ok(_) => {
info!("Cleaned up old file: {}", path.display())
}
Err(e) => warn!("Failed to remove file: {}", e),
}
}
}
}
}
}
}
Err(e) => error!("Failed to read staging directory: {}", e),
}
if let Ok(mut cache_guard) = cache.lock() {
let _now = SystemTime::now();
cache_guard.retain(|_, file| {
file.created_at
.elapsed()
.map(|age| age < max_age)
.unwrap_or(false)
});
}
}
});
}
pub fn get_staging_dir(&self) -> &Path {
&self.config.staging_dir
}
}