add file move/copy, add file delete and file upload

This commit is contained in:
jb-alvarado 2022-06-22 18:00:31 +02:00
parent 07b1bd30ae
commit b119849be4
6 changed files with 355 additions and 20 deletions

47
Cargo.lock generated
View File

@ -77,6 +77,24 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "actix-multipart"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9edfb0e7663d7fe18c8d5b668c9c1bcf79176b1dcc9d4da9592503209a6bfb0"
dependencies = [
"actix-utils",
"actix-web",
"bytes",
"derive_more",
"futures-core",
"httparse",
"local-waker",
"log",
"mime",
"twoway",
]
[[package]] [[package]]
name = "actix-router" name = "actix-router"
version = "0.5.0" version = "0.5.0"
@ -982,6 +1000,7 @@ dependencies = [
name = "ffplayout-api" name = "ffplayout-api"
version = "0.3.0" version = "0.3.0"
dependencies = [ dependencies = [
"actix-multipart",
"actix-web", "actix-web",
"actix-web-grants", "actix-web-grants",
"actix-web-httpauth", "actix-web-httpauth",
@ -992,6 +1011,7 @@ dependencies = [
"faccess", "faccess",
"ffplayout-lib", "ffplayout-lib",
"ffprobe", "ffprobe",
"futures-util",
"jsonwebtoken", "jsonwebtoken",
"log", "log",
"once_cell", "once_cell",
@ -1001,6 +1021,7 @@ dependencies = [
"regex", "regex",
"relative-path", "relative-path",
"reqwest", "reqwest",
"sanitize-filename",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
@ -2466,6 +2487,16 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "sanitize-filename"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf18934a12018228c5b55a6dae9df5d0641e3566b3630cb46cc55564068e7c2f"
dependencies = [
"lazy_static",
"regex",
]
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.20" version = "0.1.20"
@ -2996,12 +3027,28 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
[[package]]
name = "twoway"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47"
dependencies = [
"memchr",
"unchecked-index",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.15.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unchecked-index"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
[[package]] [[package]]
name = "unicase" name = "unicase"
version = "2.6.0" version = "2.6.0"

View File

@ -9,6 +9,7 @@ edition = "2021"
[dependencies] [dependencies]
ffplayout-lib = { path = "../lib" } ffplayout-lib = { path = "../lib" }
actix-multipart = "0.4"
actix-web = "4" actix-web = "4"
actix-web-grants = "3" actix-web-grants = "3"
actix-web-httpauth = "0.6" actix-web-httpauth = "0.6"
@ -18,6 +19,7 @@ clap = { version = "3.2", features = ["derive"] }
derive_more = "0.99" derive_more = "0.99"
faccess = "0.2" faccess = "0.2"
ffprobe = "0.3" ffprobe = "0.3"
futures-util = { version = "0.3", default-features = false, features = ["std"] }
jsonwebtoken = "8" jsonwebtoken = "8"
log = "0.4" log = "0.4"
once_cell = "1.10" once_cell = "1.10"
@ -26,6 +28,7 @@ rand_core = { version = "0.6", features = ["std"] }
relative-path = "1.6" relative-path = "1.6"
regex = "1" regex = "1"
reqwest = { version = "0.11", features = ["blocking", "json"] } reqwest = { version = "0.11", features = ["blocking", "json"] }
sanitize-filename = "0.3"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde_yaml = "0.8" serde_yaml = "0.8"

View File

@ -17,8 +17,9 @@ use utils::{
routes::{ routes::{
add_preset, add_user, del_playlist, file_browser, gen_playlist, get_playlist, add_preset, add_user, del_playlist, file_browser, gen_playlist, get_playlist,
get_playout_config, get_presets, get_settings, jump_to_last, jump_to_next, login, get_playout_config, get_presets, get_settings, jump_to_last, jump_to_next, login,
media_current, media_last, media_next, patch_settings, reset_playout, save_playlist, media_current, media_last, media_next, move_rename, patch_settings, remove, reset_playout,
send_text_message, update_playout_config, update_preset, update_user, save_file, save_playlist, send_text_message, update_playout_config, update_preset,
update_user,
}, },
run_args, Role, run_args, Role,
}; };
@ -95,7 +96,10 @@ async fn main() -> std::io::Result<()> {
.service(save_playlist) .service(save_playlist)
.service(gen_playlist) .service(gen_playlist)
.service(del_playlist) .service(del_playlist)
.service(file_browser), .service(file_browser)
.service(move_rename)
.service(remove)
.service(save_file),
) )
}) })
.bind((addr, port))? .bind((addr, port))?

