1use super::{now_ms, Database};
4use crate::types::{Attachment, AttachmentMeta};
5use anyhow::{anyhow, Result};
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(&self, task_id: &str, include_content: bool) -> Result<Vec<Attachment>> {
69 self.with_conn(|conn| {
70 let mut stmt = conn.prepare(
71 "SELECT task_id, order_index, name, mime_type, content, file_path, created_at
72 FROM attachments WHERE task_id = ?1 ORDER BY order_index, created_at",
73 )?;
74
75 let attachments = stmt
76 .query_map(params![task_id], |row| {
77 let task_id: String = row.get(0)?;
78 let order_index: i32 = row.get(1)?;
79 let name: String = row.get(2)?;
80 let mime_type: String = row.get(3)?;
81 let content: String = row.get(4)?;
82 let file_path: Option<String> = row.get(5)?;
83 let created_at: i64 = row.get(6)?;
84
85 Ok(Attachment {
86 task_id,
87 order_index,
88 name,
89 mime_type,
90 content: if include_content { content } else { String::new() },
91 file_path,
92 created_at,
93 })
94 })?
95 .filter_map(|r| r.ok())
96 .collect();
97
98 Ok(attachments)
99 })
100 }
101
102 pub fn get_attachments(&self, task_id: &str) -> Result<Vec<AttachmentMeta>> {
104 self.get_attachments_filtered(task_id, None, None)
105 }
106
107
108 pub fn get_attachments_filtered(
112 &self,
113 task_id: &str,
114 name_pattern: Option<&str>,
115 mime_pattern: Option<&str>,
116 ) -> Result<Vec<AttachmentMeta>> {
117 self.with_conn(|conn| {
118 let mut sql = String::from(
120 "SELECT task_id, order_index, name, mime_type, file_path, created_at
121 FROM attachments WHERE task_id = ?1"
122 );
123
124 let name_like = name_pattern.map(|p| {
126 p.replace('*', "%").replace('?', "_")
128 });
129
130 if name_like.is_some() {
131 sql.push_str(" AND name LIKE ?2 ESCAPE '\\'");
132 }
133
134 if mime_pattern.is_some() {
135 let idx = if name_like.is_some() { 3 } else { 2 };
136 sql.push_str(&format!(" AND mime_type LIKE ?{} ESCAPE '\\'", idx));
137 }
138
139 sql.push_str(" ORDER BY order_index, created_at");
140
141 let mut stmt = conn.prepare(&sql)?;
142
143 let attachments: Vec<AttachmentMeta> = match (&name_like, mime_pattern) {
145 (Some(name), Some(mime)) => {
146 let mime_like = format!("{}%", mime);
147 stmt.query_map(params![task_id, name, mime_like], |row| {
148 Self::map_attachment_meta(row)
149 })?
150 .filter_map(|r| r.ok())
151 .collect()
152 }
153 (Some(name), None) => {
154 stmt.query_map(params![task_id, name], |row| {
155 Self::map_attachment_meta(row)
156 })?
157 .filter_map(|r| r.ok())
158 .collect()
159 }
160 (None, Some(mime)) => {
161 let mime_like = format!("{}%", mime);
162 stmt.query_map(params![task_id, mime_like], |row| {
163 Self::map_attachment_meta(row)
164 })?
165 .filter_map(|r| r.ok())
166 .collect()
167 }
168 (None, None) => {
169 stmt.query_map(params![task_id], |row| {
170 Self::map_attachment_meta(row)
171 })?
172 .filter_map(|r| r.ok())
173 .collect()
174 }
175 };
176
177 Ok(attachments)
178 })
179 }
180
181 fn map_attachment_meta(row: &rusqlite::Row) -> rusqlite::Result<AttachmentMeta> {
183 Ok(AttachmentMeta {
184 task_id: row.get(0)?,
185 order_index: row.get(1)?,
186 name: row.get(2)?,
187 mime_type: row.get(3)?,
188 file_path: row.get(4)?,
189 created_at: row.get(5)?,
190 })
191 }
192
193 pub fn get_attachment(&self, task_id: &str, order_index: i32) -> Result<Option<Attachment>> {
197 self.with_conn(|conn| {
198 let mut stmt = conn.prepare(
199 "SELECT task_id, order_index, name, mime_type, content, file_path, created_at
200 FROM attachments WHERE task_id = ?1 AND order_index = ?2",
201 )?;
202
203 let result = stmt.query_row(params![task_id, order_index], |row| {
204 let task_id: String = row.get(0)?;
205 let order_index: i32 = row.get(1)?;
206 let name: String = row.get(2)?;
207 let mime_type: String = row.get(3)?;
208 let content: String = row.get(4)?;
209 let file_path: Option<String> = row.get(5)?;
210 let created_at: i64 = row.get(6)?;
211
212 Ok(Attachment {
213 task_id,
214 order_index,
215 name,
216 mime_type,
217 content,
218 file_path,
219 created_at,
220 })
221 });
222
223 match result {
224 Ok(attachment) => Ok(Some(attachment)),
225 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
226 Err(e) => Err(e.into()),
227 }
228 })
229 }
230
231 pub fn get_attachment_file_path(&self, task_id: &str, order_index: i32) -> Result<Option<String>> {
233 self.with_conn(|conn| {
234 let result = conn.query_row(
235 "SELECT file_path FROM attachments WHERE task_id = ?1 AND order_index = ?2",
236 params![task_id, order_index],
237 |row| row.get::<_, Option<String>>(0),
238 );
239
240 match result {
241 Ok(file_path) => Ok(file_path),
242 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
243 Err(e) => Err(e.into()),
244 }
245 })
246 }
247
248 pub fn delete_attachment(&self, task_id: &str, order_index: i32) -> Result<bool> {
250 self.with_conn(|conn| {
251 let deleted = conn.execute(
252 "DELETE FROM attachments WHERE task_id = ?1 AND order_index = ?2",
253 params![task_id, order_index],
254 )?;
255
256 Ok(deleted > 0)
257 })
258 }
259
260
261 pub fn delete_attachment_by_name(&self, task_id: &str, name: &str) -> Result<Option<String>> {
264 self.with_conn(|conn| {
265 let file_path: Option<String> = conn
267 .query_row(
268 "SELECT file_path FROM attachments WHERE task_id = ?1 AND name = ?2",
269 params![task_id, name],
270 |row| row.get(0),
271 )
272 .ok()
273 .flatten();
274
275 conn.execute(
277 "DELETE FROM attachments WHERE task_id = ?1 AND name = ?2",
278 params![task_id, name],
279 )?;
280
281 Ok(file_path)
282 })
283 }
284
285 pub fn delete_attachment_by_name_ex(&self, task_id: &str, name: &str) -> Result<(bool, Option<String>)> {
288 self.with_conn(|conn| {
289 let file_path: Option<String> = conn
291 .query_row(
292 "SELECT file_path FROM attachments WHERE task_id = ?1 AND name = ?2",
293 params![task_id, name],
294 |row| row.get(0),
295 )
296 .ok()
297 .flatten();
298
299 let rows_affected = conn.execute(
301 "DELETE FROM attachments WHERE task_id = ?1 AND name = ?2",
302 params![task_id, name],
303 )?;
304
305 Ok((rows_affected > 0, file_path))
306 })
307 }
308}