feat(desktop): native download and working context menu actions

This commit is contained in:
2026-02-18 19:25:52 +08:00
parent 9da90f38cc
commit 24ac734503
2 changed files with 200 additions and 21 deletions

View File

@@ -2,6 +2,10 @@ use reqwest::Method;
use reqwest::StatusCode;
use serde::Serialize;
use serde_json::{Map, Value};
use std::env;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::Duration;
struct ApiState {
@@ -42,6 +46,68 @@ fn fallback_json(status: StatusCode, text: &str) -> Value {
Value::Object(data)
}
fn sanitize_file_name(name: &str) -> String {
let raw = name.trim();
let mut cleaned = String::with_capacity(raw.len());
for ch in raw.chars() {
if matches!(ch, '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*' | '\0') {
cleaned.push('_');
} else {
cleaned.push(ch);
}
}
let normalized = cleaned.trim().trim_matches('.').to_string();
if normalized.is_empty() {
"download.bin".to_string()
} else {
normalized
}
}
fn resolve_download_dir() -> PathBuf {
if let Some(home) = env::var_os("USERPROFILE") {
return PathBuf::from(home).join("Downloads");
}
if let Some(home) = env::var_os("HOME") {
return PathBuf::from(home).join("Downloads");
}
PathBuf::from(".")
}
fn split_file_name(name: &str) -> (String, String) {
if let Some(index) = name.rfind('.') {
if index > 0 && index < name.len() - 1 {
let stem = name[..index].to_string();
let ext = name[index + 1..].to_string();
return (stem, ext);
}
}
(name.to_string(), String::new())
}
fn alloc_download_path(download_dir: &Path, preferred_name: &str) -> PathBuf {
let safe_name = sanitize_file_name(preferred_name);
let first = download_dir.join(&safe_name);
if !first.exists() {
return first;
}
let (stem, ext) = split_file_name(&safe_name);
for index in 1..10000 {
let candidate_name = if ext.is_empty() {
format!("{} ({})", stem, index)
} else {
format!("{} ({}).{}", stem, index, ext)
};
let candidate = download_dir.join(candidate_name);
if !candidate.exists() {
return candidate;
}
}
first
}
async fn request_json(
client: &reqwest::Client,
method: Method,
@@ -451,6 +517,84 @@ async fn api_create_direct_link(
.await
}
#[tauri::command]
async fn api_native_download(
state: tauri::State<'_, ApiState>,
url: String,
file_name: Option<String>,
) -> Result<BridgeResponse, String> {
let trimmed_url = url.trim().to_string();
if trimmed_url.is_empty() {
return Err("下载地址不能为空".to_string());
}
let response = state
.client
.get(&trimmed_url)
.send()
.await
.map_err(|err| format!("下载请求失败: {}", err))?;
let status = response.status();
if !status.is_success() {
return Ok(BridgeResponse {
ok: false,
status: status.as_u16(),
data: fallback_json(status, "下载失败"),
});
}
let preferred_name = file_name
.as_deref()
.map(|name| name.trim())
.filter(|name| !name.is_empty())
.unwrap_or("download.bin");
let download_dir = resolve_download_dir();
if !download_dir.exists() {
fs::create_dir_all(&download_dir)
.map_err(|err| format!("创建下载目录失败: {}", err))?;
}
let save_path = alloc_download_path(&download_dir, preferred_name);
let mut target_file =
fs::File::create(&save_path).map_err(|err| format!("创建文件失败: {}", err))?;
let mut downloaded_bytes: u64 = 0;
let mut stream = response;
while let Some(chunk) = stream
.chunk()
.await
.map_err(|err| format!("读取下载流失败: {}", err))?
{
target_file
.write_all(&chunk)
.map_err(|err| format!("写入文件失败: {}", err))?;
downloaded_bytes += chunk.len() as u64;
}
target_file
.flush()
.map_err(|err| format!("刷新文件失败: {}", err))?;
let mut data = Map::new();
data.insert("success".to_string(), Value::Bool(true));
data.insert(
"savePath".to_string(),
Value::String(save_path.to_string_lossy().to_string()),
);
data.insert(
"downloadedBytes".to_string(),
Value::Number(serde_json::Number::from(downloaded_bytes)),
);
Ok(BridgeResponse {
ok: true,
status: 200,
data: Value::Object(data),
})
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let client = reqwest::Client::builder()
@@ -475,7 +619,8 @@ pub fn run() {
api_get_my_shares,
api_create_share,
api_delete_share,
api_create_direct_link
api_create_direct_link,
api_native_download
])
.run(tauri::generate_context!())
.expect("error while running tauri application");