166 lines
5.3 KiB
Rust
166 lines
5.3 KiB
Rust
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 }))),
|
||
}
|
||
}
|