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:
parent
8a1033b23f
commit
cb65d8084f
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -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",
|
||||||
|
@ -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]
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()));
|
||||||
|
@ -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'),
|
||||||
|
@ -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> {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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![];
|
||||||
|
Loading…
Reference in New Issue
Block a user