1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use actix_web::{HttpRequest, HttpResponse, web};
use actix_web::http::StatusCode;
use fernet::Fernet;
use serde::Deserialize;
use crate::{constant, routes, squire};
/// 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 modified 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` - 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>,
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::custom::log_connection(&request, &session);
log::debug!("{}", auth_response.detail);
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) => {
path
},
Err(msg) => {
return HttpResponse::BadRequest().body(msg);
}
};
if !squire::authenticator::verify_secure_index(&PathBuf::from(&media_path), &auth_response.username) {
return squire::custom::error(
"RESTRICTED SECTION",
template.get_template("error").unwrap(),
&metadata.pkg_version,
format!("This content is not accessible, as it does not belong to the user profile '{}'", auth_response.username),
StatusCode::FORBIDDEN
);
}
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" {
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 {
HttpResponse::BadRequest().body("New name is missing!")
}
} 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!")
}
/// 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)
}
}