diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 76dd103..d274437 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" [dependencies] eframe = "0.31" -reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "multipart", "blocking"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/desktop/src/main.rs b/desktop/src/main.rs index ef11375..d9fca7b 100644 --- a/desktop/src/main.rs +++ b/desktop/src/main.rs @@ -1,38 +1,32 @@ #![windows_subsystem = "windows"] use eframe::egui; -use reqwest::blocking::multipart; +use std::process::Command; use std::sync::mpsc; -#[derive(serde::Deserialize)] -struct UploadResponse { - share_url: Option, - error: Option, -} - -enum UploadResult { - Ok(String), - Err(String), -} +const SERVER: &str = "23.226.133.121"; +const PORT: &str = "10022"; +const USERNAME: &str = "root"; +const UPLOAD_DIR: &str = "/opt/temp-file-trans/repo/uploads"; +const DB_PATH: &str = "/opt/temp-file-trans/repo/files.db"; +const BASE_URL: &str = "https://xiaji-temp.duckdns.org"; struct App { - api_url: String, status: String, share_url: String, - upload_rx: Option>, uploading: bool, - skip_ssl: bool, + upload_rx: Option>>, + expiry_hours: i32, } impl App { fn new() -> Self { Self { - api_url: "https://xiaji-temp.duckdns.org/api/upload".to_owned(), status: "就绪,拖放文件到下方区域上传".to_owned(), share_url: String::new(), - upload_rx: None, uploading: false, - skip_ssl: false, + upload_rx: None, + expiry_hours: 24, } } @@ -40,11 +34,10 @@ impl App { let (tx, rx) = mpsc::channel(); self.upload_rx = Some(rx); self.uploading = true; - let api_url = self.api_url.clone(); - let skip_ssl = self.skip_ssl; + let expiry_hours = self.expiry_hours; std::thread::spawn(move || { - let result = do_upload(&api_url, &filepath, skip_ssl); + let result = do_ssh_upload(&filepath, expiry_hours); let _ = tx.send(result); }); } @@ -55,11 +48,11 @@ impl App { self.uploading = false; self.upload_rx = None; match result { - UploadResult::Ok(url) => { + Ok(url) => { self.status = "上传成功".to_owned(); self.share_url = url; } - UploadResult::Err(e) => { + Err(e) => { self.status = format!("上传失败: {}", e); self.share_url.clear(); } @@ -69,71 +62,50 @@ impl App { } } -fn do_upload(api_url: &str, filepath: &std::path::Path, skip_ssl: bool) -> UploadResult { +fn do_ssh_upload(filepath: &std::path::Path, expiry_hours: i32) -> Result { + let local_path = filepath.to_string_lossy(); + let filename = filepath .file_name() .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| "unknown".to_owned()); - let data = match std::fs::read(filepath) { - Ok(d) => d, - Err(e) => return UploadResult::Err(format!("读取文件失败: {}", e)), - }; + let filesize = std::fs::metadata(&filepath) + .map(|m| m.len() as i64) + .map_err(|e| format!("读取文件失败: {}", e))?; - let part = multipart::Part::bytes(data).file_name(filename); - let form = multipart::Form::new() - .part("file", part) - .text("expiry", "24h"); + let remote_dest_arg = format!("root@{}:{}/{}", SERVER, UPLOAD_DIR, filename); - let mut builder = reqwest::blocking::Client::builder() - .user_agent("TempFileTransfer-Desktop/0.2") - .timeout(std::time::Duration::from_secs(300)); + let scp_output = Command::new("scp") + .args(["-P", PORT, "-p", &local_path, &remote_dest_arg]) + .output() + .map_err(|e| format!("scp 执行失败: {}", e))?; - if skip_ssl { - builder = builder.danger_accept_invalid_certs(true); + if !scp_output.status.success() { + let stderr = String::from_utf8_lossy(&scp_output.stderr); + return Err(format!("scp 上传失败: {}", stderr.trim())); } - let client = match builder.build() { - Ok(c) => c, - Err(e) => return UploadResult::Err(format!("创建客户端失败: {}", e)), - }; + let python_script = format!( + "python3 -c \"import sqlite3,uuid,os,datetime; db='{}'; upload_dir='{}'; orig='{}'; fs={}; eh={}; fid=str(uuid.uuid4()); ext=os.path.splitext(orig)[1]; sn=fid+ext; rp=os.path.join(upload_dir,sn); os.rename(os.path.join(upload_dir,'{}'),rp); now=datetime.datetime.utcnow(); ex=now+datetime.timedelta(hours=eh); c=sqlite3.connect(db); c.execute('INSERT INTO files(id,filename,filepath,filesize,expiry_hours,created_at,expires_at) VALUES(?,?,?,?,?,?,?)',(fid,orig,rp,fs,eh,now,ex)); c.commit();c.close();print('{}/file/'+fid)\"", + DB_PATH, UPLOAD_DIR, filename, filesize, expiry_hours, filename, BASE_URL + ); - let resp = match client.post(api_url).multipart(form).send() { - Ok(r) => r, - Err(e) => { - let detail = if e.is_connect() { - format!("连接失败(无法连接到服务器): {}", e) - } else if e.is_timeout() { - format!("连接超时: {}", e) - } else if e.is_body() { - format!("SSL/数据传输出错(请尝试勾选「跳过SSL验证」): {}", e) - } else { - format!("{}", e) - }; - return UploadResult::Err(detail); - } - }; + let ssh_output = Command::new("ssh") + .args(["-p", PORT, &format!("{}@{}", USERNAME, SERVER), &python_script]) + .output() + .map_err(|e| format!("ssh 执行失败: {}", e))?; - let status = resp.status(); - let body = match resp.text() { - Ok(b) => b, - Err(e) => return UploadResult::Err(format!("读取响应失败: {}", e)), - }; - - if !status.is_success() { - let err: UploadResponse = serde_json::from_str(&body).unwrap_or(UploadResponse { - share_url: None, - error: Some(body), - }); - return UploadResult::Err(err.error.unwrap_or_else(|| format!("HTTP {}", status))); + if !ssh_output.status.success() { + let stderr = String::from_utf8_lossy(&ssh_output.stderr); + return Err(format!("SSH 命令执行失败: {}", stderr.trim())); } - match serde_json::from_str::(&body) { - Ok(r) => match r.share_url { - Some(url) => UploadResult::Ok(url), - None => UploadResult::Err(r.error.unwrap_or_else(|| "未知服务器响应".to_owned())), - }, - Err(e) => UploadResult::Err(format!("解析响应失败: {} - body: {}", e, body)), + let stdout = String::from_utf8_lossy(&ssh_output.stdout).trim().to_string(); + if stdout.starts_with("https://") { + Ok(stdout) + } else { + Err(format!("服务器返回异常: {}", stdout)) } } @@ -147,31 +119,34 @@ impl eframe::App for App { ui.heading("临时文件传输"); ui.add_space(8.0); - - ui.horizontal(|ui| { - ui.label("API 地址:"); - ui.add_sized( - [400.0, 24.0], - egui::TextEdit::singleline(&mut self.api_url) - .hint_text("https://xiaji-temp.duckdns.org/api/upload"), - ); - }); + ui.label( + egui::RichText::new("通过 SSH 上传 · 服务器: 23.226.133.121:10022") + .size(12.0) + .color(egui::Color32::from_gray(150)), + ); ui.add_space(4.0); ui.horizontal(|ui| { - ui.checkbox(&mut self.skip_ssl, "跳过 SSL 证书验证(仅当连接失败时尝试)"); + ui.label("过期时间:"); + egui::ComboBox::from_id_salt("expiry") + .selected_text(format!("{} 小时", self.expiry_hours)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.expiry_hours, 1, "1 小时"); + ui.selectable_value(&mut self.expiry_hours, 24, "24 小时"); + ui.selectable_value(&mut self.expiry_hours, 168, "7 天"); + }); }); - ui.add_space(8.0); + ui.add_space(12.0); let drop_frame = egui::Frame::dark_canvas(ui.style()) .inner_margin(egui::Margin::same(40)) .corner_radius(egui::CornerRadius::same(12)); drop_frame.show(ui, |ui| { - ui.set_min_size(egui::vec2(440.0, 200.0)); + ui.set_min_size(egui::vec2(440.0, 180.0)); ui.vertical_centered(|ui| { - ui.add_space(60.0); + ui.add_space(50.0); if self.uploading { ui.add(egui::Spinner::new()); ui.add_space(8.0); @@ -184,7 +159,7 @@ impl eframe::App for App { ); ui.add_space(4.0); ui.label( - egui::RichText::new("最大 500MB · 过期 24小时") + egui::RichText::new("最大 500MB · SSH 直传") .size(14.0) .color(egui::Color32::from_gray(140)), ); @@ -281,7 +256,7 @@ fn load_chinese_font(ctx: &egui::Context) { fn main() { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() - .with_inner_size([560.0, 500.0]) + .with_inner_size([560.0, 440.0]) .with_resizable(false) .with_title("临时文件传输"), ..Default::default() @@ -295,4 +270,4 @@ fn main() { Ok(Box::new(App::new())) }), ); -} +} \ No newline at end of file