Компилиться но не работает
This commit is contained in:
167
src/web.rs
167
src/web.rs
@@ -1,8 +1,12 @@
|
||||
use crate::config::Config;
|
||||
use crate::config::{AccessType, Config};
|
||||
use crate::file_access::{
|
||||
FileAccess, LocalFileAccess, SmbAccessConfig, SmbFileAccess, SynologyFileAccess,
|
||||
};
|
||||
use crate::processor::{cleanup_finished_jobs, fetch_all_jobs, process_spreadsheet};
|
||||
use crate::synology::SynologyClient;
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::{header::CONTENT_TYPE, StatusCode},
|
||||
extract::{Path, State},
|
||||
http::{header, HeaderMap, StatusCode},
|
||||
response::{Html, IntoResponse, Json},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
@@ -17,6 +21,8 @@ use tower_http::trace::TraceLayer;
|
||||
pub struct AppState {
|
||||
pub config: Config,
|
||||
pub last_generation: Arc<Mutex<Option<chrono::DateTime<chrono::Local>>>>,
|
||||
pub source_access: Arc<dyn FileAccess>,
|
||||
pub dest_access: Arc<dyn FileAccess>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -77,10 +83,16 @@ impl JobInfo {
|
||||
}
|
||||
|
||||
pub async fn run_web_server(config: Config) -> anyhow::Result<()> {
|
||||
let source_access = create_file_access(&config.source, &config).await?;
|
||||
let dest_access = create_file_access(&config.destination, &config).await?;
|
||||
|
||||
let web_port = config.web_port;
|
||||
|
||||
let state = AppState {
|
||||
config,
|
||||
last_generation: Arc::new(Mutex::new(None)),
|
||||
source_access,
|
||||
dest_access,
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
@@ -106,48 +118,84 @@ pub async fn run_web_server(config: Config) -> anyhow::Result<()> {
|
||||
.route("/api/cleanup", post(cleanup_jobs))
|
||||
.route("/api/status", get(get_status))
|
||||
.route("/api/jobs/stop-all", post(stop_all_jobs))
|
||||
.route("/api/videos", get(list_videos))
|
||||
.route("/api/videos/{filename}", get(stream_video))
|
||||
.route("/api/videos/{filename}/move", post(move_video))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state);
|
||||
|
||||
let addr: SocketAddr = format!("0.0.0.0:{}", web_port).parse()?;
|
||||
log::info!("Web server listening on http://{}", addr);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(listener, app).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_file_access(
|
||||
access_config: &crate::config::AccessConfig,
|
||||
nas_config: &Config,
|
||||
) -> anyhow::Result<Arc<dyn FileAccess>> {
|
||||
match access_config.access_type {
|
||||
AccessType::Local => {
|
||||
let path = access_config.local_path.as_ref().unwrap();
|
||||
Ok(Arc::new(LocalFileAccess::new(path)))
|
||||
}
|
||||
AccessType::Smb => {
|
||||
let url = access_config.smb_url.as_ref().unwrap().clone();
|
||||
let user = access_config.smb_user.clone();
|
||||
let password = access_config.smb_pass.clone();
|
||||
let smb_config = SmbAccessConfig {
|
||||
url,
|
||||
user,
|
||||
password,
|
||||
};
|
||||
Ok(Arc::new(SmbFileAccess::new(smb_config)))
|
||||
}
|
||||
AccessType::Synology => {
|
||||
let mut client = SynologyClient::new(&nas_config.nas_fqdn);
|
||||
client
|
||||
.login(&nas_config.nas_user, &nas_config.nas_pass)
|
||||
.await?;
|
||||
let base_path = access_config.synology_path.as_ref().unwrap().clone();
|
||||
Ok(Arc::new(SynologyFileAccess::new(client, &base_path)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Статические ресурсы
|
||||
async fn index_page() -> Html<&'static str> {
|
||||
Html(include_str!("static/index.html"))
|
||||
}
|
||||
|
||||
async fn style_css() -> impl IntoResponse {
|
||||
(
|
||||
[(CONTENT_TYPE, "text/css")],
|
||||
[(header::CONTENT_TYPE, "text/css")],
|
||||
include_str!("static/style.css"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn fontawesome_css() -> impl IntoResponse {
|
||||
(
|
||||
[(CONTENT_TYPE, "text/css")],
|
||||
[(header::CONTENT_TYPE, "text/css")],
|
||||
include_str!("static/fontawesome/all.min.css"),
|
||||
)
|
||||
}
|
||||
|
||||
async fn logo_png() -> impl IntoResponse {
|
||||
(
|
||||
[(CONTENT_TYPE, "image/png")],
|
||||
[(header::CONTENT_TYPE, "image/png")],
|
||||
include_bytes!("../assets/logo.png").as_slice(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn favicon() -> impl IntoResponse {
|
||||
(
|
||||
[(CONTENT_TYPE, "image/x-icon")],
|
||||
[(header::CONTENT_TYPE, "image/x-icon")],
|
||||
include_bytes!("static/favicon.ico").as_slice(),
|
||||
)
|
||||
}
|
||||
|
||||
// Управление заданиями Nexrender
|
||||
async fn list_jobs(State(state): State<AppState>) -> Result<Json<Vec<JobInfo>>, AppError> {
|
||||
let jobs_json = fetch_all_jobs(&state.config.nexrender_api_url)
|
||||
.await
|
||||
@@ -235,23 +283,120 @@ async fn stop_all_jobs(State(state): State<AppState>) -> Result<impl IntoRespons
|
||||
Ok((StatusCode::OK, format!("Stopped {} jobs", stopped)))
|
||||
}
|
||||
|
||||
// Видео
|
||||
async fn list_videos(State(state): State<AppState>) -> Result<Json<Vec<String>>, AppError> {
|
||||
let files = state
|
||||
.source_access
|
||||
.list_files()
|
||||
.await
|
||||
.map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
let videos: Vec<String> = files
|
||||
.into_iter()
|
||||
.filter(|name| {
|
||||
let lower = name.to_lowercase();
|
||||
lower.ends_with(".mp4") || lower.ends_with(".mov") || lower.ends_with(".avi")
|
||||
})
|
||||
.collect();
|
||||
Ok(Json(videos))
|
||||
}
|
||||
|
||||
async fn stream_video(
|
||||
Path(filename): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let file_data = state
|
||||
.source_access
|
||||
.read_file(&filename)
|
||||
.await
|
||||
.map_err(|e| AppError(StatusCode::NOT_FOUND, format!("File not found: {}", e)))?;
|
||||
|
||||
let file_size = file_data.len() as u64;
|
||||
let range = headers.get(header::RANGE).and_then(|v| v.to_str().ok());
|
||||
|
||||
let (status, content_range, start, end) = if let Some(range_str) = range {
|
||||
let range_str = range_str.trim_start_matches("bytes=");
|
||||
let parts: Vec<&str> = range_str.split('-').collect();
|
||||
let start: u64 = parts[0].parse().unwrap_or(0);
|
||||
let end: u64 = if parts.len() > 1 && !parts[1].is_empty() {
|
||||
parts[1].parse().unwrap_or(file_size - 1)
|
||||
} else {
|
||||
file_size - 1
|
||||
};
|
||||
(
|
||||
StatusCode::PARTIAL_CONTENT,
|
||||
Some(format!("bytes {}-{}/{}", start, end, file_size)),
|
||||
start,
|
||||
end,
|
||||
)
|
||||
} else {
|
||||
(StatusCode::OK, None, 0, file_size - 1)
|
||||
};
|
||||
|
||||
let mut resp_headers = HeaderMap::new();
|
||||
resp_headers.insert(header::CONTENT_TYPE, "video/mp4".parse().unwrap());
|
||||
if let Some(content_range) = content_range {
|
||||
resp_headers.insert(header::CONTENT_RANGE, content_range.parse().unwrap());
|
||||
}
|
||||
let len = end - start + 1;
|
||||
resp_headers.insert(header::CONTENT_LENGTH, len.to_string().parse().unwrap());
|
||||
resp_headers.insert(header::ACCEPT_RANGES, "bytes".parse().unwrap());
|
||||
|
||||
let body = file_data[start as usize..=end as usize].to_vec();
|
||||
Ok((status, resp_headers, body))
|
||||
}
|
||||
|
||||
async fn move_video(
|
||||
Path(filename): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let remove = params.get("remove").map(|s| s == "true").unwrap_or(false);
|
||||
|
||||
let data = state
|
||||
.source_access
|
||||
.read_file(&filename)
|
||||
.await
|
||||
.map_err(|e| AppError(StatusCode::NOT_FOUND, e.to_string()))?;
|
||||
|
||||
state
|
||||
.dest_access
|
||||
.write_file(&filename, &data)
|
||||
.await
|
||||
.map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
if remove {
|
||||
state
|
||||
.source_access
|
||||
.delete_file(&filename)
|
||||
.await
|
||||
.map_err(|e| AppError(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
}
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
format!("File {} moved successfully", filename),
|
||||
))
|
||||
}
|
||||
|
||||
// Шрифты Font Awesome
|
||||
async fn fa_solid_woff2() -> impl IntoResponse {
|
||||
(
|
||||
[(CONTENT_TYPE, "font/woff2")],
|
||||
[(header::CONTENT_TYPE, "font/woff2")],
|
||||
include_bytes!("static/fontawesome/webfonts/fa-solid-900.woff2").as_slice(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn fa_regular_woff2() -> impl IntoResponse {
|
||||
(
|
||||
[(CONTENT_TYPE, "font/woff2")],
|
||||
[(header::CONTENT_TYPE, "font/woff2")],
|
||||
include_bytes!("static/fontawesome/webfonts/fa-regular-400.woff2").as_slice(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn fa_brands_woff2() -> impl IntoResponse {
|
||||
(
|
||||
[(CONTENT_TYPE, "font/woff2")],
|
||||
[(header::CONTENT_TYPE, "font/woff2")],
|
||||
include_bytes!("static/fontawesome/webfonts/fa-brands-400.woff2").as_slice(),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user