View File

@ -1,4 +1,4 @@
use actix_web::{error::ResponseError, HttpResponse}; use actix_web::{error::ResponseError, Error, HttpResponse};
use derive_more::Display; use derive_more::Display;
#[derive(Debug, Display)] #[derive(Debug, Display)]
@ -29,3 +29,33 @@ impl ResponseError for ServiceError {
} }
} }
} }
impl From<String> for ServiceError {
fn from(err: String) -> ServiceError {
ServiceError::BadRequest(err)
}
}
impl From<Error> for ServiceError {
fn from(err: Error) -> ServiceError {
ServiceError::BadRequest(err.to_string())
}
}
impl From<actix_multipart::MultipartError> for ServiceError {
fn from(err: actix_multipart::MultipartError) -> ServiceError {
ServiceError::BadRequest(err.to_string())
}
}
impl From<std::io::Error> for ServiceError {
fn from(err: std::io::Error) -> ServiceError {
ServiceError::BadRequest(err.to_string())
}
}
impl From<actix_web::error::BlockingError> for ServiceError {
fn from(err: actix_web::error::BlockingError) -> ServiceError {
ServiceError::BadRequest(err.to_string())
}
}

View File

@ -1,6 +1,15 @@
use std::{
fs,
io::Write,
path::{Path, PathBuf},
};
use actix_multipart::Multipart;
use actix_web::{web, HttpResponse};
use futures_util::TryStreamExt as _;
use rand::{distributions::Alphanumeric, Rng};
use relative_path::RelativePath; use relative_path::RelativePath;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fs, path::PathBuf};
use simplelog::*; use simplelog::*;
@ -9,28 +18,32 @@ use ffplayout_lib::utils::file_extension;
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PathObject { pub struct PathObject {
root: String, pub source: String,
#[serde(skip_deserializing)] folders: Option<Vec<String>>,
folders: Vec<String>, files: Option<Vec<String>>,
#[serde(skip_deserializing)]
files: Vec<String>,
} }
impl PathObject { impl PathObject {
fn new(root: String) -> Self { fn new(source: String) -> Self {
Self { Self {
root, source,
folders: vec![], folders: Some(vec![]),
files: vec![], files: Some(vec![]),
} }
} }
} }
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct MoveObject {
source: String,
target: String,
}
pub async fn browser(id: i64, path_obj: &PathObject) -> Result<PathObject, ServiceError> { pub async fn browser(id: i64, path_obj: &PathObject) -> Result<PathObject, ServiceError> {
let (config, _) = playout_config(&id).await?; let (config, _) = playout_config(&id).await?;
let path = PathBuf::from(config.storage.path); let path = PathBuf::from(config.storage.path);
let extensions = config.storage.extensions; let extensions = config.storage.extensions;
let path_component = RelativePath::new(&path_obj.root) let path_component = RelativePath::new(&path_obj.source)
.normalize() .normalize()
.to_string() .to_string()
.replace("../", ""); .replace("../", "");
@ -57,11 +70,15 @@ pub async fn browser(id: i64, path_obj: &PathObject) -> Result<PathObject, Servi
} }
if file_path.is_dir() { if file_path.is_dir() {
obj.folders.push(path_str); if let Some(ref mut folders) = obj.folders {
folders.push(path_str);
}
} else if file_path.is_file() { } else if file_path.is_file() {
if let Some(ext) = file_extension(&file_path) { if let Some(ext) = file_extension(&file_path) {
if extensions.contains(&ext.to_string().to_lowercase()) { if extensions.contains(&ext.to_string().to_lowercase()) {
obj.files.push(path_str); if let Some(ref mut files) = obj.files {
files.push(path_str);
}
} }
} }
} }
@ -69,3 +86,200 @@ pub async fn browser(id: i64, path_obj: &PathObject) -> Result<PathObject, Servi
Ok(obj) Ok(obj)
} }
// fn copy_and_delete(source: &PathBuf, target: &PathBuf) -> Result<PathObject, ServiceError> {
// match fs::copy(&source, &target) {
// Ok(_) => {
// if let Err(e) = fs::remove_file(source) {
// error!("{e}");
// return Err(ServiceError::BadRequest(
// "Removing File not possible!".into(),
// ));
// };
// return Ok(PathObject::new(target.display().to_string()));
// }
// Err(e) => {
// error!("{e}");
// Err(ServiceError::BadRequest("Error in file copy!".into()))
// }
// }
// }
fn rename(source: &PathBuf, target: &PathBuf) -> Result<MoveObject, ServiceError> {
match fs::rename(&source, &target) {
Ok(_) => Ok(MoveObject {
source: source.display().to_string(),
target: target.display().to_string(),
}),
Err(e) => {
error!("{e}");
Err(ServiceError::BadRequest("Rename failed!".into()))
}
}
}
pub async fn rename_file(id: i64, move_object: &MoveObject) -> Result<MoveObject, ServiceError> {
let (config, _) = playout_config(&id).await?;
let path = PathBuf::from(&config.storage.path);
let source = RelativePath::new(&move_object.source)
.normalize()
.to_string()
.replace("../", "");
let target = RelativePath::new(&move_object.target)
.normalize()
.to_string()
.replace("../", "");
let mut source_path = PathBuf::from(source.clone());
let mut target_path = PathBuf::from(target.clone());
let relativ_path = RelativePath::new(&config.storage.path)
.normalize()
.to_string();
if !source_path.starts_with(&relativ_path) {
source_path = path.join(source);
} else {
source_path = path.join(source_path.strip_prefix(&relativ_path).unwrap());
}
if !target_path.starts_with(&relativ_path) {
target_path = path.join(target);
} else {
target_path = path.join(target_path.strip_prefix(relativ_path).unwrap());
}
if !source_path.exists() {
return Err(ServiceError::BadRequest("Source file not exist!".into()));
}
if (source_path.is_dir() || source_path.is_file()) && source_path.parent() == Some(&target_path)
{
return rename(&source_path, &target_path);
}
if target_path.is_dir() {
target_path = target_path.join(source_path.file_name().unwrap());
}
if target_path.is_file() {
return Err(ServiceError::BadRequest(
"Target file already exists!".into(),
));
}
if source_path.is_file() && target_path.parent().is_some() {
return rename(&source_path, &target_path);
}
Err(ServiceError::InternalServerError)
}
pub async fn remove_file_or_folder(id: i64, source_path: &str) -> Result<(), ServiceError> {
let (config, _) = playout_config(&id).await?;
let source = PathBuf::from(source_path);
let test_source = RelativePath::new(&source_path)
.normalize()
.to_string()
.replace("../", "");
let test_path = RelativePath::new(&config.storage.path)
.normalize()
.to_string();
if !test_source.starts_with(&test_path) {
return Err(ServiceError::BadRequest(
"Source file is not in storage!".into(),
));
}
if !source.exists() {
return Err(ServiceError::BadRequest("Source does not exists!".into()));
}
if source.is_dir() {
match fs::remove_dir(source) {
Ok(_) => return Ok(()),
Err(e) => {
error!("{e}");
return Err(ServiceError::BadRequest(
"Delete folder failed! (Folder must be empty)".into(),
));
}
};
}
if source.is_file() {
match fs::remove_file(source) {
Ok(_) => return Ok(()),
Err(e) => {
error!("{e}");
return Err(ServiceError::BadRequest("Delete file failed!".into()));
}
};
}
Err(ServiceError::InternalServerError)
}
async fn valid_path(id: i64, path: &str) -> Result<(), ServiceError> {
let (config, _) = playout_config(&id).await?;
let test_target = RelativePath::new(&path)
.normalize()
.to_string()
.replace("../", "");
let test_path = RelativePath::new(&config.storage.path)
.normalize()
.to_string();
if !test_target.starts_with(&test_path) {
return Err(ServiceError::BadRequest(
"Target folder is not in storage!".into(),
));
}
if !Path::new(path).is_dir() {
return Err(ServiceError::BadRequest("Target folder not exists!".into()));
}
Ok(())
}
pub async fn upload(id: i64, mut payload: Multipart) -> Result<HttpResponse, ServiceError> {
while let Some(mut field) = payload.try_next().await? {
let content_disposition = field.content_disposition();
println!("{content_disposition}");
let rand_string: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(20)
.map(char::from)
.collect();
let path_name = content_disposition.get_name().unwrap_or(&rand_string);
let filename = content_disposition
.get_filename()
.map_or_else(|| rand_string.to_string(), sanitize_filename::sanitize);
if let Err(e) = valid_path(id, path_name).await {
return Err(e);
}
let filepath = PathBuf::from(path_name).join(filename);
if filepath.is_file() {
return Err(ServiceError::BadRequest("Target already exists!".into()));
}
// File::create is blocking operation, use threadpool
let mut f = web::block(|| std::fs::File::create(filepath)).await??;
while let Some(chunk) = field.try_next().await? {
f = web::block(move || f.write_all(&chunk).map(|_| f)).await??;
}
}
Ok(HttpResponse::Ok().into())
}

