From 36b909632556948e22ed806a16252ee7bf8ababa Mon Sep 17 00:00:00 2001 From: neri Date: Thu, 30 Jun 2022 01:04:03 +0200 Subject: [PATCH] add cache headers for file downloads --- src/download.rs | 61 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/src/download.rs b/src/download.rs index 4e6ba95..b7cc1ad 100644 --- a/src/download.rs +++ b/src/download.rs @@ -1,17 +1,19 @@ -use std::{path::PathBuf, str::FromStr}; +use std::{path::PathBuf, str::FromStr, time::SystemTime}; use actix_files::NamedFile; use actix_web::{ error, http::header::{ - Accept, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue, - Header, + Accept, CacheControl, CacheDirective, Charset, ContentDisposition, DispositionParam, + DispositionType, Expires, ExtendedValue, Header, HeaderValue, HttpDate, TryIntoHeaderValue, + ACCEPT, CACHE_CONTROL, EXPIRES, VARY, }, web, Error, HttpRequest, HttpResponse, }; use mime::{Mime, TEXT_HTML}; use sqlx::postgres::PgPool; use std::path::Path; +use time::OffsetDateTime; use tokio::fs; use url::Url; @@ -35,16 +37,19 @@ pub async fn download( config: web::Data, ) -> Result { let id = req.match_info().query("id"); - let (file_id, file_name, file_kind, delete) = load_file_info(id, &db).await?; + let (file_id, file_name, valid_till, file_kind, delete) = load_file_info(id, &db).await?; let mut path = config.files_dir.clone(); path.push(&file_id); let file_mime = get_content_type(&path); - let response = match get_view_type(&req, &file_kind, &file_mime, &path, delete).await { - ViewType::Raw => build_file_response(false, &file_name, path, file_mime, &req), - ViewType::Download => build_file_response(true, &file_name, path, file_mime, &req), + let mut response = match get_view_type(&req, &file_kind, &file_mime, &path, delete).await { + ViewType::Raw => build_file_response(false, &file_name, path, file_mime, &req).await, + ViewType::Download => build_file_response(true, &file_name, path, file_mime, &req).await, ViewType::Html => build_text_response(&path).await, - }; + }?; + + insert_cache_headers(&mut response, valid_till); + if delete { deleter::delete_by_id(&db, &file_id, &config.files_dir) .await @@ -53,15 +58,16 @@ pub async fn download( error::ErrorInternalServerError("could not delete file") })?; } - response + + Ok(response) } async fn load_file_info( id: &str, db: &web::Data>, -) -> Result<(String, String, String, bool), Error> { +) -> Result<(String, String, OffsetDateTime, String, bool), Error> { sqlx::query_as( - "SELECT file_id, file_name, kind, delete_on_download from files WHERE file_id = $1", + "SELECT file_id, file_name, valid_till, kind, delete_on_download from files WHERE file_id = $1", ) .bind(id) .fetch_optional(db.as_ref()) @@ -147,7 +153,7 @@ async fn build_text_response(path: &Path) -> Result { .body(html)) } -fn build_file_response( +async fn build_file_response( download: bool, file_name: &str, path: PathBuf, @@ -169,6 +175,7 @@ fn build_file_response( })? .set_content_type(content_type) .set_content_disposition(content_disposition); + Ok(file.into_response(req)) } @@ -183,3 +190,33 @@ fn get_disposition_params(filename: &str) -> Vec { } parameters } + +fn insert_cache_headers(response: &mut HttpResponse, valid_till: OffsetDateTime) { + if response.status().is_success() { + let valid_duration = valid_till - OffsetDateTime::now_utc(); + let valid_cache_seconds = valid_duration.whole_seconds().clamp(0, u32::MAX as i64) as u32; + response.headers_mut().insert( + CACHE_CONTROL, + CacheControl(vec![ + CacheDirective::Public, + CacheDirective::MustRevalidate, + CacheDirective::MaxAge(valid_cache_seconds), // todo: expiry in seconds + CacheDirective::NoTransform, + CacheDirective::Extension("immutable".to_owned(), None), + ]) + .try_into_value() + .unwrap(), + ); + response.headers_mut().insert( + EXPIRES, + Expires(HttpDate::from( + SystemTime::now() + std::time::Duration::from_secs(valid_cache_seconds.into()), + )) + .try_into_value() + .unwrap(), + ); + } + response + .headers_mut() + .insert(VARY, HeaderValue::from_name(ACCEPT)); +}