diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.rs | 21 | ||||
| -rw-r--r-- | src/media.rs | 193 | ||||
| -rw-r--r-- | src/micropub.rs | 33 | 
3 files changed, 153 insertions, 94 deletions
| diff --git a/src/main.rs b/src/main.rs index 2c81ccd..a3a7f76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,9 +15,12 @@ mod oauth;  pub struct SiteConfig {      bind: String, +    media_url: String,      token_endpoint: String,      s3_bucket: String, -    media_url: String, + +    default_width: u32, +    default_height: u32,  }  impl SiteConfig { @@ -25,6 +28,11 @@ impl SiteConfig {          &self.bind      } +    /// Base URL for serving files +    pub fn media_url(&self) -> &str { +        &self.media_url +    } +      /// The URI to use to validate an access token.      pub fn token_endpoint(&self) -> &str {          &self.token_endpoint @@ -35,9 +43,12 @@ impl SiteConfig {          &self.s3_bucket      } -    /// Base URL for S3 bucket assets. -    pub fn media_url(&self) -> &str { -        &self.media_url +    pub fn default_width(&self) -> u32 { +        self.default_width +    } + +    pub fn default_height(&self) -> u32 { +        self.default_height      }  } @@ -51,6 +62,8 @@ async fn main() -> std::io::Result<()> {          s3_bucket: std::env::var("S3_BUCKET").expect("Expected S3_BUCKET env var"),          media_url: std::env::var("MEDIA_URL").expect("Expected MEDIA_URL env var"),          token_endpoint: std::env::var("TOKEN_ENDPOINT").expect("Expected TOKEN_ENDPOINT env var"), +        default_width: std::env::var("DEFAULT_WIDTH").ok().and_then(|v| v.parse().ok()).unwrap_or(1000), +        default_height: std::env::var("DEFAULT_HEIGHT").ok().and_then(|v| v.parse().ok()).unwrap_or(0),      };      let bind = site_config.bind().to_string(); diff --git a/src/media.rs b/src/media.rs index 953dd15..0704f79 100644 --- a/src/media.rs +++ b/src/media.rs @@ -1,5 +1,4 @@ -use actix_web::client::{Client, ClientResponse}; -use actix_web::error::{ErrorBadRequest, ErrorInternalServerError}; +use actix_web::error::{ErrorBadRequest, ErrorNotFound, ErrorInternalServerError};  use actix_web::http::header;  use actix_web::{web, Error, HttpRequest, HttpResponse}; @@ -7,8 +6,38 @@ use image::imageops::FilterType;  use image::GenericImageView;  use image::ImageFormat; +use futures::TryFutureExt; +use tokio::io::AsyncReadExt; + +use rusoto_s3::{HeadObjectRequest, GetObjectRequest, S3Client, S3}; +  use crate::SiteConfig; +/// Build an HttpResponse for an AWS response +macro_rules! response_for { +    ($resp:expr) => { +        { +            let mut client_resp = HttpResponse::Ok(); + +            // This will be the default cache-control header if the object doesn't have its own. +            client_resp.set(header::CacheControl(vec![header::CacheDirective::MaxAge( +                31557600u32, +            )])); + +            // Copy all of the relevant S3 headers. +            $resp.cache_control.map(|v| client_resp.set_header(header::CACHE_CONTROL, v)); +            $resp.content_disposition.map(|v| client_resp.set_header(header::CONTENT_DISPOSITION, v)); +            $resp.content_encoding.map(|v| client_resp.set_header(header::CONTENT_ENCODING, v)); +            $resp.content_language.map(|v| client_resp.set_header(header::CONTENT_LANGUAGE, v)); +            $resp.content_type.map(|v| client_resp.set_header(header::CONTENT_TYPE, v)); +            $resp.e_tag.map(|v| client_resp.set_header(header::ETAG, v)); +            $resp.last_modified.map(|v| client_resp.set_header(header::LAST_MODIFIED, v)); + +            client_resp +        } +    }; +} +  pub fn configure(cfg: &mut web::ServiceConfig) {      cfg.service(          web::resource("/media/photo/{width:\\d+}x{height:\\d+}/{filename}") @@ -17,68 +46,49 @@ pub fn configure(cfg: &mut web::ServiceConfig) {      cfg.service(          web::resource("/media/{type}/{filename:.+}")              .route(web::get().to(serve_file)) -            .route(web::head().to(serve_file)), +            .route(web::head().to(head_file)),      );  } -async fn serve_photo( +async fn head_file(      req: HttpRequest,      config: web::Data<SiteConfig>, -    client: web::Data<Client>, +    s3_client: web::Data<S3Client>,  ) -> Result<HttpResponse, Error> { -    let width = req -        .match_info() -        .get("width") -        .ok_or(ErrorBadRequest("Bad URI")) -        .and_then(|v| v.parse().map_err(|_| ErrorBadRequest("Bad URI")))?; -    let height = req + +    // Get the path paramaters +    let media_type = req          .match_info() -        .get("height") -        .ok_or(ErrorBadRequest("Bad URI")) -        .and_then(|v| v.parse().map_err(|_| ErrorBadRequest("Bad URI")))?; +        .get("type") +        .ok_or(ErrorBadRequest("Bad URI"))?;      let filename = req          .match_info()          .get("filename")          .ok_or(ErrorBadRequest("Bad URI"))?; -    let new_url = format!("{}/photo/{}", config.media_url(), filename); - -    let forwarded_req = client.request_from(new_url, req.head()); -    let forwarded_req = if let Some(addr) = req.head().peer_addr { -        forwarded_req.header("x-forwarded-for", format!("{}", addr.ip())) -    } else { -        forwarded_req -    }; - -    let mut res = forwarded_req.send().await.map_err(Error::from)?; - -    // Check response code -    if !res.status().is_success() { -        return forward_response(res).await; -    } - -    // Get the payload, at at least 20 MB of it... -    let data = res.body().limit(20971520).await?; - -    // Resize the image -    let (mime, new_data) = web::block(move || scale_image(data.as_ref(), width, height)).await -        .map_err(|e| ErrorInternalServerError(e))?; - -    // Send the new image to the client. -    let mut client_resp = HttpResponse::build(res.status()); -    client_resp.set(header::CacheControl(vec![header::CacheDirective::MaxAge( -        86400u32, -    )])); -    client_resp.set_header(header::CONTENT_TYPE, mime); - -    Ok(client_resp.body(new_data)) +    // Construct an S3 key +    let key = format!("{}/{}", media_type, filename); +    let resp = s3_client.head_object(HeadObjectRequest { +        bucket: config.s3_bucket().to_owned(), +        key, +        ..Default::default() +    }) +    .map_err(|e| ErrorInternalServerError(e)) +    .await?; + +    let mut client_resp = response_for!(resp); +    // TODO: trick actix into returning the content-length. +    Ok(client_resp.finish())  } +  async fn serve_file(      req: HttpRequest,      config: web::Data<SiteConfig>, -    client: web::Data<Client>, +    s3_client: web::Data<S3Client>,  ) -> Result<HttpResponse, Error> { + +    // Get the path paramaters      let media_type = req          .match_info()          .get("type") @@ -88,38 +98,76 @@ async fn serve_file(          .get("filename")          .ok_or(ErrorBadRequest("Bad URI"))?; -    let new_url = format!("{}/{}/{}", config.media_url(), media_type, filename); - -    let forwarded_req = client.request_from(new_url, req.head()).no_decompress(); +    // Construct an S3 key +    let key = format!("{}/{}", media_type, filename); +    let resp = s3_client.get_object(GetObjectRequest { +        bucket: config.s3_bucket().to_owned(), +        key, +        ..Default::default() +    }) +    .map_err(|e| ErrorInternalServerError(e)) +    .await?; + +    // If there is no payload, return a 404. +    let data = resp.body.ok_or(ErrorNotFound("Not found"))?; + +    let mut client_resp = response_for!(resp); +    Ok(client_resp.streaming(data)) +} -    let forwarded_req = if let Some(addr) = req.head().peer_addr { -        forwarded_req.header("x-forwarded-for", format!("{}", addr.ip())) -    } else { -        forwarded_req -    }; +async fn serve_photo( +    req: HttpRequest, +    config: web::Data<SiteConfig>, +    s3_client: web::Data<S3Client>, +) -> Result<HttpResponse, Error> { +    let width = req +        .match_info() +        .get("width") +        .ok_or(ErrorBadRequest("Bad URI")) +        .and_then(|v| v.parse().map_err(|_| ErrorBadRequest("Bad URI")))?; +    let height = req +        .match_info() +        .get("height") +        .ok_or(ErrorBadRequest("Bad URI")) +        .and_then(|v| v.parse().map_err(|_| ErrorBadRequest("Bad URI")))?; +    let filename = req +        .match_info() +        .get("filename") +        .ok_or(ErrorBadRequest("Bad URI"))?; -    let res = forwarded_req.send().await.map_err(Error::from)?; +    let key = format!("photo/{}", filename); +    let resp = s3_client.get_object(GetObjectRequest { +        bucket: config.s3_bucket().to_owned(), +        key, +        ..Default::default() +    }) +    .map_err(|e| ErrorInternalServerError(e)) +    .await?; + +    let mut data = Vec::new(); +    resp.body +        .ok_or(ErrorNotFound("Not found"))? +        .into_async_read() +        .read_to_end(&mut data) +        .await?; -    forward_response(res).await -} +    // Resize the image +    let (mime, new_data) = web::block(move || scale_image(data.as_ref(), width, height)) +        .await +        .map_err(|e| ErrorInternalServerError(e))?; -async fn forward_response<S>(mut res: ClientResponse<S>) -> Result<HttpResponse, Error> -where -    S: futures::Stream<Item = std::result::Result<bytes::Bytes, actix_web::error::PayloadError>> -        + std::marker::Unpin, -{ -    let mut client_resp = HttpResponse::build(res.status()); - -    // Remove `Connection` as per -    // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection#Directives -    for (header_name, header_value) in res.headers().iter().filter(|(h, _)| *h != "connection") { -        client_resp.header(header_name.clone(), header_value.clone()); -    } +    // Send the new image to the client. +    let mut client_resp = response_for!(resp); +    client_resp.set_header(header::CONTENT_TYPE, mime); -    Ok(client_resp.body(res.body().limit(2147483648).await?)) +    Ok(client_resp.body(new_data))  } -fn scale_image(data: &[u8], width: u32, height: u32) -> Result<(&'static str, Vec<u8>), image::ImageError> { +fn scale_image( +    data: &[u8], +    width: u32, +    height: u32, +) -> Result<(&'static str, Vec<u8>), image::ImageError> {      // Determine the image format      let fmt = image::guess_format(data)?; @@ -143,8 +191,7 @@ fn scale_image(data: &[u8], width: u32, height: u32) -> Result<(&'static str, Ve      };      let mut new_data = Vec::new(); -    scaled -        .write_to(&mut new_data, fmt)?; // ImageOutputFormat::Jpeg(128)) +    scaled.write_to(&mut new_data, fmt)?;      Ok((mime_for_image(fmt), new_data))  } diff --git a/src/micropub.rs b/src/micropub.rs index 1afe7ba..e420188 100644 --- a/src/micropub.rs +++ b/src/micropub.rs @@ -74,17 +74,13 @@ fn random_id() -> String {      format!("{}-{}", time_part, random_part)  } -pub async fn handle_upload(req: HttpRequest, mut payload: Multipart) -> HttpResponse { -    let site = req -        .app_data::<web::Data<SiteConfig>>() -        .expect("Missing SiteConfig?"); -    let s3_client = req -        .app_data::<web::Data<S3Client>>() -        .expect("Missing S3Client?"); -    let verification_service = req -        .app_data::<web::Data<oauth::VerificationService>>() -        .expect("Missing VerificationService?"); - +pub async fn handle_upload( +    req: HttpRequest, +    mut payload: Multipart, +    site: web::Data<SiteConfig>, +    s3_client: web::Data<S3Client>, +    verification_service: web::Data<oauth::VerificationService>, +) -> HttpResponse {      let auth_header = match req          .headers()          .get(header::AUTHORIZATION) @@ -121,12 +117,16 @@ pub async fn handle_upload(req: HttpRequest, mut payload: Multipart) -> HttpResp          // This will be the key in S3.          let key = match suffix { -            Some(ext) => format!("{}/{}{}{}", classification, random_id(), sep, ext), -            None => format!("{}/{}", classification, random_id()), +            Some(ext) => format!("{}{}{}", random_id(), sep, ext), +            None => format!("{}", random_id()),          };          // This will be the publicly accessible URL for the file. -        let url = format!("{}/{}", site.media_url, key); +        let url = if classification == "photo" { +            format!("{}/photo/{}x{}/{}", site.media_url(), site.default_width(), site.default_height(), key) +        } else { +            format!("{}/{}/{}", site.media_url(), classification, key) +        };          let mut metadata: HashMap<String, String> = HashMap::new();          metadata.insert( @@ -146,7 +146,7 @@ pub async fn handle_upload(req: HttpRequest, mut payload: Multipart) -> HttpResp          let put_request = PutObjectRequest {              bucket: site.s3_bucket().to_owned(), -            key, +            key: format!("{}/{}", classification, key),              body: Some(body.into()),              metadata: Some(metadata),              content_type: Some(content_type.to_string()), @@ -156,8 +156,7 @@ pub async fn handle_upload(req: HttpRequest, mut payload: Multipart) -> HttpResp          match s3_client.put_object(put_request).await {              Ok(_) => {                  return HttpResponse::Created() -                    // Note: header must have a big L -                    .header("Location", url) +                    .header(header::LOCATION, url)                      .finish();              }              Err(e) => return HttpResponse::InternalServerError().body(format!("{}", e)), | 
