validate file version on upload

This commit is contained in:
Nico Fricke 2025-02-04 19:40:34 +01:00
parent 50d0f6dfa5
commit c239819376

View File

@ -1,3 +1,4 @@
use crate::db::repository::models::FileModel;
use crate::db::repository::{insert, Repository}; use crate::db::repository::{insert, Repository};
use crate::errors::to_internal_error; use crate::errors::to_internal_error;
use crate::files::PathInfo; use crate::files::PathInfo;
@ -17,6 +18,7 @@ use std::io::Error;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tokio::fs::{create_dir_all, File}; use tokio::fs::{create_dir_all, File};
use tokio::io::AsyncWriteExt; use tokio::io::AsyncWriteExt;
use tracing::debug;
#[debug_handler] #[debug_handler]
pub async fn handle_file_uploads( 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) None | Some("") => Err(ProblemDetails::from_status_code(StatusCode::BAD_REQUEST)
.with_detail(errors::ERROR_DETAILS_NO_NAME_PROVIDED_MULTIPART_FIELD)), .with_detail(errors::ERROR_DETAILS_NO_NAME_PROVIDED_MULTIPART_FIELD)),
Some(field_name) => { Some(field_name) => {
let path = PathBuf::from(field_name); let parsed = UploadParameter::from_string(field_name)?;
let path_info = PathInfo::build(&state.config.files.directory, user_id, &path)?; 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); let lock_id = &build_lock_id(user_id, &path_info.relative_user_location);
if state if state.get_lock().lock(lock_id).await {
.get_lock() validate_file_version(
.lock(lock_id).await state.get_repository(),
{ user_id,
&path_info.relative_user_location,
&parsed.version,
)
.await?;
let result = update_file(state, user_id, field, &path_info).await; let result = update_file(state, user_id, field, &path_info).await;
state.get_lock().unlock(lock_id).await; state.get_lock().unlock(lock_id).await;
result 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<Self, ProblemDetails> {
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>( async fn update_file<'field_lifetime>(
state: &AppState, state: &AppState,
user_id: &str, user_id: &str,