Компилиться но не работает

This commit is contained in:
2026-05-06 17:43:44 +03:00
parent fde16de0c6
commit 576e2b68fc
9 changed files with 2567 additions and 371 deletions

View File

@@ -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(),
)
}