chore(tauri): 移除已弃用的 Rust 内嵌后端(消除 52 个编译警告)

- 删除 server/storage/models/events 四模块(axum :10801 后端,功能已全部由 Python 后端 :10085 承担)
- lib.rs:删除 mod 声明,Rust 侧仅保留窗口与生命周期
- Cargo.toml:移除 axum/tower-http/tower/tokio/uuid/chrono/dirs/futures/rust-embed 等 12 个依赖
- cargo check 0 警告,编译更快
This commit is contained in:
Pine
2026-08-18 01:43:25 +08:00
parent 4e8ae4024a
commit 0dd63b1558
7 changed files with 2 additions and 1268 deletions
-263
View File
@@ -224,71 +224,6 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "axum"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
dependencies = [
"axum-core",
"axum-macros",
"bytes",
"form_urlencoded",
"futures-util",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"multer",
"percent-encoding",
"pin-project-lite",
"serde_core",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper",
"tokio",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
dependencies = [
"bytes",
"futures-core",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-macros"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "base64"
version = "0.21.7"
@@ -536,10 +471,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link 0.2.1",
]
@@ -953,15 +886,6 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "endi"
version = "1.1.1"
@@ -1132,21 +1056,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.32"
@@ -1154,7 +1063,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@@ -1222,7 +1130,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@@ -1614,24 +1521,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-range-header"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
name = "hyper"
version = "1.9.0"
@@ -1645,7 +1540,6 @@ dependencies = [
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
@@ -2102,12 +1996,6 @@ dependencies = [
"web_atoms",
]
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "memchr"
version = "2.8.0"
@@ -2129,16 +2017,6 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -2181,23 +2059,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "multer"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
dependencies = [
"bytes",
"encoding_rs",
"futures-util",
"http",
"httparse",
"memchr",
"mime",
"spin",
"version_check",
]
[[package]]
name = "ndk"
version = "0.9.0"
@@ -2932,40 +2793,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rust-embed"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.117",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
dependencies = [
"sha2",
"walkdir",
]
[[package]]
name = "rustc-hash"
version = "2.1.2"
@@ -3000,12 +2827,6 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "same-file"
version = "1.0.6"
@@ -3167,17 +2988,6 @@ dependencies = [
"zmij",
]
[[package]]
name = "serde_path_to_error"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
dependencies = [
"itoa",
"serde",
"serde_core",
]
[[package]]
name = "serde_repr"
version = "0.1.20"
@@ -3207,18 +3017,6 @@ dependencies = [
"serde_core",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_with"
version = "3.20.0"
@@ -3391,12 +3189,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
@@ -3948,37 +3740,11 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "tokio-stream"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
@@ -4125,7 +3891,6 @@ dependencies = [
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
@@ -4136,19 +3901,10 @@ checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51"
dependencies = [
"bitflags 2.11.1",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"http-range-header",
"httpdate",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite",
"tokio",
"tokio-util",
"tower",
"tower-layer",
"tower-service",
@@ -4173,7 +3929,6 @@ version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@@ -4291,12 +4046,6 @@ dependencies = [
"unic-common",
]
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -5390,21 +5139,9 @@ dependencies = [
name = "昆明大学生创业园展播系统"
version = "0.1.0"
dependencies = [
"axum",
"chrono",
"dirs 6.0.0",
"futures",
"once_cell",
"rust-embed",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-autostart",
"tauri-plugin-opener",
"tokio",
"tokio-stream",
"tower",
"tower-http",
"uuid",
]
-12
View File
@@ -17,15 +17,3 @@ tauri = { version = "2", features = ["macos-private-api"] }
tauri-plugin-opener = "2"
tauri-plugin-autostart = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
axum = { version = "0.8", features = ["macros", "multipart"] }
tower-http = { version = "0.6", features = ["cors", "fs"] }
uuid = { version = "1", features = ["v4"] }
chrono = { version = "0.4", features = ["serde"] }
tower = "0.5"
once_cell = "1"
dirs = "6"
futures = "0.3"
tokio-stream = { version = "0.1", features = ["sync"] }
rust-embed = "8"
-29
View File
@@ -1,29 +0,0 @@
use crate::models::ServerEvent;
use once_cell::sync::Lazy;
use tokio::sync::broadcast;
/// SSE 事件管理器
pub struct EventManager {
tx: broadcast::Sender<String>,
}
impl EventManager {
pub fn new() -> Self {
let (tx, _) = broadcast::channel(64);
Self { tx }
}
/// 广播事件给所有 SSE 客户端
pub fn broadcast(&self, event: &ServerEvent) {
if let Ok(json) = serde_json::to_string(event) {
let _ = self.tx.send(json);
}
}
/// 获取广播接收器
pub fn subscribe(&self) -> broadcast::Receiver<String> {
self.tx.subscribe()
}
}
pub static EVENTS: Lazy<EventManager> = Lazy::new(EventManager::new);
+2 -4
View File
@@ -1,7 +1,5 @@
mod events;
mod models;
mod server;
mod storage;
// 说明:早期 Rust 内嵌后端(server/storage/models/events,axum :10801)已随架构迁移到
// Python 后端(FastAPI :10085)而移除;当前 Rust 侧仅负责窗口与生命周期。
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
-171
View File
@@ -1,171 +0,0 @@
use serde::{Deserialize, Serialize};
// ============ Settings ============
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Settings {
pub username: String,
pub password: String,
pub volume: u32,
pub auto_play: bool,
pub play_mode: String,
pub image_duration: u32,
pub fullscreen: bool,
pub autostart: bool,
}
impl Default for Settings {
fn default() -> Self {
Self {
username: "admin".into(),
password: "123456".into(),
volume: 80,
auto_play: true,
play_mode: "sequential".into(),
image_duration: 5,
fullscreen: true,
autostart: false,
}
}
}
// ============ Playlist Item ============
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaylistItem {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
// ============ URL Media Item ============
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UrlMediaItem {
pub url: String,
pub name: String,
#[serde(rename = "type")]
pub media_type: String,
pub added_at: String,
}
// ============ Media File (response) ============
#[derive(Debug, Clone, Serialize)]
pub struct MediaFile {
pub name: String,
pub relative_path: String,
#[serde(rename = "type")]
pub file_type: String,
pub url: String,
pub source: String,
}
// ============ Playback State ============
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlaybackState {
pub status: String,
pub index: usize,
pub name: String,
#[serde(rename = "type")]
pub media_type: String,
}
impl Default for PlaybackState {
fn default() -> Self {
Self {
status: "idle".into(),
index: 0,
name: String::new(),
media_type: String::new(),
}
}
}
// ============ Events ============
#[derive(Debug, Clone, Serialize)]
pub struct ServerEvent {
pub action: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<PlaybackState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub play_mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_duration: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fullscreen: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub autostart: Option<bool>,
}
// ============ Playlist Response ============
#[derive(Debug, Clone, Serialize)]
pub struct PlaylistResponse {
pub files: Vec<MediaFile>,
pub volume: u32,
pub play_mode: String,
pub image_duration: u32,
}
// ============ API Request Bodies ============
#[derive(Debug, Deserialize)]
pub struct LoginForm {
pub username: String,
pub password: String,
}
#[derive(Debug, Serialize)]
pub struct LoginResponse {
pub success: bool,
}
#[derive(Debug, Deserialize)]
pub struct SettingsUpdate {
pub volume: Option<u32>,
pub play_mode: Option<String>,
pub image_duration: Option<u32>,
pub fullscreen: Option<bool>,
pub autostart: Option<bool>,
}
#[derive(Debug, Deserialize)]
pub struct PathBody {
pub path: String,
}
#[derive(Debug, Deserialize)]
pub struct UrlBody {
pub url: String,
}
#[derive(Debug, Deserialize)]
pub struct ActionBody {
pub action: String,
}
#[derive(Debug, Deserialize)]
pub struct StateBody {
pub state: PlaybackState,
}
#[derive(Debug, Serialize)]
pub struct OkResponse {
pub ok: bool,
}
#[derive(Debug, Serialize)]
pub struct OkDuplicateResponse {
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub duplicate: Option<bool>,
}
#[derive(Debug, Serialize)]
pub struct FilesResponse {
pub files: Vec<MediaFile>,
}
#[derive(Debug, Serialize)]
pub struct StateResponse {
pub state: PlaybackState,
}
-586
View File
@@ -1,586 +0,0 @@
use crate::events::EVENTS;
use crate::models::*;
use crate::storage::{get_media_type, is_allowed_extension, STORAGE};
use axum::{
body::Body,
extract::{DefaultBodyLimit, Form, Multipart, State},
http::{Response, StatusCode, Uri},
response::sse::{Event, KeepAlive, Sse},
response::Json,
routing::{get, post},
Router,
};
use chrono::Local;
use futures::stream::Stream;
use rust_embed::RustEmbed;
use std::convert::Infallible;
use std::path::PathBuf;
use std::sync::Arc;
use std::fs;
use tokio::io::AsyncWriteExt;
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::StreamExt;
use tower_http::cors::CorsLayer;
use tower_http::services::ServeDir;
// ===================== 嵌入式前端文件 =====================
// dist/ 编译进二进制,无需文件系统依赖
#[derive(RustEmbed)]
#[folder = "../dist"]
struct Asset;
fn mime_type(path: &str) -> &'static str {
let ext = path.rsplit('.').next().unwrap_or("");
match ext {
"html" => "text/html; charset=utf-8",
"css" => "text/css; charset=utf-8",
"js" => "application/javascript; charset=utf-8",
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"svg" => "image/svg+xml",
"ico" => "image/x-icon",
"json" => "application/json",
"woff2" => "font/woff2",
"woff" => "font/woff",
"ttf" => "font/ttf",
_ => "application/octet-stream",
}
}
async fn spa_fallback(uri: Uri) -> Response<Body> {
let path = uri.path().trim_start_matches('/');
// 1. 尝试从嵌入式文件提供
if !path.is_empty() && !path.starts_with("api/") {
if let Some(file) = Asset::get(path) {
return Response::builder()
.header("Content-Type", mime_type(path))
.body(Body::from(file.data.into_owned()))
.unwrap();
}
}
// 2. SPA 回退:所有非 API 路径都返回 index.html
if !path.starts_with("api/") {
if let Some(file) = Asset::get("index.html") {
return Response::builder()
.header("Content-Type", "text/html; charset=utf-8")
.body(Body::from(file.data.into_owned()))
.unwrap();
}
}
// 3. 文件系统回退(开发模式可用)
let dist = dist_dir();
let file_path = dist.join(path);
if let Ok(data) = fs::read(&file_path) {
return Response::builder()
.header("Content-Type", mime_type(path))
.body(Body::from(data))
.unwrap();
}
let index = dist.join("index.html");
if let Ok(data) = fs::read(&index) {
return Response::builder()
.header("Content-Type", "text/html; charset=utf-8")
.body(Body::from(data))
.unwrap();
}
Response::builder()
.status(StatusCode::NOT_FOUND)
.header("Content-Type", "text/plain; charset=utf-8")
.body(Body::from("Not Found"))
.unwrap()
}
// ===================== 媒体目录 =====================
fn media_dir() -> PathBuf {
let mut path = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
path.push("Downloads");
path.push("Media");
fs::create_dir_all(&path).ok();
path
}
fn media_dir_str() -> String {
media_dir().to_string_lossy().to_string()
}
#[derive(Clone)]
pub struct AppState {
pub playback: Arc<tokio::sync::Mutex<PlaybackState>>,
}
impl AppState {
pub fn new() -> Self {
Self {
playback: Arc::new(tokio::sync::Mutex::new(PlaybackState::default())),
}
}
}
/// 智能查找 dist 目录(前端静态文件)
/// 支持多种运行场景:
/// - tauri dev 时 CWD = src-tauri/ → ../dist
/// - 二进制在 target/release/dpm → ../../dist
/// - 从项目根目录运行时 → dist
fn dist_dir() -> PathBuf {
let candidates: Vec<PathBuf> = {
let mut c = Vec::new();
// 1. 相对于当前工作目录
c.push(PathBuf::from("dist"));
c.push(PathBuf::from("../dist"));
// 2. 相对于可执行文件路径
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
// 开发模式: 二进制在 src-tauri/target/debug/dpm → dist 在项目根目录
c.push(dir.join("../../../dist"));
c.push(dir.join("../../dist"));
// macOS .app bundle 资源目录: dpm.app/Contents/Resources/dist/
c.push(dir.join("../Resources/dist"));
// Windows/Linux: 资源在二进制同目录下的 dist/
c.push(dir.join("dist"));
// Windows NSIS 安装: 资源在 resources/dist/
c.push(dir.join("resources/dist"));
}
}
c
};
for p in &candidates {
let canonical = p.canonicalize();
if let Ok(path) = canonical {
if path.join("index.html").exists() {
return path;
}
}
}
// Fallback
let fallback = PathBuf::from("../dist");
eprintln!("[dpm] 警告: 找不到 dist 目录 (已尝试 {:?}),使用: {:?}", candidates, fallback);
fallback
}
// ===================== 路由构建 =====================
pub fn create_router(state: AppState) -> Router {
Router::new()
.route("/api/login", post(login_handler))
.route("/api/settings", get(get_settings).post(update_settings))
.route("/api/display-command", post(display_command_handler))
.route("/media", get(list_media))
.route("/upload", post(upload_handler))
.route("/api/delete", post(delete_handler))
.route("/api/media/add-url", post(add_url_handler))
.route("/api/playlist", get(get_playlist))
.route("/api/playlist/add", post(add_playlist))
.route("/api/playlist/remove", post(remove_playlist))
.route("/api/control", post(control_handler))
.route("/api/state", get(get_state).post(update_state))
.route("/api/events", get(sse_handler))
// 媒体文件:用户目录 ~/Downloads/Media/
.nest_service("/file", ServeDir::new(media_dir_str()))
// SPA 回退 → 嵌入式文件(跨平台,编译到二进制中)
.fallback_service(get(spa_fallback))
.layer(DefaultBodyLimit::max(2048 * 1024 * 1024)) // 2GB 上传限制
.layer(CorsLayer::permissive())
.with_state(state)
}
pub async fn start() {
let state = AppState::new();
let app = create_router(state);
println!("🚀 大屏媒体轮播系统启动于 http://0.0.0.0:10801");
println!(" - 管理后台: http://localhost:10801/admin");
println!(" - 大屏展示: http://localhost:10801/screen");
let listener = tokio::net::TcpListener::bind("0.0.0.0:10801")
.await
.expect("无法绑定 0.0.0.0:10801,请检查端口是否被占用");
axum::serve(listener, app)
.await
.expect("服务器启动失败");
}
// ===================== Handler: 登录 =====================
async fn login_handler(
Form(form): Form<LoginForm>,
) -> Json<LoginResponse> {
let s = STORAGE.get_settings();
Json(LoginResponse {
success: form.username == s.username && form.password == s.password,
})
}
// ===================== Handler: 设置 =====================
async fn get_settings() -> Json<serde_json::Value> {
let s = STORAGE.get_settings();
Json(serde_json::json!({
"volume": s.volume,
"play_mode": s.play_mode,
"image_duration": s.image_duration,
"fullscreen": s.fullscreen,
"autostart": s.autostart,
}))
}
async fn update_settings(
Json(body): Json<SettingsUpdate>,
) -> Json<serde_json::Value> {
STORAGE.update_settings(SettingsUpdate {
volume: body.volume,
play_mode: body.play_mode.clone(),
image_duration: body.image_duration,
fullscreen: body.fullscreen,
autostart: body.autostart,
});
EVENTS.broadcast(&ServerEvent {
action: "settings_changed".into(),
state: None,
volume: body.volume,
play_mode: body.play_mode,
image_duration: body.image_duration,
fullscreen: body.fullscreen,
autostart: body.autostart,
});
Json(serde_json::json!({"ok": true}))
}
// ===================== Handler: 媒体列表 =====================
async fn list_media() -> Json<FilesResponse> {
let mut files = Vec::new();
let dir = media_dir();
// 本地文件
if let Ok(entries) = fs::read_dir(&dir) {
for entry in entries.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
let name = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
if !is_allowed_extension(&name) {
continue;
}
let rel = name.clone();
let t = get_media_type(&name).to_string();
files.push(MediaFile {
name,
relative_path: rel.clone(),
file_type: t,
url: format!("/file/{}", rel),
source: "local".into(),
});
}
}
// URL 媒体
for item in STORAGE.get_url_media() {
files.push(MediaFile {
name: item.name,
relative_path: item.url.clone(),
file_type: item.media_type,
url: item.url,
source: "url".into(),
});
}
Json(FilesResponse { files })
}
// ===================== Handler: 上传 =====================
async fn upload_handler(
mut multipart: Multipart,
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
while let Ok(Some(mut field)) = multipart.next_field().await {
let file_name = field.file_name().unwrap_or("file").to_string();
let ext = std::path::Path::new(&file_name)
.extension()
.and_then(|e| e.to_str())
.map(|e| format!(".{}", e.to_lowercase()))
.unwrap_or_default();
if !is_allowed_extension(&ext) {
return Err((StatusCode::BAD_REQUEST, "不支持的文件类型".into()));
}
let ts = Local::now().format("%Y%m%d%H%M%S");
let stem = std::path::Path::new(&file_name)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("file");
let new_name = format!("{}_{}{}", stem, ts, ext);
let save_path = media_dir().join(&new_name);
// 流式写入文件,避免大视频文件占用过多内存
let mut file = tokio::fs::File::create(&save_path).await.map_err(|e| {
(StatusCode::INTERNAL_SERVER_ERROR, format!("文件创建失败: {}", e))
})?;
loop {
match field.chunk().await {
Ok(Some(chunk)) => {
file.write_all(&chunk).await.map_err(|e| {
(StatusCode::INTERNAL_SERVER_ERROR, format!("写入文件失败: {}", e))
})?;
}
Ok(None) => break,
Err(e) => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!("读取上传数据失败: {}", e),
))
}
}
}
}
Ok(Json(serde_json::json!({"ok": true})))
}
// ===================== Handler: 删除 =====================
async fn delete_handler(
Json(body): Json<PathBody>,
) -> Json<OkResponse> {
let path = body.path;
if !path.starts_with("http://") && !path.starts_with("https://") {
let file_path = media_dir().join(&path);
if file_path.exists() && file_path.is_file() {
fs::remove_file(&file_path).ok();
}
}
STORAGE.delete_media(&path);
EVENTS.broadcast(&ServerEvent {
action: "playlist_changed".into(),
state: None,
volume: None,
play_mode: None,
image_duration: None,
fullscreen: None,
autostart: None,
});
Json(OkResponse { ok: true })
}
// ===================== Handler: 添加 URL =====================
async fn add_url_handler(
Json(body): Json<UrlBody>,
) -> Json<OkDuplicateResponse> {
let url = body.url.trim().to_string();
if url.is_empty() {
return Json(OkDuplicateResponse { ok: false, duplicate: None });
}
let name = url.rsplit('/').next().unwrap_or(&url)
.split('?').next().unwrap_or(&url)
.to_string();
let t = get_media_type(&url).to_string();
let is_new = STORAGE.add_url_media(&url, name, t);
Json(OkDuplicateResponse { ok: true, duplicate: Some(!is_new) })
}
// ===================== Handler: 播放列表 =====================
async fn get_playlist() -> Json<PlaylistResponse> {
let items = STORAGE.get_playlist();
let settings = STORAGE.get_settings();
let dir = media_dir();
let mut files = Vec::new();
for item in &items {
if item.source.as_deref() == Some("url") {
let ext = std::path::Path::new(&item.path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let t = if ["mp4", "mkv", "avi"].contains(&ext) { "video" } else { "image" };
files.push(MediaFile {
name: item.name.clone().unwrap_or_else(|| item.path.clone()),
relative_path: item.path.clone(),
file_type: t.to_string(),
url: item.path.clone(),
source: "url".into(),
});
} else {
let file_path = dir.join(&item.path);
if file_path.exists() {
let ext = file_path.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let t = if ["mp4", "mkv", "avi"].contains(&ext) { "video" } else { "image" };
files.push(MediaFile {
name: file_path.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&item.path)
.to_string(),
relative_path: item.path.clone(),
file_type: t.to_string(),
url: format!("/file/{}", item.path),
source: "local".into(),
});
}
}
}
Json(PlaylistResponse {
files,
volume: settings.volume,
play_mode: settings.play_mode,
image_duration: settings.image_duration,
})
}
async fn add_playlist(
Json(body): Json<PathBody>,
) -> Json<OkResponse> {
STORAGE.add_to_playlist(&body.path);
EVENTS.broadcast(&ServerEvent {
action: "playlist_changed".into(),
state: None,
volume: None,
play_mode: None,
image_duration: None,
fullscreen: None,
autostart: None,
});
Json(OkResponse { ok: true })
}
async fn remove_playlist(
Json(body): Json<PathBody>,
) -> Json<OkResponse> {
STORAGE.remove_from_playlist(&body.path);
EVENTS.broadcast(&ServerEvent {
action: "playlist_changed".into(),
state: None,
volume: None,
play_mode: None,
image_duration: None,
fullscreen: None,
autostart: None,
});
Json(OkResponse { ok: true })
}
// ===================== Handler: 播放控制 =====================
async fn control_handler(
State(state): State<AppState>,
Json(body): Json<ActionBody>,
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
if !["play", "pause", "next", "prev"].contains(&body.action.as_str()) {
return Err((StatusCode::BAD_REQUEST, "action 必须是 play/pause/next/prev".into()));
}
EVENTS.broadcast(&ServerEvent {
action: body.action.clone(),
state: None,
volume: None,
play_mode: None,
image_duration: None,
fullscreen: None,
autostart: None,
});
let mut playback = state.playback.lock().await;
playback.status = match body.action.as_str() {
"play" => "playing".into(),
"pause" => "paused".into(),
_ => playback.status.clone(),
};
let state_snapshot = playback.clone();
drop(playback);
EVENTS.broadcast(&ServerEvent {
action: "state_update".into(),
state: Some(state_snapshot),
volume: None,
play_mode: None,
image_duration: None,
fullscreen: None,
autostart: None,
});
Ok(Json(serde_json::json!({"ok": true, "action": body.action})))
}
// ===================== Handler: 显示命令(最小化、全屏等) =====================
async fn display_command_handler(
Json(body): Json<ActionBody>,
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
match body.action.as_str() {
"minimize" => {
EVENTS.broadcast(&ServerEvent {
action: "minimize_window".into(),
state: None,
volume: None,
play_mode: None,
image_duration: None,
fullscreen: None,
autostart: None,
});
Ok(Json(serde_json::json!({"ok": true, "action": "minimize"})))
}
_ => Err((StatusCode::BAD_REQUEST, "不支持的显示命令".into())),
}
}
// ===================== Handler: 状态 =====================
async fn get_state(
State(state): State<AppState>,
) -> Json<StateResponse> {
let s = state.playback.lock().await;
Json(StateResponse { state: s.clone() })
}
async fn update_state(
State(state): State<AppState>,
Json(body): Json<StateBody>,
) -> Json<OkResponse> {
{
let mut s = state.playback.lock().await;
if !body.state.status.is_empty() {
s.status = body.state.status.clone();
}
s.index = body.state.index;
if !body.state.name.is_empty() {
s.name = body.state.name.clone();
}
if !body.state.media_type.is_empty() {
s.media_type = body.state.media_type.clone();
}
}
let s2 = state.playback.lock().await;
EVENTS.broadcast(&ServerEvent {
action: "state_update".into(),
state: Some(s2.clone()),
volume: None,
play_mode: None,
image_duration: None,
fullscreen: None,
autostart: None,
});
Json(OkResponse { ok: true })
}
// ===================== Handler: SSE =====================
async fn sse_handler() -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
let rx = EVENTS.subscribe();
let stream = BroadcastStream::new(rx).map(|msg| match msg {
Ok(data) => Ok(Event::default().data(data)),
Err(_) => Ok(Event::default().data("")),
});
Sse::new(stream).keep_alive(
KeepAlive::new()
.interval(std::time::Duration::from_secs(15)),
)
}
-203
View File
@@ -1,203 +0,0 @@
use crate::models::*;
use once_cell::sync::Lazy;
use std::path::PathBuf;
use std::sync::Mutex;
/// 数据文件路径:系统数据目录 /dpm/data.json
fn db_path() -> PathBuf {
let mut path = dirs::data_dir().unwrap_or_else(|| PathBuf::from("."));
path.push("dpm");
std::fs::create_dir_all(&path).ok();
path.push("data.json");
path
}
/// 持久化数据结构
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct PersistentData {
settings: std::collections::HashMap<String, Settings>,
playlist: Vec<PlaylistItem>,
url_media: Vec<UrlMediaItem>,
}
impl Default for PersistentData {
fn default() -> Self {
let mut s = std::collections::HashMap::new();
s.insert("admin".into(), Settings::default());
Self {
settings: s,
playlist: Vec::new(),
url_media: Vec::new(),
}
}
}
/// 数据存储
pub struct Storage {
inner: Mutex<PersistentData>,
path: PathBuf,
}
impl Storage {
pub fn new() -> Self {
let path = db_path();
let data = if path.exists() {
std::fs::read_to_string(&path)
.ok()
.and_then(|c| serde_json::from_str(&c).ok())
.unwrap_or_default()
} else {
PersistentData::default()
};
let storage = Self {
inner: Mutex::new(data),
path,
};
storage.save();
storage
}
fn save(&self) {
if let Ok(data) = self.inner.lock() {
if let Ok(json) = serde_json::to_string_pretty(&*data) {
std::fs::write(&self.path, json).ok();
}
}
}
// === Settings ===
pub fn get_settings(&self) -> Settings {
self.inner
.lock()
.ok()
.and_then(|d| d.settings.get("admin").cloned())
.unwrap_or_default()
}
pub fn update_settings(&self, update: SettingsUpdate) {
if let Ok(mut data) = self.inner.lock() {
if let Some(s) = data.settings.get_mut("admin") {
if let Some(v) = update.volume {
s.volume = v;
}
if let Some(m) = update.play_mode {
s.play_mode = m;
}
if let Some(d) = update.image_duration {
s.image_duration = d;
}
if let Some(f) = update.fullscreen {
s.fullscreen = f;
}
if let Some(a) = update.autostart {
s.autostart = a;
}
}
}
self.save();
}
// === Playlist ===
pub fn get_playlist(&self) -> Vec<PlaylistItem> {
self.inner
.lock()
.ok()
.map(|d| d.playlist.clone())
.unwrap_or_default()
}
pub fn add_to_playlist(&self, path: &str) {
if let Ok(mut data) = self.inner.lock() {
if !data.playlist.iter().any(|p| p.path == path) {
let mut item = PlaylistItem {
path: path.into(),
source: None,
name: None,
};
if path.starts_with("http://") || path.starts_with("https://") {
item.source = Some("url".into());
if let Some(um) = data.url_media.iter().find(|u| u.url == path) {
item.name = Some(um.name.clone());
}
}
data.playlist.push(item);
}
}
self.save();
}
pub fn remove_from_playlist(&self, path: &str) {
if let Ok(mut data) = self.inner.lock() {
data.playlist.retain(|p| p.path != path);
}
self.save();
}
// === URL Media ===
pub fn add_url_media(&self, url: &str, name: String, media_type: String) -> bool {
let is_new = if let Ok(mut data) = self.inner.lock() {
if !data.url_media.iter().any(|u| u.url == url) {
data.url_media.push(UrlMediaItem {
url: url.into(),
name,
media_type,
added_at: chrono::Local::now()
.format("%Y-%m-%dT%H:%M:%S%.3f")
.to_string(),
});
true
} else {
false
}
} else {
false
};
if is_new {
self.save();
}
is_new
}
pub fn get_url_media(&self) -> Vec<UrlMediaItem> {
self.inner
.lock()
.ok()
.map(|d| d.url_media.clone())
.unwrap_or_default()
}
pub fn remove_url_media(&self, url: &str) {
if let Ok(mut data) = self.inner.lock() {
data.url_media.retain(|u| u.url != url);
}
self.save();
}
// === Delete ===
pub fn delete_media(&self, path: &str) {
if path.starts_with("http://") || path.starts_with("https://") {
self.remove_url_media(path);
}
self.remove_from_playlist(path);
}
}
// 全局共享实例
pub static STORAGE: Lazy<Storage> = Lazy::new(Storage::new);
/// 判断文件扩展名是否允许
pub fn is_allowed_extension(path: &str) -> bool {
let allowed = [".mp4", ".mkv", ".avi", ".jpg", ".jpeg", ".png"];
let lower = path.to_lowercase();
allowed.iter().any(|ext| lower.ends_with(ext))
}
/// 获取媒体类型
pub fn get_media_type(path: &str) -> &'static str {
let lower = path.to_lowercase();
if lower.ends_with(".mp4") || lower.ends_with(".mkv") || lower.ends_with(".avi") {
"video"
} else {
"image"
}
}