View File

@ -1,6 +1,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use actix_web::{delete, get, http::StatusCode, patch, post, put, web, Responder}; use actix_multipart::Multipart;
use actix_web::{delete, get, http::StatusCode, patch, post, put, web, HttpResponse, Responder};
use actix_web_grants::{permissions::AuthDetails, proc_macro::has_any_role}; use actix_web_grants::{permissions::AuthDetails, proc_macro::has_any_role};
use argon2::{ use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, SaltString}, password_hash::{rand_core::OsRng, PasswordHash, SaltString},
@ -13,7 +14,7 @@ use crate::utils::{
auth::{create_jwt, Claims}, auth::{create_jwt, Claims},
control::{control_state, media_info, send_message}, control::{control_state, media_info, send_message},
errors::ServiceError, errors::ServiceError,
files::{browser, PathObject}, files::{browser, remove_file_or_folder, rename_file, upload, MoveObject, PathObject},
handles::{ handles::{
db_add_preset, db_add_user, db_get_presets, db_get_settings, db_login, db_role, db_add_preset, db_add_user, db_get_presets, db_get_settings, db_login, db_role,
db_update_preset, db_update_settings, db_update_user, db_update_preset, db_update_settings, db_update_user,
@ -415,7 +416,7 @@ pub async fn del_playlist(
/// ///
/// ---------------------------------------------------------------------------- /// ----------------------------------------------------------------------------
/// curl -X get http://localhost:8080/api/file/1/browse /// curl -X GET http://localhost:8080/api/file/1/browse/
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' /// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>'
#[post("/file/{id}/browse/")] #[post("/file/{id}/browse/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
@ -428,3 +429,39 @@ pub async fn file_browser(
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
/// curl -X POST http://localhost:8080/api/file/1/move/
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>'
/// -d '{"source": "<SOURCE>", "target": "<TARGET>"}'
#[post("/file/{id}/move/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn move_rename(
id: web::Path<i64>,
data: web::Json<MoveObject>,
) -> Result<impl Responder, ServiceError> {
match rename_file(*id, &data.into_inner()).await {
Ok(obj) => Ok(web::Json(obj)),
Err(e) => Err(e),
}
}
/// curl -X DELETE http://localhost:8080/api/file/1/remove/
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>'
/// -d '{"source": "<SOURCE>", "target": ""}'
#[delete("/file/{id}/remove/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn remove(
id: web::Path<i64>,
data: web::Json<PathObject>,
) -> Result<impl Responder, ServiceError> {
match remove_file_or_folder(*id, &data.into_inner().source).await {
Ok(obj) => Ok(web::Json(obj)),
Err(e) => Err(e),
}
}
#[post("/file/{id}/upload/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn save_file(id: web::Path<i64>, payload: Multipart) -> Result<HttpResponse, ServiceError> {
upload(*id, payload).await
}