1use crate::blobstore::BlobStore;
9use adminx_core::attach::{Attachment, Attachments, UploadedFile};
10use adminx_core::storage::{
11 storage, FilterClause, FilterOp, QueryOptions, StorageError,
12};
13use async_trait::async_trait;
14use serde_json::{Map, Value};
15
16pub const TABLE: &str = "adminx_attachments";
18
19const LIST_CAP: u64 = 500;
22
23pub struct AttachmentStore {
25 blobs: Box<dyn BlobStore>,
26}
27
28impl AttachmentStore {
29 pub fn new(blobs: Box<dyn BlobStore>) -> Self {
30 Self { blobs }
31 }
32
33 async fn rows(
35 &self,
36 owner_type: &str,
37 owner_id: &str,
38 field: Option<&str>,
39 ) -> Result<Vec<Value>, StorageError> {
40 let mut filters = vec![
41 FilterClause {
42 field: "owner_type".into(),
43 op: FilterOp::Eq,
44 value: owner_type.to_string(),
45 },
46 FilterClause {
47 field: "owner_id".into(),
48 op: FilterOp::Eq,
49 value: owner_id.to_string(),
50 },
51 ];
52 if let Some(f) = field {
53 filters.push(FilterClause {
54 field: "field".into(),
55 op: FilterOp::Eq,
56 value: f.to_string(),
57 });
58 }
59 let opts = QueryOptions {
60 page: 1,
61 per_page: LIST_CAP,
62 sort_by: Some("id".to_string()),
63 sort_desc: false,
64 filters,
65 };
66 Ok(storage().list(TABLE, &opts).await?.rows)
67 }
68
69 async fn remove_row(&self, row: &Value) -> Result<(), StorageError> {
73 if let Some(key) = row.get("storage_key").and_then(|v| v.as_str()) {
74 self.blobs.delete(key).await?;
75 }
76 if let Some(id) = row_id(row) {
77 storage().delete(TABLE, "id", &id, false).await?;
78 }
79 Ok(())
80 }
81}
82
83#[async_trait]
84impl Attachments for AttachmentStore {
85 async fn put(
86 &self,
87 owner_type: &str,
88 owner_id: &str,
89 field: &str,
90 file: UploadedFile,
91 ) -> Result<Attachment, StorageError> {
92 for row in self.rows(owner_type, owner_id, Some(field)).await? {
95 self.remove_row(&row).await?;
96 }
97
98 let storage_key = format!(
101 "{owner_type}/{owner_id}/{field}/{}",
102 nanoid::nanoid!()
103 );
104 self.blobs.put(&storage_key, &file.bytes).await?;
105
106 let byte_size = file.bytes.len() as u64;
107 let mut record = Map::new();
108 record.insert("owner_type".into(), Value::String(owner_type.into()));
109 record.insert("owner_id".into(), Value::String(owner_id.into()));
110 record.insert("field".into(), Value::String(field.into()));
111 record.insert("filename".into(), Value::String(file.filename.clone()));
112 record.insert("content_type".into(), Value::String(file.content_type.clone()));
113 record.insert("byte_size".into(), Value::Number(byte_size.into()));
114 record.insert("storage_key".into(), Value::String(storage_key.clone()));
115 record.insert(
116 "created_at".into(),
117 Value::String(chrono::Utc::now().to_rfc3339()),
118 );
119
120 if let Err(e) = storage().create(TABLE, record).await {
122 let _ = self.blobs.delete(&storage_key).await;
123 return Err(e);
124 }
125
126 Ok(Attachment {
127 field: field.into(),
128 filename: file.filename,
129 content_type: file.content_type,
130 byte_size,
131 storage_key,
132 })
133 }
134
135 async fn get(
136 &self,
137 owner_type: &str,
138 owner_id: &str,
139 field: &str,
140 ) -> Result<Option<Attachment>, StorageError> {
141 Ok(self
142 .rows(owner_type, owner_id, Some(field))
143 .await?
144 .first()
145 .map(to_attachment))
146 }
147
148 async fn list(
149 &self,
150 owner_type: &str,
151 owner_id: &str,
152 ) -> Result<Vec<Attachment>, StorageError> {
153 Ok(self
154 .rows(owner_type, owner_id, None)
155 .await?
156 .iter()
157 .map(to_attachment)
158 .collect())
159 }
160
161 async fn read(&self, storage_key: &str) -> Result<Vec<u8>, StorageError> {
162 self.blobs.get(storage_key).await
163 }
164
165 async fn delete(
166 &self,
167 owner_type: &str,
168 owner_id: &str,
169 field: &str,
170 ) -> Result<(), StorageError> {
171 for row in self.rows(owner_type, owner_id, Some(field)).await? {
172 self.remove_row(&row).await?;
173 }
174 Ok(())
175 }
176
177 async fn delete_all(&self, owner_type: &str, owner_id: &str) -> Result<(), StorageError> {
178 for row in self.rows(owner_type, owner_id, None).await? {
179 self.remove_row(&row).await?;
180 }
181 Ok(())
182 }
183}
184
185fn to_attachment(row: &Value) -> Attachment {
187 let s = |k: &str| row.get(k).and_then(|v| v.as_str()).unwrap_or("").to_string();
188 Attachment {
189 field: s("field"),
190 filename: s("filename"),
191 content_type: s("content_type"),
192 byte_size: row
194 .get("byte_size")
195 .and_then(|v| v.as_u64().or_else(|| v.as_str().and_then(|s| s.parse().ok())))
196 .unwrap_or(0),
197 storage_key: s("storage_key"),
198 }
199}
200
201fn row_id(row: &Value) -> Option<String> {
203 match row.get("id") {
204 Some(Value::Number(n)) => Some(n.to_string()),
205 Some(Value::String(s)) => Some(s.clone()),
206 _ => None,
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use serde_json::json;
214
215 #[test]
216 fn row_maps_to_attachment_with_either_size_type() {
217 let typed = json!({
218 "field": "avatar", "filename": "a.png", "content_type": "image/png",
219 "byte_size": 1234, "storage_key": "posts/1/avatar/x"
220 });
221 let a = to_attachment(&typed);
222 assert_eq!(a.byte_size, 1234);
223 assert_eq!(a.filename, "a.png");
224
225 let stringy = json!({ "byte_size": "1234", "storage_key": "k",
227 "field": "f", "filename": "n", "content_type": "t" });
228 assert_eq!(to_attachment(&stringy).byte_size, 1234);
229 }
230
231 #[test]
232 fn row_id_survives_int_or_string_pk() {
233 assert_eq!(row_id(&json!({"id": 5})).as_deref(), Some("5"));
234 assert_eq!(row_id(&json!({"id": "abc"})).as_deref(), Some("abc"));
235 assert_eq!(row_id(&json!({})), None);
236 }
237}