1use serde::{Deserialize, Serialize};
6use std::path::{Path, PathBuf};
7use std::sync::atomic::{AtomicUsize, Ordering};
8use std::sync::Arc;
9
10pub struct FileHandle {
16 pub id: String,
18
19 pub path: PathBuf,
21
22 pub metadata: FileMetadata,
24
25 pub ref_count: Arc<AtomicUsize>,
27}
28
29impl Clone for FileHandle {
30 fn clone(&self) -> Self {
31 Self {
32 id: self.id.clone(),
33 path: self.path.clone(),
34 metadata: self.metadata.clone(),
35 ref_count: Arc::new(AtomicUsize::new(1)),
36 }
37 }
38}
39
40impl std::fmt::Debug for FileHandle {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("FileHandle")
43 .field("id", &self.id)
44 .field("path", &self.path)
45 .field("metadata", &self.metadata)
46 .field("ref_count", &self.ref_count.load(Ordering::SeqCst))
47 .finish()
48 }
49}
50
51impl FileHandle {
52 pub fn new(id: String, path: PathBuf, metadata: FileMetadata) -> Self {
54 Self {
55 id,
56 path,
57 metadata,
58 ref_count: Arc::new(AtomicUsize::new(1)),
59 }
60 }
61
62 pub fn with_ref_count(id: String, path: PathBuf, metadata: FileMetadata, count: usize) -> Self {
64 Self {
65 id,
66 path,
67 metadata,
68 ref_count: Arc::new(AtomicUsize::new(count)),
69 }
70 }
71
72 pub fn ref_count(&self) -> usize {
74 self.ref_count.load(Ordering::SeqCst)
75 }
76
77 pub fn is_last_ref(&self) -> bool {
79 self.ref_count() <= 1
80 }
81
82 pub fn full_path(&self, base_dir: &Path) -> PathBuf {
84 base_dir.join(&self.path)
85 }
86
87 pub fn touch(&mut self) {
89 self.metadata.last_accessed_at = Some(chrono::Utc::now());
90 }
91
92 pub fn extension(&self) -> Option<&str> {
94 std::path::Path::new(&self.metadata.name)
95 .extension()
96 .and_then(|e| e.to_str())
97 }
98
99 pub fn is_image(&self) -> bool {
101 matches!(
102 self.metadata.mime_type.as_deref(),
103 Some("image/jpeg")
104 | Some("image/png")
105 | Some("image/gif")
106 | Some("image/webp")
107 | Some("image/svg+xml")
108 )
109 }
110
111 pub fn is_text(&self) -> bool {
113 matches!(
114 self.metadata.mime_type.as_deref(),
115 Some("text/plain")
116 | Some("text/markdown")
117 | Some("text/html")
118 | Some("application/json")
119 | Some("application/xml")
120 | Some("text/csv")
121 ) || self
122 .extension()
123 .map(|e| {
124 matches!(
125 e.to_lowercase().as_str(),
126 "txt" | "md" | "markdown" | "json" | "xml" | "csv" | "rs" | "py" | "js" | "ts"
127 )
128 })
129 .unwrap_or(false)
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct FileMetadata {
136 pub name: String,
138
139 pub size: u64,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub mime_type: Option<String>,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub source: Option<String>,
149
150 pub created_at: chrono::DateTime<chrono::Utc>,
152
153 #[serde(skip_serializing_if = "Option::is_none")]
155 pub last_accessed_at: Option<chrono::DateTime<chrono::Utc>>,
156
157 #[serde(skip_serializing_if = "Option::is_none")]
159 pub preview: Option<String>,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct FileIndexEntry {
165 pub id: String,
166 pub path: PathBuf,
167 pub size: u64,
168 pub ref_count: usize,
169 pub created_at: chrono::DateTime<chrono::Utc>,
170 pub last_accessed_at: Option<chrono::DateTime<chrono::Utc>>,
171 pub metadata: FileMetadata,
172}
173
174impl FileIndexEntry {
175 pub fn to_handle(&self) -> FileHandle {
177 FileHandle::with_ref_count(
178 self.id.clone(),
179 self.path.clone(),
180 self.metadata.clone(),
181 self.ref_count,
182 )
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 fn create_test_metadata() -> FileMetadata {
191 FileMetadata {
192 name: "test.txt".to_string(),
193 size: 100,
194 mime_type: Some("text/plain".to_string()),
195 source: Some("test".to_string()),
196 created_at: chrono::Utc::now(),
197 last_accessed_at: None,
198 preview: None,
199 }
200 }
201
202 #[test]
203 fn test_ref_counting() {
204 let handle = FileHandle::new(
205 "abc123".to_string(),
206 PathBuf::from("data/ab/c123"),
207 create_test_metadata(),
208 );
209
210 assert_eq!(handle.ref_count(), 1);
211
212 let cloned = handle.clone();
214 assert_eq!(handle.ref_count(), 1); assert_eq!(cloned.ref_count(), 1); }
220
221 #[test]
222 fn test_is_text() {
223 let metadata = FileMetadata {
224 name: "test.txt".to_string(),
225 size: 100,
226 mime_type: Some("text/plain".to_string()),
227 source: None,
228 created_at: chrono::Utc::now(),
229 last_accessed_at: None,
230 preview: None,
231 };
232
233 let handle = FileHandle::new("id".to_string(), PathBuf::from("path"), metadata);
234 assert!(handle.is_text());
235 }
236
237 #[test]
238 fn test_is_image() {
239 let metadata = FileMetadata {
240 name: "test.png".to_string(),
241 size: 100,
242 mime_type: Some("image/png".to_string()),
243 source: None,
244 created_at: chrono::Utc::now(),
245 last_accessed_at: None,
246 preview: None,
247 };
248
249 let handle = FileHandle::new("id".to_string(), PathBuf::from("path"), metadata);
250 assert!(handle.is_image());
251 }
252}