use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
pub struct FileHandle {
pub id: String,
pub path: PathBuf,
pub metadata: FileMetadata,
pub ref_count: Arc<AtomicUsize>,
}
impl Clone for FileHandle {
fn clone(&self) -> Self {
Self {
id: self.id.clone(),
path: self.path.clone(),
metadata: self.metadata.clone(),
ref_count: Arc::new(AtomicUsize::new(1)),
}
}
}
impl std::fmt::Debug for FileHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FileHandle")
.field("id", &self.id)
.field("path", &self.path)
.field("metadata", &self.metadata)
.field("ref_count", &self.ref_count.load(Ordering::SeqCst))
.finish()
}
}
impl FileHandle {
pub fn new(id: String, path: PathBuf, metadata: FileMetadata) -> Self {
Self {
id,
path,
metadata,
ref_count: Arc::new(AtomicUsize::new(1)),
}
}
pub fn with_ref_count(id: String, path: PathBuf, metadata: FileMetadata, count: usize) -> Self {
Self {
id,
path,
metadata,
ref_count: Arc::new(AtomicUsize::new(count)),
}
}
pub fn ref_count(&self) -> usize {
self.ref_count.load(Ordering::SeqCst)
}
pub fn is_last_ref(&self) -> bool {
self.ref_count() <= 1
}
pub fn full_path(&self, base_dir: &Path) -> PathBuf {
base_dir.join(&self.path)
}
pub fn touch(&mut self) {
self.metadata.last_accessed_at = Some(chrono::Utc::now());
}
pub fn extension(&self) -> Option<&str> {
std::path::Path::new(&self.metadata.name)
.extension()
.and_then(|e| e.to_str())
}
pub fn is_image(&self) -> bool {
matches!(
self.metadata.mime_type.as_deref(),
Some("image/jpeg")
| Some("image/png")
| Some("image/gif")
| Some("image/webp")
| Some("image/svg+xml")
)
}
pub fn is_text(&self) -> bool {
matches!(
self.metadata.mime_type.as_deref(),
Some("text/plain")
| Some("text/markdown")
| Some("text/html")
| Some("application/json")
| Some("application/xml")
| Some("text/csv")
) || self
.extension()
.map(|e| {
matches!(
e.to_lowercase().as_str(),
"txt" | "md" | "markdown" | "json" | "xml" | "csv" | "rs" | "py" | "js" | "ts"
)
})
.unwrap_or(false)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMetadata {
pub name: String,
pub size: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_accessed_at: Option<chrono::DateTime<chrono::Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preview: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileIndexEntry {
pub id: String,
pub path: PathBuf,
pub size: u64,
pub ref_count: usize,
pub created_at: chrono::DateTime<chrono::Utc>,
pub last_accessed_at: Option<chrono::DateTime<chrono::Utc>>,
pub metadata: FileMetadata,
}
impl FileIndexEntry {
pub fn to_handle(&self) -> FileHandle {
FileHandle::with_ref_count(
self.id.clone(),
self.path.clone(),
self.metadata.clone(),
self.ref_count,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_metadata() -> FileMetadata {
FileMetadata {
name: "test.txt".to_string(),
size: 100,
mime_type: Some("text/plain".to_string()),
source: Some("test".to_string()),
created_at: chrono::Utc::now(),
last_accessed_at: None,
preview: None,
}
}
#[test]
fn test_ref_counting() {
let handle = FileHandle::new(
"abc123".to_string(),
PathBuf::from("data/ab/c123"),
create_test_metadata(),
);
assert_eq!(handle.ref_count(), 1);
let cloned = handle.clone();
assert_eq!(handle.ref_count(), 1); assert_eq!(cloned.ref_count(), 1);
}
#[test]
fn test_is_text() {
let metadata = FileMetadata {
name: "test.txt".to_string(),
size: 100,
mime_type: Some("text/plain".to_string()),
source: None,
created_at: chrono::Utc::now(),
last_accessed_at: None,
preview: None,
};
let handle = FileHandle::new("id".to_string(), PathBuf::from("path"), metadata);
assert!(handle.is_text());
}
#[test]
fn test_is_image() {
let metadata = FileMetadata {
name: "test.png".to_string(),
size: 100,
mime_type: Some("image/png".to_string()),
source: None,
created_at: chrono::Utc::now(),
last_accessed_at: None,
preview: None,
};
let handle = FileHandle::new("id".to_string(), PathBuf::from("path"), metadata);
assert!(handle.is_image());
}
}