From 24c008b0de3b9e1207874cc78a97dc97ba431dce Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Fri, 12 May 2023 13:19:41 +0200 Subject: [PATCH] Add support for uploading media files to S3 --- Cargo.toml | 2 +- plume-models/src/medias.rs | 5 +- src/routes/medias.rs | 112 ++++++++++++++++++++++++++----------- src/routes/mod.rs | 7 ++- 4 files changed, 86 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d2fd4775..f5868e32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,7 +68,7 @@ ructe = "0.15.0" rsass = "0.26" [features] -default = ["postgres"] +default = ["postgres", "s3"] postgres = ["plume-models/postgres", "diesel/postgres"] sqlite = ["plume-models/sqlite", "diesel/sqlite"] debug-mailer = [] diff --git a/plume-models/src/medias.rs b/plume-models/src/medias.rs index 3dc22605..60f0d2aa 100644 --- a/plume-models/src/medias.rs +++ b/plume-models/src/medias.rs @@ -171,11 +171,12 @@ impl Media { pub fn delete(&self, conn: &Connection) -> Result<()> { if !self.is_remote { if CONFIG.s3.is_some() { + #[cfg(not(feature="s3"))] + unreachable!(); + #[cfg(feature = "s3")] CONFIG.s3.as_ref().unwrap().get_bucket() .delete_object_blocking(&self.file_path)?; - #[cfg(not(feature="s3"))] - unreachable!(); } else { fs::remove_file(self.file_path.as_str())?; } diff --git a/src/routes/medias.rs b/src/routes/medias.rs index 1981c542..b9a22dee 100644 --- a/src/routes/medias.rs +++ b/src/routes/medias.rs @@ -2,7 +2,7 @@ use crate::routes::{errors::ErrorPage, Page}; use crate::template_utils::{IntoContext, Ructe}; use guid_create::GUID; use multipart::server::{ - save::{SaveResult, SavedData}, + save::{SaveResult, SavedField, SavedData}, Multipart, }; use plume_models::{db_conn::DbConn, medias::*, users::User, Error, PlumeRocket, CONFIG}; @@ -55,41 +55,16 @@ pub fn upload( if let SaveResult::Full(entries) = Multipart::with_body(data.open(), boundary).save().temp() { let fields = entries.fields; - let filename = fields + let file = fields .get("file") .and_then(|v| v.iter().next()) - .ok_or(status::BadRequest(Some("No file uploaded")))? - .headers - .filename - .clone(); - // Remove extension if it contains something else than just letters and numbers - let ext = filename - .and_then(|f| { - f.rsplit('.') - .next() - .and_then(|ext| { - if ext.chars().any(|c| !c.is_alphanumeric()) { - None - } else { - Some(ext.to_lowercase()) - } - }) - .map(|ext| format!(".{}", ext)) - }) - .unwrap_or_default(); - let dest = format!("{}/{}{}", CONFIG.media_directory, GUID::rand(), ext); + .ok_or(status::BadRequest(Some("No file uploaded")))?; - match fields["file"][0].data { - SavedData::Bytes(ref bytes) => fs::write(&dest, bytes) - .map_err(|_| status::BadRequest(Some("Couldn't save upload")))?, - SavedData::File(ref path, _) => { - fs::copy(path, &dest) - .map_err(|_| status::BadRequest(Some("Couldn't copy upload")))?; - } - _ => { - return Ok(Redirect::to(uri!(new))); - } - } + let file_path = match save_uploaded_file(file) { + Ok(Some(file_path)) => file_path, + Ok(None) => return Ok(Redirect::to(uri!(new))), + Err(_) => return Err(status::BadRequest(Some("Couldn't save uploaded media: {}"))), + }; let has_cw = !read(&fields["cw"][0].data) .map(|cw| cw.is_empty()) @@ -97,7 +72,7 @@ pub fn upload( let media = Media::insert( &conn, NewMedia { - file_path: dest, + file_path, alt_text: read(&fields["alt"][0].data)?, is_remote: false, remote_url: None, @@ -117,6 +92,75 @@ pub fn upload( } } +fn save_uploaded_file(file: &SavedField) -> Result, plume_models::Error> { + // Remove extension if it contains something else than just letters and numbers + let ext = file + .headers + .filename + .as_ref() + .and_then(|f| { + f.rsplit('.') + .next() + .and_then(|ext| { + if ext.chars().any(|c| !c.is_alphanumeric()) { + None + } else { + Some(ext.to_lowercase()) + } + }) + .map(|ext| format!(".{}", ext)) + }) + .unwrap_or_default(); + + if CONFIG.s3.is_some() { + #[cfg(not(feature="s3"))] + unreachable!(); + + #[cfg(feature="s3")] + { + use std::borrow::Cow; + + let dest = format!("static/media/{}{}", GUID::rand(), ext); + + let bytes = match file.data { + SavedData::Bytes(ref bytes) => Cow::from(bytes), + SavedData::File(ref path, _) => Cow::from(fs::read(path)?), + _ => { + return Ok(None); + } + }; + + let bucket = CONFIG.s3.as_ref().unwrap().get_bucket(); + match &file.headers.content_type { + Some(ct) => { + bucket.put_object_with_content_type_blocking(&dest, &bytes, &ct.to_string())?; + } + None => { + bucket.put_object_blocking(&dest, &bytes)?; + } + } + + Ok(Some(dest)) + } + } else { + let dest = format!("{}/{}{}", CONFIG.media_directory, GUID::rand(), ext); + + match file.data { + SavedData::Bytes(ref bytes) => { + fs::write(&dest, bytes)?; + } + SavedData::File(ref path, _) => { + fs::copy(path, &dest)?; + } + _ => { + return Ok(None); + } + } + + Ok(Some(dest)) + } +} + fn read(data: &SavedData) -> Result> { if let SavedData::Text(s) = data { Ok(s.clone()) diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 08fd8fa1..ac016b9d 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -264,6 +264,9 @@ pub fn plume_static_files(file: PathBuf, build_id: &RawStr) -> Option")] pub fn plume_media_files(file: PathBuf) -> Option { if CONFIG.s3.is_some() { + #[cfg(not(feature="s3"))] + unreachable!(); + #[cfg(feature="s3")] { let ct = file.extension() @@ -271,15 +274,13 @@ pub fn plume_media_files(file: PathBuf) -> Option { .unwrap_or(ContentType::Binary); let data = CONFIG.s3.as_ref().unwrap().get_bucket() - .get_object_blocking(format!("plume-media/{}", file.to_string_lossy())).ok()?; + .get_object_blocking(format!("static/media/{}", file.to_string_lossy())).ok()?; Some(CachedFile { inner: FileKind::S3 ( data.to_vec(), ct), cache_control: CacheControl(vec![CacheDirective::MaxAge(60 * 60 * 24 * 30)]), }) } - #[cfg(not(feature="s3"))] - unreachable!(); } else { NamedFile::open(Path::new(&CONFIG.media_directory).join(file)) .ok()