From c2398193760000f534b7f4147225e3732e2c2574 Mon Sep 17 00:00:00 2001 From: Nico Fricke Date: Tue, 4 Feb 2025 19:40:34 +0100 Subject: [PATCH] validate file version on upload --- casket-backend/src/files/upload.rs | 83 +++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/casket-backend/src/files/upload.rs b/casket-backend/src/files/upload.rs index 620dd7b..e586762 100644 --- a/casket-backend/src/files/upload.rs +++ b/casket-backend/src/files/upload.rs @@ -1,3 +1,4 @@ +use crate::db::repository::models::FileModel; use crate::db::repository::{insert, Repository}; use crate::errors::to_internal_error; use crate::files::PathInfo; @@ -17,6 +18,7 @@ use std::io::Error; use std::path::{Path, PathBuf}; use tokio::fs::{create_dir_all, File}; use tokio::io::AsyncWriteExt; +use tracing::debug; #[debug_handler] pub async fn handle_file_uploads( @@ -46,13 +48,17 @@ async fn handle_single_file_upload<'field_lifetime>( None | Some("") => Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST) .with_detail(errors::ERROR_DETAILS_NO_NAME_PROVIDED_MULTIPART_FIELD)), Some(field_name) => { - let path = PathBuf::from(field_name); - let path_info = PathInfo::build(&state.config.files.directory, user_id, &path)?; + let parsed = UploadParameter::from_string(field_name)?; + let path_info = PathInfo::build(&state.config.files.directory, user_id, &parsed.path)?; let lock_id = &build_lock_id(user_id, &path_info.relative_user_location); - if state - .get_lock() - .lock(lock_id).await - { + if state.get_lock().lock(lock_id).await { + validate_file_version( + state.get_repository(), + user_id, + &path_info.relative_user_location, + &parsed.version, + ) + .await?; let result = update_file(state, user_id, field, &path_info).await; state.get_lock().unlock(lock_id).await; result @@ -64,6 +70,71 @@ async fn handle_single_file_upload<'field_lifetime>( } } +async fn validate_file_version( + repo: &impl Repository, + user_id: &str, + path: &Path, + version: &u64, +) -> Result<(), ProblemDetails> { + match repo.get_file(user_id, &path.to_string_lossy()).await? { + None => validate_initial_version(version)?, + Some(file_model) => validate_version_increment(&file_model, version)?, + } + Ok(()) +} + +fn validate_version_increment(model: &FileModel, version: &u64) -> Result<(), ProblemDetails> { + if model.version + 1 != *version { + return Err( + ProblemDetails::from_status_code(StatusCode::CONFLICT).with_detail(format!( + "Version does not match. Provided {} is not one higher than existing {}", + version, model.version + )), + ); + } + Ok(()) +} + +fn validate_initial_version(version: &u64) -> Result<(), ProblemDetails> { + if *version != 0 { + return Err( + ProblemDetails::from_status_code(StatusCode::BAD_REQUEST).with_detail(format!( + "Version for new files must be 0. Provided version {}", + version + )), + ); + } + Ok(()) +} + +struct UploadParameter { + path: PathBuf, + version: u64, +} + +impl UploadParameter { + fn from_string(name: &str) -> Result { + let mut split = name.split(';'); + let path = split.next().ok_or_else(build_wrong_field_name_error)?; + let version: u64 = split + .next() + .ok_or_else(build_wrong_field_name_error)? + .parse() + .map_err(|err| { + debug!("Error while parsing version: {}", err); + ProblemDetails::from_status_code(StatusCode::BAD_REQUEST) + .with_detail("Version cannot be parsed as a number") + })?; + let path = PathBuf::from(path); + Ok(Self { path, version }) + } +} + +fn build_wrong_field_name_error() -> ProblemDetails { + ProblemDetails::from_status_code(StatusCode::BAD_REQUEST) + .with_detail("Multipart field name must have the following structure: {path_to_file};{version_number + 1}. Example: folder/nested/file.pdf;15") +} + async fn update_file<'field_lifetime>( state: &AppState, user_id: &str,