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]]
|
||||
name = "ffplayout-api"
|
||||
version = "0.3.3"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"actix-multipart",
|
||||
"actix-web",
|
||||
|
@ -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]
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()));
|
||||
|
@ -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'),
|
||||
|
@ -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> {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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![];
|
||||
|
Loading…
Reference in New Issue
Block a user