rust_web_server/attachment/
mod.rs1#[cfg(test)]
33mod tests;
34
35use crate::request::Request;
36use crate::storage::{Storage, StorageError};
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct Attachment {
41 pub key: String,
43 pub filename: String,
45 pub content_type: String,
46 pub size: u64,
48}
49
50impl Attachment {
51 fn new_key(prefix: &str, filename: &str) -> String {
52 let safe: String = filename
53 .rsplit(['/', '\\'])
54 .next()
55 .unwrap_or("")
56 .chars()
57 .map(|c| if c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_') { c } else { '_' })
58 .collect();
59 let safe = safe.trim_start_matches('.');
60 let safe = if safe.is_empty() { "file" } else { safe };
61 let id = unique_id();
62 let prefix = prefix.trim_matches('/');
63 if prefix.is_empty() {
64 format!("{id}/{safe}")
65 } else {
66 format!("{prefix}/{id}/{safe}")
67 }
68 }
69
70 pub fn store(storage: &dyn Storage, prefix: &str, filename: &str, content_type: &str, data: &[u8]) -> Result<Attachment, StorageError> {
72 let key = storage.put(&Self::new_key(prefix, filename), data, content_type)?;
73 Ok(Attachment { key, filename: filename.to_string(), content_type: content_type.to_string(), size: data.len() as u64 })
74 }
75
76 pub fn store_stream(storage: &dyn Storage, prefix: &str, filename: &str, content_type: &str, reader: &mut dyn std::io::Read) -> Result<Attachment, StorageError> {
79 let mut counting = Counting { inner: reader, count: 0 };
80 let key = storage.put_stream(&Self::new_key(prefix, filename), &mut counting, content_type)?;
81 Ok(Attachment { key, filename: filename.to_string(), content_type: content_type.to_string(), size: counting.count })
82 }
83
84 pub fn from_multipart(storage: &dyn Storage, prefix: &str, request: &Request, field: &str) -> Result<Option<Attachment>, StorageError> {
89 use crate::body::multipart_form_data::FormMultipartData;
90 use crate::header::content_disposition::ContentDisposition;
91
92 let content_type = request.header("content-type").map(|h| h.value.to_string()).unwrap_or_default();
93 if !content_type.to_ascii_lowercase().starts_with("multipart/form-data") {
94 return Err(StorageError::new("the request body isn't multipart/form-data"));
95 }
96 let boundary = FormMultipartData::extract_boundary(&content_type).map_err(StorageError::new)?;
97 let parts = FormMultipartData::parse(&request.body, boundary).map_err(StorageError::new)?;
98 for part in parts {
99 let Some(disposition) = part.header("content-disposition") else { continue };
100 let Ok(disposition) = ContentDisposition::parse(&disposition.value) else {
101 continue;
102 };
103 if disposition.field_name.as_deref() != Some(field) {
104 continue;
105 }
106 let filename = disposition.file_name.unwrap_or_default();
108 if filename.is_empty() {
109 return Ok(None);
110 }
111 let part_type = part.header("content-type").map(|h| h.value.to_string()).unwrap_or_else(|| "application/octet-stream".to_string());
112 return Self::store(storage, prefix, &filename, &part_type, &part.body).map(Some);
113 }
114 Ok(None)
115 }
116
117 pub fn url(&self, storage: &dyn Storage) -> String {
119 storage.url(&self.key)
120 }
121
122 pub fn read(&self, storage: &dyn Storage) -> Result<Vec<u8>, StorageError> {
124 storage.get(&self.key)
125 }
126
127 pub fn delete(&self, storage: &dyn Storage) -> Result<(), StorageError> {
129 storage.delete(&self.key)
130 }
131
132 pub fn encode(&self) -> String {
134 use crate::router::build_path;
135 let size = self.size.to_string();
137 let query = build_path("/", &[("key", &self.key), ("name", &self.filename), ("type", &self.content_type), ("size", &size)]).unwrap_or_default();
138 query.trim_start_matches("/?").to_string()
139 }
140
141 pub fn decode(text: &str) -> Option<Attachment> {
144 let mut attachment = Attachment { key: String::new(), filename: String::new(), content_type: String::new(), size: 0 };
145 for pair in text.split('&').filter(|p| !p.is_empty()) {
146 let (name, value) = pair.split_once('=')?;
147 let value = percent_decode(value)?;
148 match name {
149 "key" => attachment.key = value,
150 "name" => attachment.filename = value,
151 "type" => attachment.content_type = value,
152 "size" => attachment.size = value.parse().ok()?,
153 _ => {}
154 }
155 }
156 (!attachment.key.is_empty()).then_some(attachment)
157 }
158}
159
160fn unique_id() -> String {
163 use std::hash::{BuildHasher, Hasher};
164 use std::sync::atomic::{AtomicU64, Ordering};
165 use std::sync::OnceLock;
166 static SEED: OnceLock<std::collections::hash_map::RandomState> = OnceLock::new();
167 static COUNTER: AtomicU64 = AtomicU64::new(0);
168 let nanos = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_nanos() as u64).unwrap_or(0);
169 let mut hasher = SEED.get_or_init(Default::default).build_hasher();
170 hasher.write_u64(COUNTER.fetch_add(1, Ordering::Relaxed));
171 hasher.write_u64(nanos);
172 format!("{nanos:016x}{:016x}", hasher.finish())
173}
174
175fn percent_decode(text: &str) -> Option<String> {
176 let bytes = text.as_bytes();
177 let mut out = Vec::with_capacity(bytes.len());
178 let mut i = 0;
179 while i < bytes.len() {
180 if bytes[i] == b'%' {
181 out.push(u8::from_str_radix(text.get(i + 1..i + 3)?, 16).ok()?);
182 i += 3;
183 } else {
184 out.push(bytes[i]);
185 i += 1;
186 }
187 }
188 String::from_utf8(out).ok()
189}
190
191struct Counting<'a> {
192 inner: &'a mut dyn std::io::Read,
193 count: u64,
194}
195
196impl std::io::Read for Counting<'_> {
197 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
198 let n = self.inner.read(buf)?;
199 self.count += n as u64;
200 Ok(n)
201 }
202}
203
204#[cfg(any(feature = "model-sqlite", feature = "model-postgres", feature = "model-mysql"))]
205mod column {
206 use super::Attachment;
207 use crate::model::{DbError, FromColumn, ToColumn, Value};
208
209 impl FromColumn for Attachment {
210 fn from_column(v: Value) -> Result<Self, DbError> {
211 match v {
212 Value::Text(text) => Attachment::decode(&text).ok_or_else(|| DbError::new(format!("not an attachment: {text:?}"))),
213 Value::Null => Err(DbError::new("unexpected NULL for Attachment")),
214 other => Err(DbError::new(format!("cannot convert {other:?} to Attachment"))),
215 }
216 }
217 }
218
219 impl ToColumn for Attachment {
220 fn to_column(&self) -> Value {
221 Value::Text(self.encode())
222 }
223 }
224}