prepare for version 0.4.0

- error type NoContent
- file list with duration info
- fix sort
- simplify upload
- file extension without dot
- add queries to the routes
This commit is contained in:
jb-alvarado 2022-07-04 17:59:22 +02:00
parent 8a1033b23f
commit cb65d8084f
8 changed files with 80 additions and 43 deletions

2
Cargo.lock generated
View File

@ -1027,7 +1027,7 @@ dependencies = [
[[package]] [[package]]
name = "ffplayout-api" name = "ffplayout-api"
version = "0.3.3" version = "0.4.0"
dependencies = [ dependencies = [
"actix-multipart", "actix-multipart",
"actix-web", "actix-web",

View File

@ -4,7 +4,7 @@ description = "Rest API for ffplayout"
license = "GPL-3.0" license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"] authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md" readme = "README.md"
version = "0.3.3" version = "0.4.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -14,6 +14,9 @@ pub enum ServiceError {
#[display(fmt = "Unauthorized")] #[display(fmt = "Unauthorized")]
Unauthorized, Unauthorized,
#[display(fmt = "NoContent: {}", _0)]
NoContent(String),
} }
// impl ResponseError trait allows to convert our errors into http responses with appropriate data // impl ResponseError trait allows to convert our errors into http responses with appropriate data
@ -26,6 +29,7 @@ impl ResponseError for ServiceError {
ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message), ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message),
ServiceError::Conflict(ref message) => HttpResponse::Conflict().json(message), ServiceError::Conflict(ref message) => HttpResponse::Conflict().json(message),
ServiceError::Unauthorized => HttpResponse::Unauthorized().json("No Permission!"), ServiceError::Unauthorized => HttpResponse::Unauthorized().json("No Permission!"),
ServiceError::NoContent(ref message) => HttpResponse::NoContent().json(message),
} }
} }
} }

View File

