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 attachment_type: String,
16 name: String,
17 content: String,
18 mime_type: Option<String>,
19 file_path: Option<String>,
20 ) -> Result<i32> {
21 let now = now_ms();
22 let mime_type = mime_type.unwrap_or_else(|| "text/plain".to_string());
23
24 self.with_conn_mut(|conn| {
25 let tx = conn.transaction()?;
26
27 let exists: bool = tx
29 .query_row(
30 "SELECT 1 FROM tasks WHERE id = ?1",
31 params![task_id],
32 |_| Ok(true),
33 )
34 .unwrap_or(false);
35
36 if !exists {
37 return Err(anyhow!("Task not found"));
38 }
39
40 let max_seq: Option<i32> = tx.query_row(
42 "SELECT MAX(sequence) FROM attachments WHERE task_id = ?1 AND attachment_type = ?2",
43 params![task_id, attachment_type],
44 |row| row.get(0),
45 )?;
46 let sequence = max_seq.unwrap_or(-1) + 1;
47
48 tx.execute(
49 "INSERT INTO attachments (task_id, attachment_type, sequence, name, mime_type, content, file_path, created_at)
50 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
51 params![
52 task_id,
53 attachment_type,
54 sequence,
55 name,
56 mime_type,
57 content,
58 file_path,
59 now,
60 ],
61 )?;
62
63 tx.commit()?;
64 Ok(sequence)
65 })
66 }
67
68 pub fn get_attachments_full(
71 &self,
72 task_id: &str,
73 include_content: bool,
74 ) -> Result<Vec<Attachment>> {
75 self.with_conn(|conn| {
76 let mut stmt = conn.prepare(
77 "SELECT task_id, attachment_type, sequence, name, mime_type, content, file_path, created_at
78 FROM attachments WHERE task_id = ?1 ORDER BY attachment_type, sequence",
79 )?;
80
81 let attachments = stmt
82 .query_map(params![task_id], |row| {
83 let task_id: String = row.get(0)?;
84 let attachment_type: String = row.get(1)?;
85 let sequence: i32 = row.get(2)?;
86 let name: String = row.get(3)?;
87 let mime_type: String = row.get(4)?;
88 let content: String = row.get(5)?;
89 let file_path: Option<String> = row.get(6)?;
90 let created_at: i64 = row.get(7)?;
91
92 Ok(Attachment {
93 task_id,
94 attachment_type,
95 sequence,
96 name,
97 mime_type,
98 content: if include_content {
99 content
100 } else {
101 String::new()
102 },
103 file_path,
104 created_at,
105 })
106 })?
107 .filter_map(|r| r.ok())
108 .collect();
109
110 Ok(attachments)
111 })
112 }
113
114 pub fn get_attachments(&self, task_id: &str) -> Result<Vec<AttachmentMeta>> {
116 self.get_attachments_filtered(task_id, None, None)
117 }
118
119 pub fn get_attachments_filtered(
123 &self,
124 task_id: &str,
125 type_pattern: Option<&str>,
126 mime_pattern: Option<&str>,
127 ) -> Result<Vec<AttachmentMeta>> {
128 self.with_conn(|conn| {
129 let mut sql = String::from(
131 "SELECT task_id, attachment_type, sequence, name, mime_type, file_path, created_at
132 FROM attachments WHERE task_id = ?1",
133 );
134
135 let type_like = type_pattern.map(|p| {
137 p.replace('*', "%").replace('?', "_")
139 });
140
141 if type_like.is_some() {
142 sql.push_str(" AND attachment_type LIKE ?2 ESCAPE '\\'");
143 }
144
145 if mime_pattern.is_some() {
146 let idx = if type_like.is_some() { 3 } else { 2 };
147 sql.push_str(&format!(" AND mime_type LIKE ?{} ESCAPE '\\'", idx));
148 }
149
150 sql.push_str(" ORDER BY attachment_type, sequence");
151
152 let mut stmt = conn.prepare(&sql)?;
153
154 let attachments: Vec<AttachmentMeta> = match (&type_like, mime_pattern) {
156 (Some(type_pat), Some(mime)) => {
157 let mime_like = format!("{}%", mime);
158 stmt.query_map(params![task_id, type_pat, mime_like], |row| {
159 Self::map_attachment_meta(row)
160 })?
161 .filter_map(|r| r.ok())
162 .collect()
163 }
164 (Some(type_pat), None) => stmt
165 .query_map(params![task_id, type_pat], Self::map_attachment_meta)?
166 .filter_map(|r| r.ok())
167 .collect(),
168 (None, Some(mime)) => {
169 let mime_like = format!("{}%", mime);
170 stmt.query_map(params![task_id, mime_like], |row| {
171 Self::map_attachment_meta(row)
172 })?
173 .filter_map(|r| r.ok())
174 .collect()
175 }
176 (None, None) => stmt
177 .query_map(params![task_id], Self::map_attachment_meta)?
178 .filter_map(|r| r.ok())
179 .collect(),
180 };
181
182 Ok(attachments)
183 })
184 }
185
186 fn map_attachment_meta(row: &rusqlite::Row) -> rusqlite::Result<AttachmentMeta> {
188 Ok(AttachmentMeta {
189 task_id: row.get(0)?,
190 attachment_type: row.get(1)?,
191 sequence: row.get(2)?,
192 name: row.get(3)?,
193 mime_type: row.get(4)?,
194 file_path: row.get(5)?,
195 created_at: row.get(6)?,
196 })
197 }
198
199 pub fn get_attachment(
203 &self,
204 task_id: &str,
205 attachment_type: &str,
206 sequence: i32,
207 ) -> Result<Option<Attachment>> {
208 self.with_conn(|conn| {
209 let mut stmt = conn.prepare(
210 "SELECT task_id, attachment_type, sequence, name, mime_type, content, file_path, created_at
211 FROM attachments WHERE task_id = ?1 AND attachment_type = ?2 AND sequence = ?3",
212 )?;
213
214 let result = stmt.query_row(params![task_id, attachment_type, sequence], |row| {
215 let task_id: String = row.get(0)?;
216 let attachment_type: String = row.get(1)?;
217 let sequence: i32 = row.get(2)?;
218 let name: String = row.get(3)?;
219 let mime_type: String = row.get(4)?;
220 let content: String = row.get(5)?;
221 let file_path: Option<String> = row.get(6)?;
222 let created_at: i64 = row.get(7)?;
223
224 Ok(Attachment {
225 task_id,
226 attachment_type,
227 sequence,
228 name,
229 mime_type,
230 content,
231 file_path,
232 created_at,
233 })
234 });
235
236 match result {
237 Ok(attachment) => Ok(Some(attachment)),
238 Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
239 Err(e) => Err(e.into()),
240 }
241 })
242 }
243
244 pub fn get_attachment_file_paths_by_type(
246 &self,
247 task_id: &str,
248 attachment_type: &str,
249 ) -> Result<Vec<String>> {
250 self.with_conn(|conn| {
251 let mut stmt = conn.prepare(
252 "SELECT file_path FROM attachments WHERE task_id = ?1 AND attachment_type = ?2 AND file_path IS NOT NULL",
253 )?;
254
255 let paths: Vec<String> = stmt
256 .query_map(params![task_id, attachment_type], |row| row.get(0))?
257 .filter_map(|r| r.ok())
258 .collect();
259
260 Ok(paths)
261 })
262 }
263
264 pub fn delete_attachment(
266 &self,
267 task_id: &str,
268 attachment_type: &str,
269 sequence: i32,
270 ) -> Result<bool> {
271 self.with_conn(|conn| {
272 let deleted = conn.execute(
273 "DELETE FROM attachments WHERE task_id = ?1 AND attachment_type = ?2 AND sequence = ?3",
274 params![task_id, attachment_type, sequence],
275 )?;
276
277 Ok(deleted > 0)
278 })
279 }
280
281 pub fn delete_attachments_by_type(
284 &self,
285 task_id: &str,
286 attachment_type: &str,
287 ) -> Result<Vec<String>> {
288 self.with_conn(|conn| {
289 let file_paths = {
291 let mut stmt = conn.prepare(
292 "SELECT file_path FROM attachments WHERE task_id = ?1 AND attachment_type = ?2 AND file_path IS NOT NULL",
293 )?;
294 stmt.query_map(params![task_id, attachment_type], |row| row.get(0))?
295 .filter_map(|r| r.ok())
296 .collect::<Vec<String>>()
297 };
298
299 conn.execute(
301 "DELETE FROM attachments WHERE task_id = ?1 AND attachment_type = ?2",
302 params![task_id, attachment_type],
303 )?;
304
305 Ok(file_paths)
306 })
307 }
308
309 pub fn delete_attachments_by_type_ex(
312 &self,
313 task_id: &str,
314 attachment_type: &str,
315 ) -> Result<(usize, Vec<String>)> {
316 self.with_conn(|conn| {
317 let file_paths = {
319 let mut stmt = conn.prepare(
320 "SELECT file_path FROM attachments WHERE task_id = ?1 AND attachment_type = ?2 AND file_path IS NOT NULL",
321 )?;
322 stmt.query_map(params![task_id, attachment_type], |row| row.get(0))?
323 .filter_map(|r| r.ok())
324 .collect::<Vec<String>>()
325 };
326
327 let deleted = conn.execute(
329 "DELETE FROM attachments WHERE task_id = ?1 AND attachment_type = ?2",
330 params![task_id, attachment_type],
331 )?;
332
333 Ok((deleted, file_paths))
334 })
335 }
336}