Files
Thing/src-tauri/src/download_engine/server.rs
T

166 lines
5.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::collections::HashMap;
use std::net::SocketAddr;
use axum::{
extract::{Path, State},
http::{HeaderMap, StatusCode},
response::Json,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter};
use super::engine::DownloadEngine;
use super::task::{DownloadTask, TaskStatus};
/// 扩展 HTTP API 服务器
pub struct ExtensionServer;
#[derive(Serialize)]
struct HealthResponse {
ok: bool,
version: &'static str,
}
#[derive(Deserialize)]
struct CreateDownloadRequest {
url: String,
#[serde(default)]
filename: Option<String>,
#[serde(default)]
dir: Option<String>,
#[serde(default)]
headers: HashMap<String, String>,
}
#[derive(Serialize)]
struct CreateDownloadResponse {
id: String,
}
#[derive(Serialize)]
struct ErrorResponse {
error: String,
}
impl ExtensionServer {
/// 启动 HTTP API 服务器(绑定到 127.0.0.1:port
pub async fn start(engine: DownloadEngine, port: u16, secret: String, app_handle: AppHandle) {
let addr: SocketAddr = format!("127.0.0.1:{}", port).parse().expect("无效端口");
let app = Router::new()
.route("/health", get(health))
.route("/api/downloads", post(create_download).get(list_downloads))
.route("/api/downloads/:id", axum::routing::delete(remove_download))
.with_state(AppState {
engine,
secret,
app_handle,
});
let listener = match tokio::net::TcpListener::bind(&addr).await {
Ok(l) => l,
Err(e) => {
crate::logger::log_error("download", &format!("扩展 HTTP 服务启动失败 ({}): {}", addr, e));
return;
}
};
crate::logger::log_info("download", &format!("扩展 HTTP 服务已启动: http://{}", addr));
if let Err(e) = axum::serve(listener, app).await {
crate::logger::log_error("download", &format!("扩展 HTTP 服务异常: {}", e));
}
}
}
#[derive(Clone)]
struct AppState {
engine: DownloadEngine,
secret: String,
app_handle: AppHandle,
}
/// 鉴权检查:如果配置了 secret,校验 Bearer token
fn check_auth(headers: &HeaderMap, secret: &str) -> bool {
if secret.is_empty() {
return true;
}
if let Some(auth) = headers.get("authorization") {
if let Ok(s) = auth.to_str() {
if let Some(token) = s.strip_prefix("Bearer ") {
return token == secret;
}
}
}
false
}
async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
let _ = state; // 不需要鉴权
Json(HealthResponse {
ok: true,
version: "thing-download-engine/1.0",
})
}
async fn create_download(
State(state): State<AppState>,
headers: HeaderMap,
Json(req): Json<CreateDownloadRequest>,
) -> Result<Json<CreateDownloadResponse>, (StatusCode, Json<ErrorResponse>)> {
if !check_auth(&headers, &state.secret) {
return Err((StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "未授权".into() })));
}
// 去重:同 URL 已有非终态任务(活跃/排队/暂停)时直接返回既有任务,
// 避免浏览器重复转发同一下载造成重复下载
if let Some(existing) = state.engine.get_tasks().into_iter().find(|t| {
matches!(t.status, TaskStatus::Active | TaskStatus::Queued | TaskStatus::Paused) && t.url == req.url
}) {
return Ok(Json(CreateDownloadResponse { id: existing.id }));
}
match state.engine.add_task(req.url, req.filename, req.dir, req.headers, true, None).await {
Ok(id) => {
// 浏览器扩展发起下载:不再置前主窗口,改为带 task id 通知前端,
// 由前端为该任务创建一个专属的一次性下载窗口(不打断主界面)
let _ = state.app_handle.emit(
crate::constants::events::DOWNLOAD_EXTENSION_ADDED,
serde_json::json!({ "id": id }),
);
Ok(Json(CreateDownloadResponse { id }))
}
Err(e) => Err((StatusCode::BAD_REQUEST, Json(ErrorResponse { error: e }))),
}
}
async fn list_downloads(
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<Vec<DownloadTask>>, (StatusCode, Json<ErrorResponse>)> {
if !check_auth(&headers, &state.secret) {
return Err((StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "未授权".into() })));
}
Ok(Json(state.engine.get_tasks()))
}
async fn remove_download(
State(state): State<AppState>,
headers: HeaderMap,
Path(id): Path<String>,
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
if !check_auth(&headers, &state.secret) {
return Err((StatusCode::UNAUTHORIZED, Json(ErrorResponse { error: "未授权".into() })));
}
match state.engine.remove_task(&id, false) {
Ok(()) => {
// 通知前端刷新任务列表(扩展删除时前端无从感知,否则列表残留已删除任务)
let _ = state.app_handle.emit(crate::constants::events::DOWNLOAD_REMOVED, serde_json::json!({ "id": id }));
Ok(StatusCode::NO_CONTENT)
}
Err(e) => Err((StatusCode::NOT_FOUND, Json(ErrorResponse { error: e }))),
}
}