feat(desktop): add drag-and-drop upload for file view

This commit is contained in:
2026-02-18 19:46:11 +08:00
parent 2b36275c4a
commit 09043e8059
4 changed files with 292 additions and 21 deletions

View File

@@ -2050,6 +2050,16 @@ 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"
@@ -3085,6 +3095,7 @@ dependencies = [
"cookie",
"cookie_store",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
@@ -3093,6 +3104,7 @@ dependencies = [
"hyper-util",
"js-sys",
"log",
"mime_guess",
"percent-encoding",
"pin-project-lite",
"quinn",
@@ -4457,6 +4469,12 @@ dependencies = [
"unic-common",
]
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
version = "1.0.24"

View File

@@ -22,5 +22,5 @@ tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", default-features = false, features = ["json", "cookies", "rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "cookies", "multipart", "rustls-tls"] }
urlencoding = "2.1"

View File

@@ -595,6 +595,82 @@ async fn api_native_download(
})
}
#[tauri::command]
async fn api_upload_file(
state: tauri::State<'_, ApiState>,
base_url: String,
file_path: String,
target_path: String,
) -> Result<BridgeResponse, String> {
let trimmed_path = file_path.trim().to_string();
if trimmed_path.is_empty() {
return Err("上传文件路径不能为空".to_string());
}
let source_path = PathBuf::from(trimmed_path);
if !source_path.exists() {
return Err("上传文件不存在".to_string());
}
if !source_path.is_file() {
return Err("仅支持上传文件,不支持文件夹".to_string());
}
let file_name = source_path
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.to_string())
.ok_or_else(|| "无法识别文件名".to_string())?;
let file_bytes = fs::read(&source_path).map_err(|err| format!("读取文件失败: {}", err))?;
let normalized_target = if target_path.trim().is_empty() {
"/".to_string()
} else {
target_path
};
let csrf_token = fetch_csrf_token(&state.client, &base_url).await?;
let upload_url = join_api_url(&base_url, "/api/upload");
if upload_url.trim().is_empty() {
return Err("API 地址不能为空".to_string());
}
let multipart = reqwest::multipart::Form::new()
.text("path", normalized_target)
.part("file", reqwest::multipart::Part::bytes(file_bytes).file_name(file_name));
let mut request = state
.client
.post(&upload_url)
.header("Accept", "application/json")
.timeout(Duration::from_secs(60 * 30))
.multipart(multipart);
if let Some(csrf) = csrf_token {
request = request.header("X-CSRF-Token", csrf);
}
let response = request
.send()
.await
.map_err(|err| format!("上传请求失败: {}", err))?;
let status = response.status();
let text = response
.text()
.await
.map_err(|err| format!("读取响应失败: {}", err))?;
let data = match serde_json::from_str::<Value>(&text) {
Ok(parsed) => parsed,
Err(_) => fallback_json(status, &text),
};
Ok(BridgeResponse {
ok: status.is_success(),
status: status.as_u16(),
data,
})
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let client = reqwest::Client::builder()
@@ -620,7 +696,8 @@ pub fn run() {
api_create_share,
api_delete_share,
api_create_direct_link,
api_native_download
api_native_download,
api_upload_file
])
.run(tauri::generate_context!())
.expect("error while running tauri application");