rustream/routes/
fileio.rs1use std::fs;
2
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use actix_web::{HttpRequest, HttpResponse, web};
7use actix_web::http::StatusCode;
8use fernet::Fernet;
9use serde::Deserialize;
10
11use crate::{constant, routes, squire};
12
13#[derive(Debug, Deserialize)]
15struct Payload {
16 url_locator: Option<String>,
17 path_locator: Option<String>,
18 new_name: Option<String>
19}
20
21fn extract_media_path(payload: &web::Json<Payload>, media_source: &Path) -> Result<PathBuf, String> {
35 let url_locator = payload.url_locator.as_deref();
36 let path_locator = payload.path_locator.as_deref();
37 if let (Some(url_str), Some(path_str)) = (url_locator, path_locator) {
38 for locator in &[url_str, path_str] {
40 if let Some(media_path) = locator.split("stream").nth(1) {
41 let path = media_source.join(media_path.strip_prefix('/').unwrap());
44 if path.exists() {
45 log::debug!("Extracted from '{}'", locator);
46 return Ok(path);
47 }
48 }
49 }
50 return Err(String::from("Unable to extract path from either of the parameters"));
51 }
52 Err(String::from("Both URL locator and path locator must be provided"))
53}
54
55#[post("/edit")]
74pub async fn edit(request: HttpRequest,
75 payload: web::Json<Payload>,
76 fernet: web::Data<Arc<Fernet>>,
77 session: web::Data<Arc<constant::Session>>,
78 metadata: web::Data<Arc<constant::MetaData>>,
79 config: web::Data<Arc<squire::settings::Config>>,
80 template: web::Data<Arc<minijinja::Environment<'static>>>) -> HttpResponse {
81 let auth_response = squire::authenticator::verify_token(&request, &config, &fernet, &session);
82 if !auth_response.ok {
83 return routes::auth::failed_auth(auth_response, &config);
84 }
85 let (_host, _last_accessed) = squire::custom::log_connection(&request, &session);
86 log::debug!("{}", auth_response.detail);
87 let extracted = extract_media_path(&payload, &config.media_source);
88 let media_path: PathBuf = match extracted {
90 Ok(path) => {
91 path
92 },
93 Err(msg) => {
94 return HttpResponse::BadRequest().body(msg);
95 }
96 };
97 if !squire::authenticator::verify_secure_index(&PathBuf::from(&media_path), &auth_response.username) {
98 return squire::custom::error(
99 "RESTRICTED SECTION",
100 template.get_template("error").unwrap(),
101 &metadata.pkg_version,
102 format!("This content is not accessible, as it does not belong to the user profile '{}'", auth_response.username),
103 StatusCode::FORBIDDEN
104 );
105 }
106 if let Some(edit_action) = request.headers().get("edit-action") {
107 let action = edit_action.to_str().unwrap();
108 log::info!("{} requested to {} {:?}", &auth_response.username, action, &media_path);
109 return if action == "delete" {
110 return delete(media_path);
111 } else if action == "rename" {
112 let new_name_str = payload.new_name.as_deref();
113 if let Some(new_name) = new_name_str {
114 return rename(media_path, new_name.trim());
115 } else {
116 HttpResponse::BadRequest().body("New name is missing!")
117 }
118 } else {
119 log::warn!("Unsupported action: {} requested to {} {:?}", &auth_response.username, action, &media_path);
120 HttpResponse::BadRequest().body("Unsupported action!")
121 };
122 }
123 log::warn!("No action received for: {:?}", media_path);
124 HttpResponse::BadRequest().body("No action received!")
125}
126
127fn is_valid_name(old_filepath: &PathBuf, new_name: &str) -> Result<bool, String> {
148 let old_name_str = old_filepath.file_name().unwrap_or_default().to_str().unwrap_or_default();
149 if old_name_str == new_name {
150 return Err(format!("New name cannot be the same as old\n\n'{:?}'=='{new_name}'", old_filepath))
151 }
152 if new_name.starts_with('_') || new_name.ends_with('_') ||
153 new_name.starts_with('.') || new_name.ends_with('.') {
154 return Err(format!("New name cannot start or end with '.' or '_'\n\n'{}'", new_name))
155 }
156 let old_extension = old_filepath.extension().unwrap().to_str().unwrap();
157 let new_extension = new_name.split('.').last().unwrap_or_default();
158 if old_extension != new_extension {
159 return Err(format!("File extension cannot be changed\n\n'{new_extension}' => '{old_extension}'"))
160 }
161 if new_name.len() <= old_extension.len() + 1 {
162 return Err(format!("At least one character is required as filename\n\nReceived {}", new_name.len()))
163 }
164 Ok(true)
165}
166
167fn rename(media_path: PathBuf, new_name: &str) -> HttpResponse {
180 if new_name.is_empty() {
181 let reason = "New name not received in payload";
182 log::warn!("{}", reason);
183 return HttpResponse::BadRequest().body(reason);
184 }
185 if !media_path.is_file() {
186 let reason = format!("{:?} is an invalid file entry", media_path);
187 return HttpResponse::BadRequest().body(reason);
188 }
189 let validity = is_valid_name(
190 &media_path, new_name
191 );
192 return match validity {
193 Ok(_) => {
194 let new_path = media_path.parent().unwrap().join(new_name).to_string_lossy().to_string();
195 let old_path = media_path.to_string_lossy().to_string();
196 if let Err(error) = fs::rename(old_path, new_path) {
197 let reason = format!("Error renaming file: {}", error);
198 log::error!("{}", reason);
199 HttpResponse::InternalServerError().body(reason)
200 } else {
201 HttpResponse::Ok().finish()
202 }
203 },
204 Err(msg) => {
205 HttpResponse::BadRequest().body(msg)
206 }
207 };
208}
209
210fn delete(media_path: PathBuf) -> HttpResponse {
222 if media_path.is_file() {
223 if let Err(error) = fs::remove_file(media_path) {
224 let reason = format!("Error deleting file: {}", error);
225 log::error!("{}", reason);
226 HttpResponse::InternalServerError().body(reason)
227 } else {
228 HttpResponse::Ok().finish()
229 }
230 } else if media_path.is_dir() {
231 if let Err(error) = fs::remove_dir_all(media_path) {
232 let reason = format!("Error deleting directory: {}", error);
233 log::error!("{}", reason);
234 HttpResponse::InternalServerError().body(reason)
235 } else {
236 HttpResponse::Ok().finish()
237 }
238 } else {
239 let reason = format!("{:?} was neither a file nor a directory", media_path);
240 log::warn!("{}", reason);
241 HttpResponse::BadRequest().body(reason)
242 }
243}