@ -10,14 +10,14 @@ use serde::{Deserialize, Serialize};
use simplelog::*; use simplelog::*;
use crate::utils::{errors::ServiceError, playout_config}; use crate::utils::{errors::ServiceError, playout_config};
use ffplayout_lib::utils::file_extension; use ffplayout_lib::utils::{file_extension, MediaProbe};
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PathObject { pub struct PathObject {
pub source: String, pub source: String,
parent: Option<String>, parent: Option<String>,
folders: Option<Vec<String>>, folders: Option<Vec<String>>,
files: Option<Vec<String>>, files: Option<Vec<VideoFile>>,
} }
impl PathObject { impl PathObject {
@ -37,6 +37,12 @@ pub struct MoveObject {
target: String, target: String,
} }
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct VideoFile {
name: String,
duration: f64,
}
/// Normalize absolut path /// Normalize absolut path
/// ///
/// This function takes care, that it is not possible to break out from root_path. /// This function takes care, that it is not possible to break out from root_path.
@ -91,7 +97,9 @@ pub async fn browser(id: i64, path_obj: &PathObject) -> Result<PathObject, Servi
} }
}; };
paths.sort_by_key(|dir| dir.path()); paths.sort_by_key(|dir| dir.path().display().to_string().to_lowercase());
let mut files = vec![];
let mut folders = vec![];
for path in paths { for path in paths {
let file_path = path.path().to_owned(); let file_path = path.path().to_owned();
@ -103,20 +111,30 @@ pub async fn browser(id: i64, path_obj: &PathObject) -> Result<PathObject, Servi
} }
if file_path.is_dir() { if file_path.is_dir() {
if let Some(ref mut folders) = obj.folders {
folders.push(path.file_name().unwrap().to_string_lossy().to_string()); folders.push(path.file_name().unwrap().to_string_lossy().to_string());
}
} 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()) {
if let Some(ref mut files) = obj.files { let media = MediaProbe::new(&path.display().to_string());
files.push(path.file_name().unwrap().to_string_lossy().to_string()); let mut duration = 0.0;
}
if let Some(dur) = media.format.and_then(|f| f.duration) {
duration = dur.parse().unwrap_or(0.0)
}
let video = VideoFile {
name: path.file_name().unwrap().to_string_lossy().to_string(),
duration,
};
files.push(video);
} }
} }
} }
} }
obj.folders = Some(folders);
obj.files = Some(files);
Ok(obj) Ok(obj)
} }
@ -240,7 +258,7 @@ pub async fn remove_file_or_folder(id: i64, source_path: &String) -> Result<(),
Err(ServiceError::InternalServerError) Err(ServiceError::InternalServerError)
} }
async fn valid_path(id: i64, path: &String) -> Result<(), ServiceError> { async fn valid_path(id: i64, path: &String) -> Result<PathBuf, ServiceError> {
let (config, _) = playout_config(&id).await?; let (config, _) = playout_config(&id).await?;
let (test_path, _, _) = norm_abs_path(&config.storage.path, path); let (test_path, _, _) = norm_abs_path(&config.storage.path, path);
@ -248,10 +266,14 @@ async fn valid_path(id: i64, path: &String) -> Result<(), ServiceError> {
return Err(ServiceError::BadRequest("Target folder not exists!".into())); return Err(ServiceError::BadRequest("Target folder not exists!".into()));
} }
Ok(()) Ok(test_path)
} }
pub async fn upload(id: i64, mut payload: Multipart) -> Result<HttpResponse, ServiceError> { pub async fn upload(
id: i64,
mut payload: Multipart,
path: &String,
) -> Result<HttpResponse, ServiceError> {
while let Some(mut field) = payload.try_next().await? { while let Some(mut field) = payload.try_next().await? {
let content_disposition = field.content_disposition(); let content_disposition = field.content_disposition();
debug!("{content_disposition}"); debug!("{content_disposition}");
@ -260,16 +282,12 @@ pub async fn upload(id: i64, mut payload: Multipart) -> Result<HttpResponse, Ser
.take(20) .take(20)
.map(char::from) .map(char::from)
.collect(); .collect();
let path_name = content_disposition.get_name().unwrap_or(&rand_string);
let filename = content_disposition let filename = content_disposition
.get_filename() .get_filename()
.map_or_else(|| rand_string.to_string(), sanitize_filename::sanitize); .map_or_else(|| rand_string.to_string(), sanitize_filename::sanitize);
if let Err(e) = valid_path(id, &path_name.to_string()).await { let target_path = valid_path(id, path).await?;
return Err(e); let filepath = target_path.join(filename);
}
let filepath = PathBuf::from(path_name).join(filename);
if filepath.is_file() { if filepath.is_file() {
return Err(ServiceError::BadRequest("Target already exists!".into())); return Err(ServiceError::BadRequest("Target already exists!".into()));

View File

@ -106,7 +106,7 @@ pub async fn db_init() -> Result<&'static str, Box<dyn std::error::Error>> {
INSERT INTO global(secret) VALUES($1); INSERT INTO global(secret) VALUES($1);
INSERT INTO settings(channel_name, preview_url, config_path, extra_extensions, timezone, service) INSERT INTO settings(channel_name, preview_url, config_path, extra_extensions, timezone, service)
VALUES('Channel 1', 'http://localhost/live/preview.m3u8', VALUES('Channel 1', 'http://localhost/live/preview.m3u8',
'/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png', 'UTC', 'ffplayout.service'); '/etc/ffplayout/ffplayout.yml', 'jpg,jpeg,png', 'UTC', 'ffplayout.service');
INSERT INTO roles(name) VALUES('admin'), ('user'), ('guest'); INSERT INTO roles(name) VALUES('admin'), ('user'), ('guest');
INSERT INTO presets(name, text, x, y, fontsize, line_spacing, fontcolor, box, boxcolor, boxborderw, alpha, channel_id) INSERT INTO presets(name, text, x, y, fontsize, line_spacing, fontcolor, box, boxcolor, boxborderw, alpha, channel_id)
VALUES('Default', 'Wellcome to ffplayout messenger!', '(w-text_w)/2', '(h-text_h)/2', '24', '4', '#ffffff@0xff', '0', '#000000@0x80', '4', '1.0', '1'), VALUES('Default', 'Wellcome to ffplayout messenger!', '(w-text_w)/2', '(h-text_h)/2', '24', '4', '#ffffff@0xff', '0', '#000000@0x80', '4', '1.0', '1'),

View File

@ -37,11 +37,10 @@ pub async fn read_playlist(id: i64, date: String) -> Result<JsonPlaylist, Servic
.join(date.clone()) .join(date.clone())
.with_extension("json"); .with_extension("json");
if let Ok(p) = json_reader(&playlist_path) { match json_reader(&playlist_path) {
return Ok(p); Ok(p) => Ok(p),
}; Err(e) => Err(ServiceError::NoContent(e.to_string())),
}
Err(ServiceError::InternalServerError)
} }
pub async fn write_playlist(id: i64, json_data: JsonPlaylist) -> Result<String, ServiceError> { pub async fn write_playlist(id: i64, json_data: JsonPlaylist) -> Result<String, ServiceError> {

View File

@ -7,7 +7,7 @@ use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, SaltString}, password_hash::{rand_core::OsRng, PasswordHash, SaltString},
Argon2, PasswordHasher, PasswordVerifier, Argon2, PasswordHasher, PasswordVerifier,
}; };
use serde::Serialize; use serde::{Deserialize, Serialize};
use simplelog::*; use simplelog::*;
use crate::utils::{ use crate::utils::{
@ -41,6 +41,18 @@ struct UserObj<T> {
user: Option<T>, user: Option<T>,
} }
#[derive(Debug, Deserialize, Serialize)]
pub struct DateObj {
#[serde(default)]
date: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct FileObj {
#[serde(default)]
path: String,
}
/// curl -X POST http://127.0.0.1:8080/auth/login/ -H "Content-Type: application/json" \ /// curl -X POST http://127.0.0.1:8080/auth/login/ -H "Content-Type: application/json" \
/// -d '{"username": "<USER>", "password": "<PASS>" }' /// -d '{"username": "<USER>", "password": "<PASS>" }'
#[post("/auth/login/")] #[post("/auth/login/")]
@ -396,14 +408,15 @@ pub async fn process_control(
/// ///
/// ---------------------------------------------------------------------------- /// ----------------------------------------------------------------------------
/// curl -X GET http://localhost:8080/api/playlist/1/2022-06-20 /// curl -X GET http://localhost:8080/api/playlist/1?date=2022-06-20
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' /// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>'
#[get("/playlist/{id}/{date}")] #[get("/playlist/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn get_playlist( pub async fn get_playlist(
params: web::Path<(i64, String)>, id: web::Path<i64>,
obj: web::Query<DateObj>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match read_playlist(params.0, params.1.clone()).await { match read_playlist(*id, obj.date.clone()).await {
Ok(playlist) => Ok(web::Json(playlist)), Ok(playlist) => Ok(web::Json(playlist)),
Err(e) => Err(e), Err(e) => Err(e),
} }
@ -455,14 +468,13 @@ pub async fn del_playlist(
/// ///
/// ---------------------------------------------------------------------------- /// ----------------------------------------------------------------------------
#[get("/log/{req:.*}")] #[get("/log/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn get_log(req: web::Path<String>) -> Result<impl Responder, ServiceError> { pub async fn get_log(
let mut segments = req.split('/'); id: web::Path<i64>,
let id: i64 = segments.next().unwrap_or_default().parse().unwrap_or(0); log: web::Query<DateObj>,
let date = segments.next().unwrap_or_default(); ) -> Result<impl Responder, ServiceError> {
read_log_file(&id, &log.date).await
read_log_file(&id, date).await
} }
/// ---------------------------------------------------------------------------- /// ----------------------------------------------------------------------------
@ -511,10 +523,10 @@ pub async fn move_rename(
} }
} }
/// curl -X DELETE http://localhost:8080/api/file/1/remove/ /// curl -X POST http://localhost:8080/api/file/1/remove/
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' /// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>'
/// -d '{"source": "<SOURCE>"}' /// -d '{"source": "<SOURCE>"}'
#[delete("/file/{id}/remove/")] #[post("/file/{id}/remove/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn remove( pub async fn remove(
id: web::Path<i64>, id: web::Path<i64>,
@ -526,8 +538,12 @@ pub async fn remove(
} }
} }
#[post("/file/{id}/upload/")] #[put("/file/{id}/upload/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn save_file(id: web::Path<i64>, payload: Multipart) -> Result<HttpResponse, ServiceError> { async fn save_file(
upload(*id, payload).await id: web::Path<i64>,
payload: Multipart,
obj: web::Query<FileObj>,
) -> Result<HttpResponse, ServiceError> {
upload(*id, payload, &obj.path).await
} }

View File

@ -152,7 +152,7 @@ pub struct MediaProbe {
} }
impl MediaProbe { impl MediaProbe {
fn new(input: &str) -> Self { pub fn new(input: &str) -> Self {
let probe = ffprobe(input); let probe = ffprobe(input);
let mut a_stream = vec![]; let mut a_stream = vec![];
let mut v_stream = vec![]; let mut v_stream = vec![];