diff --git a/moneymgr_backend/Cargo.lock b/moneymgr_backend/Cargo.lock index d978de7..fd064bb 100644 --- a/moneymgr_backend/Cargo.lock +++ b/moneymgr_backend/Cargo.lock @@ -2098,6 +2098,7 @@ dependencies = [ "diesel_migrations", "env_logger", "futures-util", + "httpdate", "ipnet", "jwt-simple", "lazy-regex", diff --git a/moneymgr_backend/Cargo.toml b/moneymgr_backend/Cargo.toml index 05fca71..1240ce1 100644 --- a/moneymgr_backend/Cargo.toml +++ b/moneymgr_backend/Cargo.toml @@ -30,3 +30,4 @@ jwt-simple = { version = "0.12.11", default-features = false, features = ["pure- mime_guess = "2.0.5" rust-embed = { version = "8.6.0" } sha2 = "0.10.8" +httpdate = "1.0.3" diff --git a/moneymgr_backend/src/controllers/files_controller.rs b/moneymgr_backend/src/controllers/files_controller.rs index 42fa637..8b6a4b0 100644 --- a/moneymgr_backend/src/controllers/files_controller.rs +++ b/moneymgr_backend/src/controllers/files_controller.rs @@ -1,8 +1,14 @@ use crate::controllers::HttpResult; use crate::extractors::auth_extractor::AuthExtractor; use crate::extractors::file_extractor::FileExtractor; +use crate::extractors::file_id_extractor::FileIdExtractor; +use crate::models::files::File; use crate::services::files_service; -use actix_web::HttpResponse; +use crate::utils::time_utils; +use actix_web::http::header; +use actix_web::{HttpRequest, HttpResponse}; +use std::ops::Add; +use std::time::Duration; /// Upload a new file pub async fn upload(auth: AuthExtractor, file: FileExtractor) -> HttpResult { @@ -16,3 +22,39 @@ pub async fn upload(auth: AuthExtractor, file: FileExtractor) -> HttpResult { Ok(HttpResponse::Ok().json(file)) } + +/// Download an uploaded file +pub async fn download(req: HttpRequest, file_extractor: FileIdExtractor) -> HttpResult { + serve_file(req, file_extractor.as_ref()).await +} + +/// Serve a file, returning 304 status code if the requested file already exists +pub async fn serve_file(req: HttpRequest, file: &File) -> HttpResult { + // Check if the browser already knows the etag + if let Some(c) = req.headers().get(header::IF_NONE_MATCH) { + if c.to_str().unwrap_or("") == file.sha512.as_str() { + return Ok(HttpResponse::NotModified().finish()); + } + } + + // Check if the browser already knows the file by date + if let Some(c) = req.headers().get(header::IF_MODIFIED_SINCE) { + let date_str = c.to_str().unwrap_or(""); + if let Ok(date) = httpdate::parse_http_date(date_str) { + if date.add(Duration::from_secs(1)) + >= time_utils::unix_to_system_time(file.time_create as u64) + { + return Ok(HttpResponse::NotModified().finish()); + } + } + } + + Ok(HttpResponse::Ok() + .content_type(file.mime_type.as_str()) + .insert_header(("etag", file.sha512.as_str())) + .insert_header(( + "last-modified", + time_utils::unix_to_http_date(file.time_create as u64), + )) + .body(files_service::get_file_content(file).await?)) +} diff --git a/moneymgr_backend/src/controllers/tokens_controller.rs b/moneymgr_backend/src/controllers/tokens_controller.rs index 21cc5f5..6c759b3 100644 --- a/moneymgr_backend/src/controllers/tokens_controller.rs +++ b/moneymgr_backend/src/controllers/tokens_controller.rs @@ -15,7 +15,7 @@ pub struct CreateTokenBody { right_account: bool, right_movement: bool, right_inbox: bool, - right_attachment: bool, + right_file: bool, right_auth: bool, } @@ -59,7 +59,7 @@ pub async fn create(auth: AuthExtractor, req: web::Json) -> Htt right_account: req.right_account, right_movement: req.right_movement, right_inbox: req.right_inbox, - right_file: req.right_attachment, + right_file: req.right_file, right_auth: req.right_auth, }) .await?; diff --git a/moneymgr_backend/src/extractors/file_id_extractor.rs b/moneymgr_backend/src/extractors/file_id_extractor.rs new file mode 100644 index 0000000..6ca161e --- /dev/null +++ b/moneymgr_backend/src/extractors/file_id_extractor.rs @@ -0,0 +1,61 @@ +use crate::extractors::auth_extractor::AuthExtractor; +use crate::models::files::{File, FileID}; +use crate::services::files_service; +use actix_web::dev::Payload; +use actix_web::{FromRequest, HttpRequest}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct FileIdInPath { + file_id: FileID, +} + +#[derive(thiserror::Error, Debug)] +enum FileIdExtractorError { + #[error("Current user does not own the file!")] + UserDoesNotOwnFile, +} + +pub struct FileIdExtractor(File); + +impl FileIdExtractor { + pub async fn load_file_from_path(auth: &AuthExtractor, id: FileID) -> anyhow::Result { + let file = files_service::get_file_with_id(id)?; + + if file.user_id() != auth.user_id() { + return Err(FileIdExtractorError::UserDoesNotOwnFile.into()); + } + + Ok(Self(file)) + } +} + +impl FromRequest for FileIdExtractor { + type Error = actix_web::Error; + type Future = futures_util::future::LocalBoxFuture<'static, Result>; + + fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { + let req = req.clone(); + Box::pin(async move { + let auth = AuthExtractor::extract(&req).await?; + + let file_id = + actix_web::web::Path::::from_request(&req, &mut Payload::None) + .await? + .file_id; + + Self::load_file_from_path(&auth, file_id) + .await + .map_err(|e| { + log::error!("Failed to extract file ID from URL! {}", e); + actix_web::error::ErrorNotFound("Could not fetch file information!") + }) + }) + } +} + +impl AsRef for FileIdExtractor { + fn as_ref(&self) -> &File { + &self.0 + } +} diff --git a/moneymgr_backend/src/extractors/mod.rs b/moneymgr_backend/src/extractors/mod.rs index 0272db3..1f63609 100644 --- a/moneymgr_backend/src/extractors/mod.rs +++ b/moneymgr_backend/src/extractors/mod.rs @@ -1,4 +1,5 @@ pub mod account_extractor; pub mod auth_extractor; pub mod file_extractor; +pub mod file_id_extractor; pub mod money_session; diff --git a/moneymgr_backend/src/main.rs b/moneymgr_backend/src/main.rs index 7d196c1..91de894 100644 --- a/moneymgr_backend/src/main.rs +++ b/moneymgr_backend/src/main.rs @@ -121,6 +121,11 @@ async fn main() -> std::io::Result<()> { ) // Files controller .route("/api/file", web::post().to(files_controller::upload)) + .route( + "/api/file/{file_id}", + web::get().to(files_controller::download), + ) + // TODO Delete file // Static assets .route("/", web::get().to(static_controller::root_index)) .route( diff --git a/moneymgr_backend/src/models/files.rs b/moneymgr_backend/src/models/files.rs index c77afbb..1df9643 100644 --- a/moneymgr_backend/src/models/files.rs +++ b/moneymgr_backend/src/models/files.rs @@ -25,7 +25,7 @@ impl File { } pub fn user_id(&self) -> UserID { - UserID(self.id) + UserID(self.user_id) } } diff --git a/moneymgr_backend/src/utils/time_utils.rs b/moneymgr_backend/src/utils/time_utils.rs index 7c035af..9c2a441 100644 --- a/moneymgr_backend/src/utils/time_utils.rs +++ b/moneymgr_backend/src/utils/time_utils.rs @@ -1,6 +1,6 @@ //! # Time utilities -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// Get the current time since epoch pub fn time() -> u64 { @@ -9,3 +9,13 @@ pub fn time() -> u64 { .unwrap() .as_secs() } + +/// Format UNIX time to HTTP date +pub fn unix_to_system_time(time: u64) -> SystemTime { + UNIX_EPOCH + Duration::from_secs(time) +} + +/// Format UNIX time to HTTP date +pub fn unix_to_http_date(time: u64) -> String { + httpdate::fmt_http_date(unix_to_system_time(time)) +}