add file move/copy, add file delete and file upload
This commit is contained in:
parent
07b1bd30ae
commit
b119849be4
47
Cargo.lock
generated
47
Cargo.lock
generated
@ -77,6 +77,24 @@ dependencies = [
|
||||
"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]]
|
||||
name = "actix-router"
|
||||
version = "0.5.0"
|
||||
@ -982,6 +1000,7 @@ dependencies = [
|
||||
name = "ffplayout-api"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"actix-multipart",
|
||||
"actix-web",
|
||||
"actix-web-grants",
|
||||
"actix-web-httpauth",
|
||||
@ -992,6 +1011,7 @@ dependencies = [
|
||||
"faccess",
|
||||
"ffplayout-lib",
|
||||
"ffprobe",
|
||||
"futures-util",
|
||||
"jsonwebtoken",
|
||||
"log",
|
||||
"once_cell",
|
||||
@ -1001,6 +1021,7 @@ dependencies = [
|
||||
"regex",
|
||||
"relative-path",
|
||||
"reqwest",
|
||||
"sanitize-filename",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
@ -2466,6 +2487,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "schannel"
|
||||
version = "0.1.20"
|
||||
@ -2996,12 +3027,28 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "typenum"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||
|
||||
[[package]]
|
||||
name = "unchecked-index"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.6.0"
|
||||
|
@ -9,6 +9,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
ffplayout-lib = { path = "../lib" }
|
||||
actix-multipart = "0.4"
|
||||
actix-web = "4"
|
||||
actix-web-grants = "3"
|
||||
actix-web-httpauth = "0.6"
|
||||
@ -18,6 +19,7 @@ clap = { version = "3.2", features = ["derive"] }
|
||||
derive_more = "0.99"
|
||||
faccess = "0.2"
|
||||
ffprobe = "0.3"
|
||||
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||
jsonwebtoken = "8"
|
||||
log = "0.4"
|
||||
once_cell = "1.10"
|
||||
@ -26,6 +28,7 @@ rand_core = { version = "0.6", features = ["std"] }
|
||||
relative-path = "1.6"
|
||||
regex = "1"
|
||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||
sanitize-filename = "0.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.8"
|
||||
|
@ -17,8 +17,9 @@ use utils::{
|
||||
routes::{
|
||||
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,
|
||||
media_current, media_last, media_next, patch_settings, reset_playout, save_playlist,
|
||||
send_text_message, update_playout_config, update_preset, update_user,
|
||||
media_current, media_last, media_next, move_rename, patch_settings, remove, reset_playout,
|
||||
save_file, save_playlist, send_text_message, update_playout_config, update_preset,
|
||||
update_user,
|
||||
},
|
||||
run_args, Role,
|
||||
};
|
||||
@ -95,7 +96,10 @@ async fn main() -> std::io::Result<()> {
|
||||
.service(save_playlist)
|
||||
.service(gen_playlist)
|
||||
.service(del_playlist)
|
||||
.service(file_browser),
|
||||
.service(file_browser)
|
||||
.service(move_rename)
|
||||
.service(remove)
|
||||
.service(save_file),
|
||||
)
|
||||
})
|
||||
.bind((addr, port))?
|
||||
|
@ -1,4 +1,4 @@
|
||||
use actix_web::{error::ResponseError, HttpResponse};
|
||||
use actix_web::{error::ResponseError, Error, HttpResponse};
|
||||
use derive_more::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())
|
||||
}
|
||||
}
|
||||
|
@ -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 serde::{Deserialize, Serialize};
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use simplelog::*;
|
||||
|
||||
@ -9,28 +18,32 @@ use ffplayout_lib::utils::file_extension;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct PathObject {
|
||||
root: String,
|
||||
#[serde(skip_deserializing)]
|
||||
folders: Vec<String>,
|
||||
#[serde(skip_deserializing)]
|
||||
files: Vec<String>,
|
||||
pub source: String,
|
||||
folders: Option<Vec<String>>,
|
||||
files: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl PathObject {
|
||||
fn new(root: String) -> Self {
|
||||
fn new(source: String) -> Self {
|
||||
Self {
|
||||
root,
|
||||
folders: vec![],
|
||||
files: vec![],
|
||||
source,
|
||||
folders: Some(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> {
|
||||
let (config, _) = playout_config(&id).await?;
|
||||
let path = PathBuf::from(config.storage.path);
|
||||
let extensions = config.storage.extensions;
|
||||
let path_component = RelativePath::new(&path_obj.root)
|
||||
let path_component = RelativePath::new(&path_obj.source)
|
||||
.normalize()
|
||||
.to_string()
|
||||
.replace("../", "");
|
||||
@ -57,11 +70,15 @@ pub async fn browser(id: i64, path_obj: &PathObject) -> Result<PathObject, Servi
|
||||
}
|
||||
|
||||
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() {
|
||||
if let Some(ext) = file_extension(&file_path) {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
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 argon2::{
|
||||
password_hash::{rand_core::OsRng, PasswordHash, SaltString},
|
||||
@ -13,7 +14,7 @@ use crate::utils::{
|
||||
auth::{create_jwt, Claims},
|
||||
control::{control_state, media_info, send_message},
|
||||
errors::ServiceError,
|
||||
files::{browser, PathObject},
|
||||
files::{browser, remove_file_or_folder, rename_file, upload, MoveObject, PathObject},
|
||||
handles::{
|
||||
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,
|
||||
@ -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>'
|
||||
#[post("/file/{id}/browse/")]
|
||||
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
|
||||
@ -428,3 +429,39 @@ pub async fn file_browser(
|
||||
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user