1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
use agent_diva_core::attachment::FileAttachment;
use agent_diva_files::handle::FileMetadata;
use agent_diva_files::FileManager;
use std::sync::Arc;
/// File service for handling file uploads and downloads
///
/// Uses a shared Arc<FileManager> to ensure data consistency
/// across all components. Do NOT create multiple FileManager instances.
pub struct FileService {
manager: Arc<FileManager>,
}
impl FileService {
/// Create a new file service with the given file manager
///
/// # Arguments
/// * `manager` - Shared FileManager instance
///
/// # Example
/// ```ignore
/// use agent_diva_files::{FileManager, FileConfig};
/// use agent_diva_manager::file_service::FileService;
/// use std::sync::Arc;
///
/// let config = FileConfig::default();
/// let manager = Arc::new(FileManager::new(config).await?);
/// let file_service = FileService::new(manager);
/// ```
pub fn new(manager: Arc<FileManager>) -> Self {
Self { manager }
}
/// Upload a file and return a FileAttachment
///
/// # Arguments
/// * `file_name` - Original file name
/// * `bytes` - File content
/// * `channel` - Source channel (e.g., "ui", "telegram")
/// * `message_id` - Optional message ID for association
pub async fn upload_file(
&self,
file_name: &str,
bytes: Vec<u8>,
channel: &str,
message_id: Option<&str>,
) -> anyhow::Result<FileAttachment> {
let mime_type = mime_guess::from_path(file_name)
.first()
.map(|m| m.to_string());
let metadata = FileMetadata {
name: file_name.to_string(),
size: bytes.len() as u64,
mime_type,
source: Some(channel.to_string()),
created_at: chrono::Utc::now(),
last_accessed_at: None,
preview: None,
};
let handle = self.manager.store(&bytes, metadata).await?;
let attachment = FileAttachment::from_handle(handle, channel, message_id);
Ok(attachment)
}
/// Read file content by ID
///
/// # Arguments
/// * `file_id` - File ID (SHA256 hash)
pub async fn read_file(&self, file_id: &str) -> anyhow::Result<Vec<u8>> {
let handle = self.manager.get(file_id).await?;
let content = self.manager.read(&handle).await?;
Ok(content)
}
/// Get the underlying FileManager
pub fn manager(&self) -> &FileManager {
&self.manager
}
/// Get a clone of the Arc<FileManager>
pub fn manager_arc(&self) -> Arc<FileManager> {
self.manager.clone()
}
}
impl Clone for FileService {
fn clone(&self) -> Self {
Self {
manager: self.manager.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use agent_diva_files::FileConfig;
use tempfile::TempDir;
async fn create_test_service() -> (FileService, TempDir) {
let temp_dir = TempDir::new().unwrap();
let config = FileConfig::with_path(temp_dir.path().to_path_buf());
let manager = Arc::new(FileManager::new(config).await.unwrap());
let service = FileService::new(manager);
(service, temp_dir)
}
#[tokio::test]
async fn test_upload_and_read() {
let (service, _temp) = create_test_service().await;
let content = b"hello world";
let attachment = service
.upload_file("test.txt", content.to_vec(), "test", None)
.await
.unwrap();
assert!(attachment.file_id.starts_with("sha256:"));
assert_eq!(attachment.file_name, "test.txt");
// Read back
let read_content = service.read_file(&attachment.file_id).await.unwrap();
assert_eq!(read_content, content);
}
#[tokio::test]
async fn test_clone() {
let (service, _temp) = create_test_service().await;
let cloned = service.clone();
// Both should share the same manager
let content = b"test";
let attachment = service
.upload_file("test.txt", content.to_vec(), "test", None)
.await
.unwrap();
// Clone should be able to read
let read_content = cloned.read_file(&attachment.file_id).await.unwrap();
assert_eq!(read_content, content);
}
}