Skip to content

Commit

Permalink
Allow users to delete file/folder with right click
Browse files Browse the repository at this point in the history
Create a dedicated crate to handle deletions
Renames to be implemented
  • Loading branch information
dormant-user committed Mar 15, 2024
1 parent 43777e8 commit 820fcf5
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ pub async fn start() -> io::Result<()> {
.service(routes::auth::logout)
.service(routes::auth::home)
.service(routes::basics::profile)
.service(routes::fileio::edit)
.service(routes::auth::error)
.service(routes::media::track)
.service(routes::media::stream)
Expand Down
133 changes: 133 additions & 0 deletions src/routes/fileIO.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use std::fs::{remove_dir_all, remove_file};

use std::path::{Path, PathBuf};
use std::sync::Arc;

use actix_web::{HttpRequest, HttpResponse, web};
use fernet::Fernet;
use serde::Deserialize;

use crate::{constant, routes, squire};

/// Struct to represent the payload data with both the URL locator and path locator
#[derive(Debug, Deserialize)]
struct Payload {
url_locator: Option<String>,
path_locator: Option<String>,
}

/// Extracts the path the file/directory that has to be deleted from the payload received.
///
/// # Arguments
///
/// * `payload` - Payload received from the UI as JSON body.
/// * `media_source` - Media source configured for the server.
///
/// # Returns
///
/// Returns a result object to describe the status of the extraction.
///
/// * `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> {
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) {
// Create a collection since a tuple is a fixed-size collection in rust and doesn't allow iteration
for locator in &[url_str, path_str] {
if let Some(media_path) = locator.split("stream").nth(1) {
// Without stripping the '/' in front of the path, Rust will assume that's a root path
// This will overwrite media_source and render the joined path instead of combining the two
let path = media_source.join(media_path.strip_prefix('/').unwrap());
if path.exists() {
log::debug!("Extracted from '{}'", locator);
return Ok(path);
}
}
}
return Err(String::from("Unable to extract path from either of the parameters"));
}
Err(String::from("Both URL locator and path locator must be provided"))
}

