1use super::{Database, now_ms};
4use crate::types::{Attachment, AttachmentMeta};
5use anyhow::{Result, anyhow};
6use rusqlite::params;
7
8impl Database {
9 pub fn add_attachment(
13 &self,
14 task_id: &str,
15 name: String,
16 content: String,
17 mime_type: Option<String>,
18 file_path: Option<String>,
19 ) -> Result<i32> {
20 let now = now_ms();
21 let mime_type = mime_type.unwrap_or_else(|| "text/plain".to_string());
22
23 self.with_conn_mut(|conn| {
24 let tx = conn.transaction()?;
25
26 let exists: bool = tx
28 .query_row(
29 "SELECT 1 FROM tasks WHERE id = ?1",
30 params![task_id],
31 |_| Ok(true),
32 )
33 .unwrap_or(false);
34
35 if !exists {
36 return Err(anyhow!("Task not found"));
37 }
38
39 let max_order: Option<i32> = tx.query_row(
41 "SELECT MAX(order_index) FROM attachments WHERE task_id = ?1",
42 params![task_id],
43 |row| row.get(0),
44 )?;
45 let order_index = max_order.unwrap_or(-1) + 1;
46
47 tx.execute(
48 "INSERT INTO attachments (task_id, order_index, name, mime_type, content, file_path, created_at)
49 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
50 params![
51 task_id,
52 order_index,
53 name,
54 mime_type,
55 content,
56 file_path,
57 now,
58 ],
59 )?;
60
61 tx.commit()?;
62 Ok(order_index)
63 })
64 }
65
66 pub fn get_attachments_full(
69 &self,
70 task_id: &str,
71 include_content: bool,
72 ) -> Result<Vec<Attachment>> {
73 self.with_conn(|conn| {
74 let mut stmt = conn.prepare(
75 "SELECT task_id, order_index, name, mime_type, content, file_path, created_at
76 FROM attachments WHERE task_id = ?1 ORDER BY order_index, created_at",
77 )?;
78
79 let attachments = stmt
80 .query_map(params![task_id], |row| {
81 let task_id: String = row.get(0)?;
82 let order_index: i32 = row.get(1)?;
83 let name: String = row.get(2)?;
84 let mime_type: String = row.get(3)?;
85 let content: String = row.get(4)?;
86 let file_path: Option<String> = row.get(5)?;
87 let created_at: i64 = row.get(6)?;
88
89 Ok(Attachment {
90 task_id,
91 order_index,
92 name,
93 mime_type,
94 content: if include_content {
95 content
96 } else {
97 String::new()
98 },
99 file_path,
100 created_at,
101 })
102 })?
103 .filter_map(|r| r.ok())
104 .collect();
105
106 Ok(attachments)
107 })
108 }
109
110 pub fn get_attachments(&self, task_id: &str) -> Result<Vec<AttachmentMeta>> {
112 self.get_attachments_filtered(task_id, None, None)
113 }
114
115 pub fn get_attachments_filtered(
119 &self,
120 task_id: &str,
121 name_pattern: Option<&str>,
122 mime_pattern: Option<&str>,
123 ) -> Result<Vec<AttachmentMeta>> {
124 self.with_conn(|conn| {
125 let mut sql = String::from(
127 "SELECT task_id, order_index, name, mime_type, file_path, created_at
128 FROM attachments WHERE task_id = ?1",
129 );
130
131 let name_like = name_pattern.map(|p| {
133 p.replace('*', "%").replace('?', "_")
135 });
136
137 if name_like.is_some() {
138 sql.push_str(" AND name LIKE ?2 ESCAPE '\\'");
139 }
140
141 if mime_pattern.is_some() {
142 let idx = if name_like.is_some() { 3 } else { 2 };
143 sql.push_str(&format!(" AND mime_type LIKE ?{} ESCAPE '\\'", idx));
144 }
145
146 sql.push_str(" ORDER BY order_index, created_at");
147
148 let mut stmt = conn.prepare(&sql)?;
149
150 let attachments: Vec<AttachmentMeta> = match (&name_like, mime_pattern) {
152 (Some(name), Some(mime)) => {
153 let mime_like = format!("{}%", mime);
154 stmt.query_map(params![task_id, name, mime_like], |row| {
155 Self::map_attachment_meta(row)
156 })?
157 .filter_map(|r| r.ok())
158 .collect()
159 }
160 (Some(name), None) => stmt
161 .query_map(params![task_id, name], Self::map_attachment_meta)?
162 .filter_map(|r| r.ok())
163 .collect(),
164 (None, Some(mime)) => {
165 let mime_like = format!("{}%", mime);
166 stmt.query_map(params![task_id, mime_like], |row| {
167 Self::map_attachment_meta(row)
168 })?
169 .filter_map(|r| r.ok())
170 .collect()
171 }
172 (None, None) => stmt
173 .query_map(params![task_id], Self::map_attachment_meta)?
174 .filter_map(|r| r.ok())
175 .collect(),
176 };
177
178 Ok(attachments)
179 })
180 }
181
182 fn map_attachment_meta(row: &rusqlite::Row) -> rusqlite::Result<AttachmentMeta> {
184 Ok(AttachmentMeta {
185 task_id: row.get(0)?,
186 order_index: row.get(1)?,
187 name: row.get(2)?,
188 mime_type: row.get(3)?,
189 file_path: row.get(4)?,
190 created_at: row.get(5)?,
191 })
192 }
193
194 pub fn get_attachment(&self, task_id: &str, order_index: i32) -> Result<Option<Attachment>> {
198 self.with_conn(|conn| {
199 let mut stmt = conn.prepare(
200 "SELECT task_id, order_index, name, mime_type, content, file_path, created_at
201 FROM attachments WHERE task_id = ?1 AND order_index = ?2",
202 )?;
203
204 let result = stmt.query_row(params![task_id, order_index], |row| {
205 let task_id: String = row.get(0)?;
206 let order_index: i32 = row.get(1)?;
207 let name: String = row.get(2)?;
208 let mime_type: String = row.get(3)?;
209 let content: String = row.get(4)?;
210 let file_path: Option<String> = row.get(5)?;
211 let created_at: i64 = row.get(6)?;
212
213 Ok(Attachment {
214 task_id,
215 order_index,
216 name,
217 mime_type,
218 content,
219 file_path,
220 created_at,
221 })
222 });
223
224 match result {
225 Ok(attachment) => Ok(Some(attachment)),
226 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
227 Err(e) => Err(e.into()),
228 }
229 })
230 }
231
232 pub fn get_attachment_file_path(
234 &self,
235 task_id: &str,
236 order_index: i32,
237 ) -> Result<Option<String>> {
238 self.with_conn(|conn| {
239 let result = conn.query_row(
240 "SELECT file_path FROM attachments WHERE task_id = ?1 AND order_index = ?2",
241 params![task_id, order_index],
242 |row| row.get::<_, Option<String>>(0),
243 );
244
245 match result {
246 Ok(file_path) => Ok(file_path),
247 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
248 Err(e) => Err(e.into()),
249 }
250 })
251 }
252
253 pub fn delete_attachment(&self, task_id: &str, order_index: i32) -> Result<bool> {
255 self.with_conn(|conn| {
256 let deleted = conn.execute(
257 "DELETE FROM attachments WHERE task_id = ?1 AND order_index = ?2",
258 params![task_id, order_index],
259 )?;
260
261 Ok(deleted > 0)
262 })
263 }
264
265 pub fn delete_attachment_by_name(&self, task_id: &str, name: &str) -> Result<Option<String>> {
268 self.with_conn(|conn| {
269 let file_path: Option<String> = conn
271 .query_row(
272 "SELECT file_path FROM attachments WHERE task_id = ?1 AND name = ?2",
273 params![task_id, name],
274 |row| row.get(0),
275 )
276 .ok()
277 .flatten();
278
279 conn.execute(
281 "DELETE FROM attachments WHERE task_id = ?1 AND name = ?2",
282 params![task_id, name],
283 )?;
284
285 Ok(file_path)
286 })
287 }
288
289 pub fn delete_attachment_by_name_ex(
292 &self,
293 task_id: &str,
294 name: &str,
295 ) -> Result<(bool, Option<String>)> {
296 self.with_conn(|conn| {
297 let file_path: Option<String> = conn
299 .query_row(
300 "SELECT file_path FROM attachments WHERE task_id = ?1 AND name = ?2",
301 params![task_id, name],
302 |row| row.get(0),
303 )
304 .ok()
305 .flatten();
306
307 let rows_affected = conn.execute(
309 "DELETE FROM attachments WHERE task_id = ?1 AND name = ?2",
310 params![task_id, name],
311 )?;
312
313 Ok((rows_affected > 0, file_path))
314 })
315 }
316}