From de075981ec66a6110e33354398e7eedb269a1eb3 Mon Sep 17 00:00:00 2001 From: thevickypedia Date: Fri, 15 Mar 2024 20:22:30 -0500 Subject: [PATCH] Include an option to rename secure files Setup validations in both server and client --- src/routes/fileio.rs | 157 +++++++++++++++++++++++++++++++++------ src/templates/listing.rs | 93 +++++++++++++++++------ 2 files changed, 203 insertions(+), 47 deletions(-) diff --git a/src/routes/fileio.rs b/src/routes/fileio.rs index 7b416a9..8f3ebb0 100644 --- a/src/routes/fileio.rs +++ b/src/routes/fileio.rs @@ -1,4 +1,4 @@ -use std::fs::{remove_dir_all, remove_file}; +use std::fs; use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -10,14 +10,15 @@ use serde::Deserialize; use crate::{constant, routes, squire}; -/// Struct to represent the payload data with both the URL locator and path locator +/// Struct to represent the payload data with the URL locator and path locator and the new name for the file. #[derive(Debug, Deserialize)] struct Payload { url_locator: Option, path_locator: Option, + new_name: Option } -/// Extracts the path the file/directory that has to be deleted from the payload received. +/// Extracts the path the file/directory that has to be modified from the payload received. /// /// # Arguments /// @@ -30,7 +31,7 @@ struct Payload { /// /// * `Ok(PathBuf)` - If the extraction was successful and the path exists in the server. /// * `Err(String)` - If the extraction has failed or if the path doesn't exist in the server. -fn extract_media_path(payload: web::Json, media_source: &Path) -> Result { +fn extract_media_path(payload: &web::Json, media_source: &Path) -> Result { let url_locator = payload.url_locator.as_deref(); let path_locator = payload.path_locator.as_deref(); if let (Some(url_str), Some(path_str)) = (url_locator, path_locator) { @@ -65,9 +66,10 @@ fn extract_media_path(payload: web::Json, media_source: &Path) -> Resul /// /// # Returns /// -/// * `200` - HttpResponse with a `session_token` and redirect URL to the `/home` entrypoint. +/// * `200` - Blank HttpResponse to indicate that the request was successful. /// * `400` - HttpResponse with an error message for invalid action or incorrect payload. /// * `401` - HttpResponse with an error message for failed authentication. +/// * `500` - HttpResponse with an error message for failed delete/rename. #[post("/edit")] pub async fn edit(request: HttpRequest, payload: web::Json, @@ -82,7 +84,7 @@ pub async fn edit(request: HttpRequest, } let (_host, _last_accessed) = squire::custom::log_connection(&request, &session); log::debug!("{}", auth_response.detail); - let extracted = extract_media_path(payload, &config.media_source); + let extracted = extract_media_path(&payload, &config.media_source); // todo: styling of the pop up is very basic let media_path: PathBuf = match extracted { Ok(path) => { @@ -103,26 +105,15 @@ pub async fn edit(request: HttpRequest, } if let Some(edit_action) = request.headers().get("edit-action") { let action = edit_action.to_str().unwrap(); + log::info!("{} requested to {} {:?}", &auth_response.username, action, &media_path); return if action == "delete" { - log::info!("{} requested to delete {:?}", &auth_response.username, &media_path); - if media_path.is_file() { - if let Err(error) = remove_file(media_path) { - let reason = format!("Error deleting file: {}", error); - HttpResponse::InternalServerError().body(reason) - } else { - HttpResponse::Ok().finish() - } - } else if media_path.is_dir() { - if let Err(error) = remove_dir_all(media_path) { - let reason = format!("Error deleting directory: {}", error); - HttpResponse::InternalServerError().body(reason) - } else { - HttpResponse::Ok().finish() - } + return delete(media_path); + } else if action == "rename" { + let new_name_str = payload.new_name.as_deref(); + if let Some(new_name) = new_name_str { + return rename(media_path, new_name.trim()); } else { - let reason = format!("{:?} was neither a file nor a directory", media_path); - log::warn!("{}", reason); - HttpResponse::BadRequest().body(reason) + HttpResponse::BadRequest().body("New name is missing!") } } else { log::warn!("Unsupported action: {} requested to {} {:?}", &auth_response.username, action, &media_path); @@ -132,3 +123,121 @@ pub async fn edit(request: HttpRequest, log::warn!("No action received for: {:?}", media_path); HttpResponse::BadRequest().body("No action received!") } + +/// Checks if the new filename is valid with multiple conditions. +/// +/// # Arguments +/// +/// * `old_filepath` - PathBuf object to the file that has to be renamed. +/// * `new_name` - New name for the file. +/// +/// ## See Also +/// +/// - `Condition 1` - Validate if the new filename is the same as old. +/// - `Condition 2` - Validate if the new filename starts or ends with `.` or `_` +/// - `Condition 3` - Validate if the new filename and the old has the same file extension. +/// - `Condition 4` - Validate if the new filename has at least one character, apart from the file extension. +/// +/// # Returns +/// +/// Returns a result object to describe the status of the validation. +/// +/// * `Ok(bool)` - If the new name has passed all the validations. +/// * `Err(String)` - If the validation has failed. +fn is_valid_name(old_filepath: &PathBuf, new_name: &str) -> Result { + let old_name_str = old_filepath.file_name().unwrap_or_default().to_str().unwrap_or_default(); + if old_name_str == new_name { + return Err(format!("New name cannot be the same as old\n\n'{:?}'=='{new_name}'", old_filepath)) + } + if new_name.starts_with('_') || new_name.ends_with('_') || + new_name.starts_with('.') || new_name.ends_with('.') { + return Err(format!("New name cannot start or end with '.' or '_'\n\n'{}'", new_name)) + } + let old_extension = old_filepath.extension().unwrap().to_str().unwrap(); + let new_extension = new_name.split('.').last().unwrap_or_default(); + if old_extension != new_extension { + return Err(format!("File extension cannot be changed\n\n'{new_extension}' => '{old_extension}'")) + } + if new_name.len() <= old_extension.len() + 1 { + return Err(format!("At least one character is required as filename\n\nReceived {}", new_name.len())) + } + Ok(true) +} + +/// Renames the file. +/// +/// # Arguments +/// +/// - `old_filepath` - PathBuf object to the file that has to be renamed. +/// - `new_name` - New name for the file. +/// +/// # Returns +/// +/// * `200` - Blank HttpResponse to indicate that the request was successful. +/// * `400` - HttpResponse with an error message for invalid action or incorrect payload. +/// * `500` - HttpResponse with an error message for failed rename. +fn rename(media_path: PathBuf, new_name: &str) -> HttpResponse { + if new_name.is_empty() { + let reason = "New name not received in payload"; + log::warn!("{}", reason); + return HttpResponse::BadRequest().body(reason); + } + if !media_path.is_file() { + let reason = format!("{:?} is an invalid file entry", media_path); + return HttpResponse::BadRequest().body(reason); + } + let validity = is_valid_name( + &media_path, new_name + ); + return match validity { + Ok(_) => { + let new_path = media_path.parent().unwrap().join(new_name).to_string_lossy().to_string(); + let old_path = media_path.to_string_lossy().to_string(); + if let Err(error) = fs::rename(old_path, new_path) { + let reason = format!("Error renaming file: {}", error); + log::error!("{}", reason); + HttpResponse::InternalServerError().body(reason) + } else { + HttpResponse::Ok().finish() + } + }, + Err(msg) => { + HttpResponse::BadRequest().body(msg) + } + }; +} + +/// Deletes the file. +/// +/// # Arguments +/// +/// - `media_path` - PathBuf object to the file that has to be deleted. +/// +/// # Returns +/// +/// * `200` - Blank HttpResponse to indicate that the request was successful. +/// * `400` - HttpResponse with an error message for invalid action or incorrect payload. +/// * `500` - HttpResponse with an error message for failed delete. +fn delete(media_path: PathBuf) -> HttpResponse { + if media_path.is_file() { + if let Err(error) = fs::remove_file(media_path) { + let reason = format!("Error deleting file: {}", error); + log::error!("{}", reason); + HttpResponse::InternalServerError().body(reason) + } else { + HttpResponse::Ok().finish() + } + } else if media_path.is_dir() { + if let Err(error) = fs::remove_dir_all(media_path) { + let reason = format!("Error deleting directory: {}", error); + log::error!("{}", reason); + HttpResponse::InternalServerError().body(reason) + } else { + HttpResponse::Ok().finish() + } + } else { + let reason = format!("{:?} was neither a file nor a directory", media_path); + log::warn!("{}", reason); + HttpResponse::BadRequest().body(reason) + } +} diff --git a/src/templates/listing.rs b/src/templates/listing.rs index 5bf209c..b58ee41 100644 --- a/src/templates/listing.rs +++ b/src/templates/listing.rs @@ -106,7 +106,7 @@ pub fn get_content() -> String { @@ -183,9 +191,9 @@ pub fn get_content() -> String {



-