Skip to content

Commit 3038784

Browse files
committed
feat: preserve original filename with short hash suffix on upload
1 parent 8bab5cd commit 3038784

2 files changed

Lines changed: 86 additions & 7 deletions

File tree

agents/src/routes.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,11 @@ async fn handle_post(
138138
.map_err(|_| {
139139
AppError::BadRequest("Expected multipart/form-data with a 'file' field".into())
140140
})?;
141-
let pdf_bytes = extract_multipart(multipart).await?;
142-
let result = state.storage.upload(pdf_bytes).await?;
141+
let upload = extract_multipart(multipart).await?;
142+
let result = state
143+
.storage
144+
.upload(upload.bytes, upload.filename.as_deref())
145+
.await?;
143146

144147
Ok(Json(AgentResponse::new(
145148
&result.presigned_url,
@@ -179,18 +182,27 @@ async fn handle_post(
179182
}
180183
}
181184

182-
async fn extract_multipart(mut multipart: Multipart) -> Result<Vec<u8>, AppError> {
185+
struct MultipartUpload {
186+
bytes: Vec<u8>,
187+
filename: Option<String>,
188+
}
189+
190+
async fn extract_multipart(mut multipart: Multipart) -> Result<MultipartUpload, AppError> {
183191
while let Some(field) = multipart
184192
.next_field()
185193
.await
186194
.map_err(|_| AppError::BadRequest("Failed to read multipart field".into()))?
187195
{
188196
if field.name() == Some("file") {
197+
let filename = field.file_name().map(String::from);
189198
let bytes = field
190199
.bytes()
191200
.await
192201
.map_err(|_| AppError::BadRequest("Failed to read file".into()))?;
193-
return Ok(bytes.to_vec());
202+
return Ok(MultipartUpload {
203+
bytes: bytes.to_vec(),
204+
filename,
205+
});
194206
}
195207
}
196208

agents/src/storage.rs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,17 @@ impl Storage {
4242
}
4343
}
4444

45-
pub async fn upload(&self, bytes: Vec<u8>) -> Result<UploadResult, AppError> {
45+
pub async fn upload(
46+
&self,
47+
bytes: Vec<u8>,
48+
original_filename: Option<&str>,
49+
) -> Result<UploadResult, AppError> {
4650
if bytes.len() < 5 || &bytes[..5] != b"%PDF-" {
4751
return Err(AppError::BadRequest("Not a valid PDF file".into()));
4852
}
4953

50-
let id = Uuid::new_v4().to_string();
51-
let key = format!("uploads/{id}.pdf");
54+
let hash = &Uuid::new_v4().to_string()[..8];
55+
let key = build_key(original_filename, hash);
5256

5357
self.client
5458
.put_object()
@@ -78,3 +82,66 @@ impl Storage {
7882
Ok(UploadResult { presigned_url })
7983
}
8084
}
85+
86+
fn build_key(original_filename: Option<&str>, hash: &str) -> String {
87+
let stem_and_ext = original_filename
88+
.filter(|name| !name.is_empty())
89+
.map(|name| {
90+
let name = name
91+
.rsplit('/')
92+
.next()
93+
.unwrap_or(name)
94+
.rsplit('\\')
95+
.next()
96+
.unwrap_or(name);
97+
98+
match name.rsplit_once('.') {
99+
Some((stem, ext)) => (stem.to_string(), format!(".{ext}")),
100+
None => (name.to_string(), String::new()),
101+
}
102+
});
103+
104+
match stem_and_ext {
105+
Some((stem, ext)) => format!("uploads/{stem}-{hash}{ext}"),
106+
None => format!("uploads/{hash}.pdf"),
107+
}
108+
}
109+
110+
#[cfg(test)]
111+
mod tests {
112+
use super::*;
113+
114+
#[test]
115+
fn test_build_key_with_filename() {
116+
assert_eq!(
117+
build_key(Some("invoice.pdf"), "a1b2c3d4"),
118+
"uploads/invoice-a1b2c3d4.pdf"
119+
);
120+
}
121+
122+
#[test]
123+
fn test_build_key_with_path() {
124+
assert_eq!(
125+
build_key(Some("path/to/report.pdf"), "a1b2c3d4"),
126+
"uploads/report-a1b2c3d4.pdf"
127+
);
128+
}
129+
130+
#[test]
131+
fn test_build_key_without_extension() {
132+
assert_eq!(
133+
build_key(Some("document"), "a1b2c3d4"),
134+
"uploads/document-a1b2c3d4"
135+
);
136+
}
137+
138+
#[test]
139+
fn test_build_key_none() {
140+
assert_eq!(build_key(None, "a1b2c3d4"), "uploads/a1b2c3d4.pdf");
141+
}
142+
143+
#[test]
144+
fn test_build_key_empty() {
145+
assert_eq!(build_key(Some(""), "a1b2c3d4"), "uploads/a1b2c3d4.pdf");
146+
}
147+
}

0 commit comments

Comments
 (0)