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]]
name = "ffplayout-api"
version = "0.3.3"
version = "0.4.0"
dependencies = [
"actix-multipart",
"actix-web",

View File

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

View File

@ -14,6 +14,9 @@ pub enum ServiceError {
#[display(fmt = "Unauthorized")]
Unauthorized,
#[display(fmt = "NoContent: {}", _0)]
NoContent(String),
}
// 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::Conflict(ref message) => HttpResponse::Conflict().json(message),
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 crate::utils::{errors::ServiceError, playout_config};
use ffplayout_lib::utils::file_extension;
use ffplayout_lib::utils::{file_extension, MediaProbe};
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PathObject {
pub source: String,
parent: Option<String>,
folders: Option<Vec<String>>,
files: Option<Vec<String>>,
files: Option<Vec<VideoFile>>,
}
impl PathObject {
@ -37,6 +37,12 @@ pub struct MoveObject {
target: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct VideoFile {
name: String,
duration: f64,
}
/// Normalize absolut 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 {
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 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() {
if let Some(ext) = file_extension(&file_path) {
if extensions.contains(&ext.to_string().to_lowercase()) {
if let Some(ref mut files) = obj.files {
files.push(path.file_name().unwrap().to_string_lossy().to_string());
let media = MediaProbe::new(&path.display().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)
}
@ -240,7 +258,7 @@ pub async fn remove_file_or_folder(id: i64, source_path: &String) -> Result<(),
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 (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()));
}
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? {
let content_disposition = field.content_disposition();
debug!("{content_disposition}");
@ -260,16 +282,12 @@ pub async fn upload(id: i64, mut payload: Multipart) -> Result<HttpResponse, Ser
.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.to_string()).await {
return Err(e);
}
let filepath = PathBuf::from(path_name).join(filename);
let target_path = valid_path(id, path).await?;
let filepath = target_path.join(filename);
if filepath.is_file() {
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 settings(channel_name, preview_url, config_path, extra_extensions, timezone, service)
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 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'),

View File

@ -37,11 +37,10 @@ pub async fn read_playlist(id: i64, date: String) -> Result<JsonPlaylist, Servic
.join(date.clone())
.with_extension("json");
if let Ok(p) = json_reader(&playlist_path) {
return Ok(p);
};
Err(ServiceError::InternalServerError)
match json_reader(&playlist_path) {
Ok(p) => Ok(p),
Err(e) => Err(ServiceError::NoContent(e.to_string())),
}
}
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},
Argon2, PasswordHasher, PasswordVerifier,
};
use serde::Serialize;
use serde::{Deserialize, Serialize};
use simplelog::*;
use crate::utils::{
@ -41,6 +41,18 @@ struct UserObj<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" \
/// -d '{"username": "<USER>", "password": "<PASS>" }'
#[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>'
#[get("/playlist/{id}/{date}")]
#[get("/playlist/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn get_playlist(
params: web::Path<(i64, String)>,
id: web::Path<i64>,
obj: web::Query<DateObj>,
) -> 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)),
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")]
pub async fn get_log(req: web::Path<String>) -> Result<impl Responder, ServiceError> {
let mut segments = req.split('/');
let id: i64 = segments.next().unwrap_or_default().parse().unwrap_or(0);
let date = segments.next().unwrap_or_default();
read_log_file(&id, date).await
pub async fn get_log(
id: web::Path<i64>,
log: web::Query<DateObj>,
) -> Result<impl Responder, ServiceError> {
read_log_file(&id, &log.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>'
/// -d '{"source": "<SOURCE>"}'
#[delete("/file/{id}/remove/")]
#[post("/file/{id}/remove/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn remove(
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")]
async fn save_file(id: web::Path<i64>, payload: Multipart) -> Result<HttpResponse, ServiceError> {
upload(*id, payload).await
async fn save_file(
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 {
fn new(input: &str) -> Self {
pub fn new(input: &str) -> Self {
let probe = ffprobe(input);
let mut a_stream = vec![];
let mut v_stream = vec![];