/// Handles requests for the `/edit` endpoint, to delete/rename media files and directories.
///
/// # Arguments
///
/// * `request` - A reference to the Actix web `HttpRequest` object.
/// * `payload` - JSON payload with `url_path` and `true_path` received from the UI.
/// * `fernet` - Fernet object to encrypt the auth payload that will be set as `session_token` cookie.
/// * `session` - Session struct that holds the `session_mapping` and `session_tracker` to handle sessions.
/// * `metadata` - Struct containing metadata of the application.
/// * `config` - Configuration data for the application.
/// * `template` - Configuration container for the loaded templates.
///
/// # Returns
///
/// * `200` - HttpResponse with a `session_token` and redirect URL to the `/home` entrypoint.
/// * `400` - HttpResponse with an error message for invalid action or incorrect payload.
/// * `401` - HttpResponse with an error message for failed authentication.
#[post("/edit")]
pub async fn edit(request: HttpRequest,
payload: web::Json<Payload>,
fernet: web::Data<Arc<Fernet>>,
session: web::Data<Arc<constant::Session>>,
metadata: web::Data<Arc<constant::MetaData>>,
config: web::Data<Arc<squire::settings::Config>>,
template: web::Data<Arc<minijinja::Environment<'static>>>) -> HttpResponse {
let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session);
if !auth_response.ok {
return routes::auth::failed_auth(auth_response, &config);
}
let (_host, _last_accessed) = squire::logger::log_connection(&request, &session);
log::debug!("{}", auth_response.detail);
let extracted = extract_media_path(payload, &config.media_source);
// todo: pop up doesn't always occur next to the mouse
// styling of the pop up is very basic
// make custom error responses generic
let media_path: PathBuf = match extracted {
Ok(path) => {
path
},
Err(msg) => {
return HttpResponse::BadRequest().body(msg);
}
};
if !squire::authenticator::verify_secure_index(&PathBuf::from(&media_path), &auth_response.username) {
return squire::responses::restricted(
template.get_template("error").unwrap(),
&auth_response.username,
&metadata.pkg_version,
);
}
if let Some(edit_action) = request.headers().get("edit-action") {
let action = edit_action.to_str().unwrap();
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()
}
} else {
let reason = format!("{:?} was neither a file nor a directory", media_path);
log::warn!("{}", reason);
HttpResponse::BadRequest().body(reason)
}
} else {
log::warn!("Unsupported action: {} requested to {} {:?}", &auth_response.username, action, &media_path);
HttpResponse::BadRequest().body("Unsupported action!")
};
}
log::warn!("No action received for: {:?}", media_path);
HttpResponse::BadRequest().body("No action received!")
}
7 changes: 5 additions & 2 deletions src/routes/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ struct Subtitles {
/// * `path` - The input path string to be URL encoded.
///
/// ## References
/// - [RustJobs](https://rustjobs.dev/blog/how-to-url-encode-strings-in-rust/)
/// - [rustjobs.dev](https://rustjobs.dev/blog/how-to-url-encode-strings-in-rust/)
///
/// # Returns
///
Expand Down Expand Up @@ -174,6 +174,8 @@ pub async fn stream(request: HttpRequest,
&metadata.pkg_version
);
}
let secure_path = if filepath.contains(constant::SECURE_INDEX) { "true" } else { "false" };
let secure_flag = secure_path.to_string();
// True path of the media file
let __target = config.media_source.join(&filepath);
if !__target.exists() {
Expand Down Expand Up @@ -251,7 +253,8 @@ pub async fn stream(request: HttpRequest,
user => auth_response.username,
secure_index => constant::SECURE_INDEX,
directories => listing_page.directories,
secured_directories => listing_page.secured_directories
secured_directories => listing_page.secured_directories,
secure_path => &secure_flag
)).unwrap());
}
log::error!("Something went horribly wrong");
Expand Down
6 changes: 4 additions & 2 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/// Module for the primary and health check entry points.
/// Module for `/`, `/health` and `/profile` entrypoints.
pub mod basics;
/// Module for all the rendering based entry points.
pub mod media;
/// Module for `/home`, `/login`, `/logout` and `/error` entrypoints.
pub mod auth;
/// Module to handle upload entrypoint.
/// Module for `/upload` entrypoint that handles the file uploads.
pub mod upload;
/// Module for `/edit` entrypoint that handles delete/rename actions.
pub mod fileio;
4 changes: 2 additions & 2 deletions src/squire/ascii_art.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use rand::prelude::SliceRandom;
/// Prints random ASCII art of a horse, dog or a dolphin.
///
/// ## References
/// - [https://www.asciiart.eu](https://www.asciiart.eu)
/// - [https://asciiart.cc](https://asciiart.cc)
/// - [asciiart.eu](https://www.asciiart.eu)
/// - [asciiart.cc](https://asciiart.cc)
pub fn random() {
let horse = r"
# #
Expand Down
114 changes: 112 additions & 2 deletions src/templates/listing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,26 @@ pub fn get_content() -> String {
margin-right: 0.5rem;
}
</style>
<style>
/* Style for context menu */
.context-menu {
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
padding: 5px 0;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
}
.context-menu-item {
padding: 5px 10px;
cursor: pointer;
background-color: #fff !important; /* White background */
color: #000 !important; /* Black font */
}
.context-menu-item:hover {
background-color: #000 !important; /* White background */
color: #fff !important; /* Black font */
}
</style>
</head>
<noscript>
<style>
Expand Down Expand Up @@ -162,6 +182,11 @@ pub fn get_content() -> String {
</div>
</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>
{% if custom_title %}
<h1>{{ custom_title }}</h1>
{% else %}
Expand All @@ -177,7 +202,11 @@ pub fn get_content() -> String {
{% if files %}
<h3>Files {{ files|length }}</h3>
{% for file in files %}
<li><i class="{{ file.font }}"></i>&nbsp;&nbsp;<a href="{{ file.path }}">{{ file.name }}</a></li>
{% if secure_path == 'true' %}
<li><i class="{{ file.font }}"></i>&nbsp;&nbsp;<a oncontextmenu="showContextMenu(event, '{{ file.path }}')" href="{{ file.path }}">{{ file.name }}</a></li>
{% else %}
<li><i class="{{ file.font }}"></i>&nbsp;&nbsp;<a href="{{ file.path }}">{{ file.name }}</a></li>
{% endif %}
{% endfor %}
{% endif %}
<!-- Display number of directories and list the directories -->
Expand All @@ -190,7 +219,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 href="{{ directory.path }}">{{ directory.name }}</a></li>
<li><i class="{{ directory.font }}"></i>&nbsp;&nbsp;<a oncontextmenu="showContextMenu(event, '{{ directory.path }}')" href="{{ directory.path }}">{{ directory.name }}</a></li>
{% endfor %}
{% endif %}
{% else %}
Expand All @@ -214,6 +243,87 @@ pub fn get_content() -> String {
window.history.back();
}
</script>
<script>
var contextMenu = document.getElementById('contextMenu');
// Function to show context menu
function showContextMenu(event, path) {
event.preventDefault();
// Set the global variable to the current file path
currentPath = path;
// Position the context menu beneath the clicked icon
contextMenu.style.display = 'block';
contextMenu.style.left = event.clientX + 'px';
contextMenu.style.top = event.clientY + 'px';
}
function editAction(action, trueURL, relativePath) {
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
http.setRequestHeader('edit-action', action);
let data = {
url_locator: trueURL,
path_locator: relativePath
};
let jsonData = JSON.stringify(data);
http.onreadystatechange = function() {
if (http.readyState === XMLHttpRequest.DONE) {
if (http.status === 200) {
window.location.reload();
} else {
console.error('Error:', http.status);
}
}
};
http.send(jsonData);
}
function getConfirmation(fileName, action) {
let confirmation = confirm(`Are you sure you want to ${action}?\n\n'${fileName}'`);
if (!confirmation) {
contextMenu.style.display = 'none';
return false;
}
return true;
}
function extractFileName(path) {
// Find the last occurrence of either '/' or '\'
const lastIndex = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
// Extract the filename using substring
return path.substring(lastIndex + 1);
}
// Function to handle delete action
function deleteItem(relativePath) {
contextMenu.style.display = 'none';
let fileName = extractFileName(relativePath);
let pass = getConfirmation(fileName, 'delete');
if (!pass) {
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!!`);
}
// Hide context menu when clicking outside
document.addEventListener('click', function(event) {
if (event.target !== contextMenu && !contextMenu.contains(event.target)) {
contextMenu.style.display = 'none';
}
});
</script>
</body>
</html>
"###.to_string()
Expand Down

0 comments on commit 820fcf5

Please sign in to comment.