Skip to main content

agent_diva_files/
handle.rs

1//! File handle with reference counting
2//!
3//! FileHandle provides a safe reference to a stored file.
4
5use serde::{Deserialize, Serialize};
6use std::path::{Path, PathBuf};
7use std::sync::atomic::{AtomicUsize, Ordering};
8use std::sync::Arc;
9
10/// A handle to a stored file
11///
12/// FileHandle tracks the file ID, path, and metadata.
13/// Note: Reference counting is managed by FileIndex, not FileHandle.
14/// FileHandle's ref_count is for informational purposes only.
15pub struct FileHandle {
16    /// Unique file ID (SHA256 hash)
17    pub id: String,
18
19    /// Storage path relative to the data directory
20    pub path: PathBuf,
21
22    /// File metadata
23    pub metadata: FileMetadata,
24
25    /// Local reference count view (for debugging/information)
26    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    /// Create a new file handle
53    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    /// Create from index entry with specific ref count
63    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    /// Get the current reference count
73    pub fn ref_count(&self) -> usize {
74        self.ref_count.load(Ordering::SeqCst)
75    }
76
77    /// Check if this is the last reference
78    pub fn is_last_ref(&self) -> bool {
79        self.ref_count() <= 1
80    }
81
82    /// Get the full path given a base directory
83    pub fn full_path(&self, base_dir: &Path) -> PathBuf {
84        base_dir.join(&self.path)
85    }
86
87    /// Update last accessed timestamp
88    pub fn touch(&mut self) {
89        self.metadata.last_accessed_at = Some(chrono::Utc::now());
90    }
91
92    /// Get file extension (if any)
93    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    /// Check if file is an image
100    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    /// Check if file is a text file
112    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/// File metadata stored with the handle
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct FileMetadata {
136    /// Original filename
137    pub name: String,
138
139    /// File size in bytes
140    pub size: u64,
141
142    /// MIME type (if known)
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub mime_type: Option<String>,
145
146    /// Source channel (telegram, discord, ui, etc.)
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub source: Option<String>,
149
150    /// Creation timestamp
151    pub created_at: chrono::DateTime<chrono::Utc>,
152
153    /// Last access timestamp
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub last_accessed_at: Option<chrono::DateTime<chrono::Utc>>,
156
157    /// Optional preview/content for small files
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub preview: Option<String>,
160}
161
162/// Index entry for persistent storage
163#[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    /// Convert to FileHandle (with index's ref_count)
176    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        // Clone creates independent handle with its own ref_count
213        let cloned = handle.clone();
214        assert_eq!(handle.ref_count(), 1); // Original unchanged
215        assert_eq!(cloned.ref_count(), 1); // Clone starts at 1
216
217        // Note: Reference counting should be managed through FileManager
218        // FileHandle.ref_count is for informational purposes only
219    }
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}