Skip to content

Commit

Permalink
Include an option to rename secure files
Browse files Browse the repository at this point in the history
Setup validations in both server and client
  • Loading branch information
dormant-user committed Mar 16, 2024
1 parent 2eab7c8 commit de07598
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 47 deletions.
157 changes: 133 additions & 24 deletions src/routes/fileio.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::fs::{remove_dir_all, remove_file};
use std::fs;

use std::path::{Path, PathBuf};
use std::sync::Arc;
Expand All @@ -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<String>,
path_locator: Option<String>,
new_name: Option<String>
}

/// 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
///
Expand All @@ -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<Payload>, media_source: &Path) -> Result<PathBuf, String> {
fn extract_media_path(payload: &web::Json<Payload>, media_source: &Path) -> Result<PathBuf, String> {
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) {
Expand Down Expand Up @@ -65,9 +66,10 @@ fn extract_media_path(payload: web::Json<Payload>, 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<Payload>,
Expand All @@ -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) => {
Expand All @@ -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);
Expand All @@ -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<bool, String> {
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)
}
}
93 changes: 70 additions & 23 deletions src/templates/listing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ pub fn get_content() -> String {
</style>
<!-- Title list CSS -->
<style>
a:hover, a:active { font-size: 120%; opacity: 0.7; }
a:hover, a:active { font-size: 102%; opacity: 0.5; }
a:link { color: blue; }
a:visited { color: blue; }
ol {
Expand Down Expand Up @@ -145,8 +145,16 @@ pub fn get_content() -> String {
color: #000 !important; /* Black font */
}
.context-menu-item:hover {
background-color: #000 !important; /* White background */
color: #fff !important; /* Black font */
background-color: #000 !important; /* Black background */
color: #fff !important; /* White font */
}
.icon {
background-color: #fff !important; /* White background */
color: #000 !important; /* Black font */
}
.icon:hover {
background-color: #000 !important; /* Black background */
color: #fff !important; /* White font */
}
</style>
</head>
Expand Down Expand Up @@ -183,9 +191,9 @@ pub fn get_content() -> String {
</div>
<br><br><br><br>
<!-- Context menu template (hidden by default) -->
<div id="contextMenu" class="context-menu" style="display: none;">
<div class="context-menu-item" onclick="deleteItem(currentPath)">Delete</div>
<!-- <div class="context-menu-item" onclick="renameItem(currentPath)">Rename</div> -->
<div id="contextMenu" class="context-menu icon" style="display: none;">
<div class="context-menu-item" onclick="editItem(currentPath, 'delete')"><i class="fa-regular fa-trash-can"></i>&nbsp;&nbsp;Delete</div>
<div class="context-menu-item" onclick="editItem(currentPath, 'rename')"><i class="fa-solid fa-pen"></i></i>&nbsp;&nbsp;Rename</div>
</div>
{% if custom_title %}
<h1>{{ custom_title }}</h1>
Expand Down Expand Up @@ -219,7 +227,7 @@ pub fn get_content() -> String {
{% if secured_directories %}
<h3>Secured Directory</h3>
{% for directory in secured_directories %}
<li><i class="{{ directory.font }}"></i>&nbsp;&nbsp;<a oncontextmenu="showContextMenu(event, '{{ directory.path }}')" href="{{ directory.path }}">{{ directory.name }}</a></li>
<li><i class="{{ directory.font }}"></i>&nbsp;&nbsp;<a oncontextmenu="showContextMenu(event, '{{ directory.path }}', true)" href="{{ directory.path }}">{{ directory.name }}</a></li>
{% endfor %}
{% endif %}
{% else %}
Expand Down Expand Up @@ -247,11 +255,12 @@ pub fn get_content() -> String {
var contextMenu = document.getElementById('contextMenu');
// Function to show context menu
function showContextMenu(event, path) {
function showContextMenu(event, path, isDir = false) {
event.preventDefault();
// Set the global variable to the current file path
currentPath = path;
directory = isDir;
// Calculate the appropriate coordinates for the context menu
var mouseX = event.clientX;
Expand All @@ -273,7 +282,7 @@ pub fn get_content() -> String {
contextMenu.style.display = 'block';
}
function editAction(action, trueURL, relativePath) {
function editAction(action, trueURL, relativePath, newName) {
let http = new XMLHttpRequest();
http.open('POST', window.location.origin + `/edit`, true); // asynchronous session
http.setRequestHeader('Content-Type', 'application/json'); // Set content type to JSON
Expand All @@ -283,13 +292,18 @@ pub fn get_content() -> String {
if (http.status === 200) {
window.location.reload();
} else {
console.error('Error:', http.status);
if (http.responseText !== "") {
alert(`Error: ${http.responseText}`);
} else {
alert(`Error: ${http.statusText}`);
}
}
}
};
let data = {
url_locator: trueURL,
path_locator: relativePath
path_locator: relativePath,
new_name: newName
};
http.send(JSON.stringify(data));
}
Expand All @@ -311,23 +325,56 @@ pub fn get_content() -> String {
return path.substring(lastIndex + 1);
}
// Function to handle delete action
function deleteItem(relativePath) {
function isValidName(oldName, newName) {
// Condition 1 - Validate if the new filename is the same as old.
if (oldName === newName) {
alert(`New name is the same as old\n\n'${oldName}'=='${newName}'`);
}
// Condition 2 - Validate if the new filename starts or ends with . or _
if (newName.startsWith('_') || newName.endsWith('_') ||
newName.startsWith('.') || newName.endsWith('.')) {
alert(`New name cannot start or end with '.' or '_'\n\n${newName}`);
return false;
}
// Condition 3 - Validate if the new filename and the old has the same file extension.
const oldExtension = oldName.split('.').pop();
const newExtension = newName.split('.').pop();
// Check condition 3
if (oldExtension !== newExtension) {
alert(`File extension cannot be changed\n\n'${newExtension}' => '${oldExtension}'`);
return false;
}
// Condition 4 - Validate if the new filename has at least one character, apart from the file extension.
if (newName.length <= oldExtension.length + 1) {
alert(`At least one character is required as filename\n\nReceived ${newName.length}`);
return false;
}
return true;
}
// Function to handle delete/rename action
function editItem(relativePath, action) {
contextMenu.style.display = 'none';
let fileName = extractFileName(relativePath);
let pass = getConfirmation(fileName, 'delete');
if (!pass) {
return;
if (action === 'delete') {
let pass = getConfirmation(fileName, action);
if (!pass) {
return;
}
var newName = null;
} else {
if (directory) {
alert("Only a 'delete' action is permitted on directories");
return;
}
var newName = prompt(`Enter a new name for the file\n\nCurrent: ${fileName}\n`);
if (!isValidName(fileName, newName)) {
return;
}
}
let trueURL = window.location.href + '/' + fileName;
editAction("delete", trueURL, relativePath);
}
// Function to handle rename action
function renameItem(path) {
contextMenu.style.display = 'none';
alert(`Rename of ${path} is not enabled yet!!`);
editAction(action, trueURL, relativePath, newName);
}
// Hide context menu when clicking outside
Expand Down

0 comments on commit de07598

Please sign in to comment.