1use std::collections::HashMap;
9
10use crate::types::Attachment;
11
12const SELECT_COLS: &str = "event_id, att_index, hash, key, nonce, extension, name, url, \
13 path, size, img_meta, downloaded, webxdc_topic, group_id, original_hash, fallback_urls";
14
15fn row_to_attachment(row: &rusqlite::Row) -> rusqlite::Result<(String, Attachment)> {
18 let event_id: String = row.get(0)?;
19 let img_meta_json: Option<String> = row.get(10)?;
20 let att = Attachment {
21 id: row.get(2)?,
22 key: row.get(3)?,
23 nonce: row.get(4)?,
24 extension: row.get(5)?,
25 name: row.get(6)?,
26 url: row.get(7)?,
27 path: row.get(8)?,
28 size: row.get::<_, i64>(9)? as u64,
29 img_meta: img_meta_json.and_then(|j| serde_json::from_str(&j).ok()),
30 downloading: false,
31 downloaded: row.get::<_, i64>(11)? != 0,
32 webxdc_topic: row.get(12)?,
33 group_id: row.get(13)?,
34 original_hash: row.get(14)?,
35 fallback_urls: row
36 .get::<_, String>(15)?
37 .split_whitespace()
38 .map(|s| s.to_string())
39 .collect(),
40 };
41 Ok((event_id, att))
42}
43
44pub fn insert_attachment_rows(conn: &rusqlite::Connection, event_id: &str, attachments: &[Attachment]) -> Result<(), String> {
54 if attachments.is_empty() {
55 return Ok(());
56 }
57 let mut stmt = conn.prepare_cached(
60 "INSERT INTO attachments (event_id, att_index, hash, key, nonce, extension, name, url, \
61 path, size, img_meta, downloaded, webxdc_topic, group_id, original_hash, fallback_urls) \
62 VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16) \
63 ON CONFLICT(event_id, att_index) DO UPDATE SET \
64 key=excluded.key, nonce=excluded.nonce, extension=excluded.extension, \
65 name=excluded.name, url=excluded.url, size=excluded.size, img_meta=excluded.img_meta, \
66 webxdc_topic=excluded.webxdc_topic, group_id=excluded.group_id, \
67 original_hash=excluded.original_hash, \
68 fallback_urls=CASE WHEN excluded.fallback_urls='' THEN fallback_urls ELSE excluded.fallback_urls END, \
69 downloaded=MAX(downloaded, excluded.downloaded), \
70 hash=CASE WHEN excluded.downloaded=1 THEN excluded.hash ELSE hash END, \
71 path=CASE WHEN excluded.downloaded=1 THEN excluded.path ELSE path END",
72 ).map_err(|e| format!("prepare insert attachment: {e}"))?;
73 for (i, a) in attachments.iter().enumerate() {
74 let img_meta_json = a.img_meta.as_ref().and_then(|m| serde_json::to_string(m).ok());
75 stmt.execute(
76 rusqlite::params![
77 event_id, i as i64, a.id, a.key, a.nonce, a.extension, a.name, a.url,
78 a.path, a.size as i64, img_meta_json, a.downloaded as i64,
79 a.webxdc_topic, a.group_id, a.original_hash,
80 a.fallback_urls.join(" "),
81 ],
82 ).map_err(|e| format!("insert attachment: {e}"))?;
83 }
84 Ok(())
85}
86
87pub fn get_attachments_for_events(event_ids: &[String]) -> Result<HashMap<String, Vec<Attachment>>, String> {
90 let mut out: HashMap<String, Vec<Attachment>> = HashMap::new();
91 if event_ids.is_empty() {
92 return Ok(out);
93 }
94 let conn = super::get_db_connection_guard_static()?;
95 let placeholders = event_ids.iter().map(|_| "?").collect::<Vec<_>>().join(",");
96 let sql = format!(
97 "SELECT {SELECT_COLS} FROM attachments WHERE event_id IN ({placeholders}) ORDER BY event_id, att_index"
98 );
99 let mut stmt = conn.prepare(&sql).map_err(|e| format!("prepare get_attachments: {e}"))?;
100 let params = rusqlite::params_from_iter(event_ids.iter());
101 let rows = stmt.query_map(params, row_to_attachment)
102 .map_err(|e| format!("query get_attachments: {e}"))?;
103 for r in rows.flatten() {
104 out.entry(r.0).or_default().push(r.1);
105 }
106 Ok(out)
107}
108
109pub fn get_attachments_for_event(event_id: &str) -> Result<Vec<Attachment>, String> {
111 let map = get_attachments_for_events(std::slice::from_ref(&event_id.to_string()))?;
112 Ok(map.into_values().next().unwrap_or_default())
113}
114
115pub fn set_attachment_downloaded(event_id: &str, hash: &str, downloaded: bool, path: &str) -> Result<(), String> {
118 let conn = super::get_write_connection_guard_static()?;
119 conn.execute(
120 "UPDATE attachments SET downloaded=?1, path=?2 WHERE event_id=?3 AND hash=?4",
121 rusqlite::params![downloaded as i64, path, event_id, hash],
122 ).map_err(|e| format!("set_attachment_downloaded: {e}"))?;
123 Ok(())
124}
125
126pub fn backfill_downloaded_by_hash(hash: &str, path: &str, exclude_event_id: &str) -> Result<Vec<String>, String> {
130 let conn = super::get_write_connection_guard_static()?;
131 let affected: Vec<String> = {
132 let mut stmt = conn.prepare(
133 "SELECT DISTINCT event_id FROM attachments WHERE hash=?1 AND event_id!=?2 AND downloaded=0"
134 ).map_err(|e| format!("prepare backfill_by_hash: {e}"))?;
135 let rows = stmt.query_map(rusqlite::params![hash, exclude_event_id], |r| r.get::<_, String>(0))
136 .map_err(|e| format!("query backfill_by_hash: {e}"))?;
137 rows.flatten().collect()
138 };
139 conn.execute(
140 "UPDATE attachments SET downloaded=1, path=?1 WHERE hash=?2 AND event_id!=?3 AND downloaded=0",
141 rusqlite::params![path, hash, exclude_event_id],
142 ).map_err(|e| format!("backfill_by_hash update: {e}"))?;
143 Ok(affected)
144}
145
146pub fn downloaded_attachment_paths() -> Result<Vec<(String, String, String)>, String> {
150 let conn = super::get_db_connection_guard_static()?;
151 let mut stmt = conn.prepare(
152 "SELECT event_id, hash, path FROM attachments WHERE downloaded=1 AND path!=''"
153 ).map_err(|e| format!("prepare downloaded_paths: {e}"))?;
154 let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?, r.get::<_, String>(2)?)))
155 .map_err(|e| format!("query downloaded_paths: {e}"))?;
156 Ok(rows.flatten().collect())
157}
158
159pub fn clear_attachment_download(event_id: &str, hash: &str) -> Result<(), String> {
161 let conn = super::get_write_connection_guard_static()?;
162 conn.execute(
163 "UPDATE attachments SET downloaded=0, path='' WHERE event_id=?1 AND hash=?2",
164 rusqlite::params![event_id, hash],
165 ).map_err(|e| format!("clear_attachment_download: {e}"))?;
166 Ok(())
167}
168
169pub fn rewrite_downloaded_paths(old_prefixes: &[String], new_dir: &std::path::Path) -> Result<Vec<String>, String> {
173 let conn = super::get_write_connection_guard_static()?;
174 let rows: Vec<(i64, String, String)> = {
175 let mut stmt = conn.prepare("SELECT id, event_id, path FROM attachments WHERE downloaded=1 AND path!=''")
176 .map_err(|e| format!("prepare rewrite_paths: {e}"))?;
177 let mapped = stmt.query_map([], |r| Ok((r.get::<_, i64>(0)?, r.get::<_, String>(1)?, r.get::<_, String>(2)?)))
178 .map_err(|e| format!("query rewrite_paths: {e}"))?;
179 mapped.flatten().collect()
180 };
181 let mut affected = Vec::new();
182 for (rowid, event_id, path) in rows {
183 if !old_prefixes.iter().any(|p| path.starts_with(p.as_str())) {
184 continue;
185 }
186 let Some(name) = std::path::Path::new(&path).file_name() else { continue };
187 let new_path = new_dir.join(name);
188 let res = if new_path.exists() {
189 conn.execute("UPDATE attachments SET path=?1 WHERE id=?2",
190 rusqlite::params![new_path.to_string_lossy().to_string(), rowid])
191 } else {
192 conn.execute("UPDATE attachments SET downloaded=0, path='' WHERE id=?1", rusqlite::params![rowid])
193 };
194 if res.is_ok() {
195 affected.push(event_id);
196 }
197 }
198 Ok(affected)
199}