From 73b3425f5ff3cd75b71ac5e5c199f80e71981f53 Mon Sep 17 00:00:00 2001 From: Sunho Kim Date: Fri, 28 Jun 2024 02:37:24 -0700 Subject: [PATCH 1/2] [feat] optimize tauri write binary file --- src-tauri/Cargo.toml | 4 +++ src-tauri/src/main.rs | 56 ++++++++++++++++++++++++++++++++++++- src/ts/storage/globalApi.ts | 28 ++++++++++++++----- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 325a06ed..91de72f9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,6 +23,10 @@ zip = "0.6.6" tar = "0.4.40" eventsource-client = "0.12.2" futures = "0.3.30" +actix-web = "4.0" +actix-cors = "0.6" +actix-rt = "2.5" +url = "2.2" [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d708daff..8e8dabfe 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -15,6 +15,10 @@ use std::io::Write; use std::{time::Duration, path::Path}; use serde_json::json; use std::collections::HashMap; +use actix_cors::Cors; +use tauri::api::path::app_data_dir; +use actix_web::{web, HttpRequest, HttpResponse, HttpServer, Responder, App, post, get}; +use std::fs::File; #[tauri::command] async fn native_request(url: String, body: String, header: String, method:String) -> String { @@ -448,6 +452,33 @@ async fn streamed_fetch(id:String, url:String, headers: String, body: String, ha } } +#[post("/")] +async fn write_binary_file_to_appdata(req: HttpRequest, body: web::Bytes, app_handle: web::Data) -> impl Responder { + let query = req.query_string(); + let params: std::collections::HashMap<_, _> = url::form_urlencoded::parse(query.as_bytes()).into_owned().collect(); + let app_data_dir = app_data_dir(&app_handle.config()).expect("App dir must be returned by tauri"); + if let Some(file_path) = params.get("path") { + let full_path = app_data_dir.join(file_path); + if let Some(parent) = Path::new(&full_path).parent() { + if let Err(e) = std::fs::create_dir_all(parent) { + return HttpResponse::InternalServerError().body(format!("Failed to create directories: {}", e)); + } + } + + match File::create(&full_path) { + Ok(mut file) => { + if let Err(e) = file.write_all(&body) { + return HttpResponse::InternalServerError().body(format!("Failed to write to file: {}", e)); + } + HttpResponse::Ok().body("File written successfully") + } + Err(e) => HttpResponse::InternalServerError().body(format!("Failed to create file: {}", e)), + } + } else { + HttpResponse::BadRequest().body("Missing file path in query string") + } +} + fn main() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ @@ -463,6 +494,29 @@ fn main() { install_py_dependencies, streamed_fetch ]) + .setup(|app| { + let handle = app.handle().clone(); + tauri::async_runtime::spawn(async move { + HttpServer::new(move || { + App::new() + .wrap( + Cors::default() + .allow_any_origin() + .allow_any_method() + .allow_any_header() + .max_age(3600) + ) + .app_data(web::PayloadConfig::new(1024 * 1024 * 1024)) // 1 GB + .app_data(web::Data::new(handle.clone())) + .service(write_binary_file_to_appdata) + }) + .bind(("127.0.0.1", 5354)) + .expect("Failed to bind to port 5354") + .run() + .await; + }); + Ok(()) + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } @@ -474,4 +528,4 @@ fn header_map_to_json(header_map: &HeaderMap) -> serde_json::Value { map.insert(key.as_str().to_string(), value.to_str().unwrap().to_string()); } json!(map) -} \ No newline at end of file +} diff --git a/src/ts/storage/globalApi.ts b/src/ts/storage/globalApi.ts index 89a4c23a..69d07051 100644 --- a/src/ts/storage/globalApi.ts +++ b/src/ts/storage/globalApi.ts @@ -55,6 +55,22 @@ interface fetchLog{ let fetchLog:fetchLog[] = [] +async function writeBinaryFileFast(appPath: string, data: Uint8Array) { + const apiUrl = `http://127.0.0.1:5354/?path=${encodeURIComponent(appPath)}`; + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/octet-stream' + }, + body: new Blob([data]) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } +} + export async function downloadFile(name:string, dat:Uint8Array|ArrayBuffer|string) { if(typeof(dat) === 'string'){ dat = Buffer.from(dat, 'utf-8') @@ -233,7 +249,7 @@ export async function saveAsset(data:Uint8Array, customId:string = '', fileName: fileExtension = fileName.split('.').pop() } if(isTauri){ - await writeBinaryFile(`assets/${id}.${fileExtension}`, data ,{dir: BaseDirectory.AppData}) + await writeBinaryFileFast(`assets/${id}.${fileExtension}`, data); return `assets/${id}.${fileExtension}` } else{ @@ -299,8 +315,8 @@ export async function saveDb(){ db.saveTime = Math.floor(Date.now() / 1000) const dbData = encodeRisuSave(db) if(isTauri){ - await writeBinaryFile('database/database.bin', dbData, {dir: BaseDirectory.AppData}) - await writeBinaryFile(`database/dbbackup-${(Date.now()/100).toFixed()}.bin`, dbData, {dir: BaseDirectory.AppData}) + await writeBinaryFileFast('database/database.bin', dbData); + await writeBinaryFileFast(`database/dbbackup-${(Date.now()/100).toFixed()}.bin`, dbData); } else{ if(!forageStorage.isAccount){ @@ -393,9 +409,7 @@ export async function loadData() { await createDir('assets', {dir: BaseDirectory.AppData}) } if(!await exists('database/database.bin', {dir: BaseDirectory.AppData})){ - await writeBinaryFile('database/database.bin', - encodeRisuSave({}) - ,{dir: BaseDirectory.AppData}) + await writeBinaryFileFast('database/database.bin', encodeRisuSave({})); } try { setDatabase( @@ -1586,4 +1600,4 @@ export class BlankWriter{ async end(){ //do nothing, just to make compatible with other writer } -} \ No newline at end of file +} From 079410f70da42cb48fa598146672ac46224fd4a2 Mon Sep 17 00:00:00 2001 From: Sunho Kim Date: Sat, 29 Jun 2024 08:57:23 -0700 Subject: [PATCH 2/2] [feat] address comments for optimize writeBinaryFile --- src-tauri/Cargo.toml | 1 + src-tauri/src/main.rs | 85 +++++++++++++++++++++++++++---------- src/ts/storage/globalApi.ts | 9 +++- src/ts/storage/risuSave.ts | 2 +- 4 files changed, 72 insertions(+), 25 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 91de72f9..6c680735 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -27,6 +27,7 @@ actix-web = "4.0" actix-cors = "0.6" actix-rt = "2.5" url = "2.2" +uuid = { version = "1.9.1", features = [ "v4" ] } [features] # this feature is used for production builds or when `devPath` points to the filesystem diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 8e8dabfe..f31d8502 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -7,11 +7,15 @@ fn greet(name: &str) -> String { format!("Hello, {}! You've been greeted from Rust!", name) } +use actix_web::dev::Server; +use actix_web::http::header; use serde_json::Value; use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use base64::{engine::general_purpose, Engine as _}; use tauri::Manager; +use tauri::State; use std::io::Write; +use std::sync::Mutex; use std::{time::Duration, path::Path}; use serde_json::json; use std::collections::HashMap; @@ -19,6 +23,8 @@ use actix_cors::Cors; use tauri::api::path::app_data_dir; use actix_web::{web, HttpRequest, HttpResponse, HttpServer, Responder, App, post, get}; use std::fs::File; +struct HttpSecret(Mutex); +struct HttpPort(Mutex); #[tauri::command] async fn native_request(url: String, body: String, header: String, method:String) -> String { @@ -379,10 +385,8 @@ fn run_server_local(){ } - #[tauri::command] async fn streamed_fetch(id:String, url:String, headers: String, body: String, handle: tauri::AppHandle) -> String { - //parse headers let headers_json: Value = match serde_json::from_str(&headers) { Ok(h) => h, @@ -452,9 +456,24 @@ async fn streamed_fetch(id:String, url:String, headers: String, body: String, ha } } +#[tauri::command] +fn get_http_secret(secret_state: State) -> String { + secret_state.0.lock().unwrap().clone() +} + +#[tauri::command] +fn get_http_port(port_state: State) -> u16 { + port_state.0.lock().unwrap().clone() +} + #[post("/")] -async fn write_binary_file_to_appdata(req: HttpRequest, body: web::Bytes, app_handle: web::Data) -> impl Responder { +async fn write_binary_file_to_appdata(req: HttpRequest, body: web::Bytes, app_handle: web::Data, secret: web::Data) -> impl Responder { let query = req.query_string(); + let headers = req.headers(); + let req_secret = headers.get("x-tauri-secret").unwrap().to_str().unwrap(); + if req_secret != *secret.as_ref() { + return HttpResponse::Unauthorized().body("Unauthorized"); + } let params: std::collections::HashMap<_, _> = url::form_urlencoded::parse(query.as_bytes()).into_owned().collect(); let app_data_dir = app_data_dir(&app_handle.config()).expect("App dir must be returned by tauri"); if let Some(file_path) = params.get("path") { @@ -479,8 +498,41 @@ async fn write_binary_file_to_appdata(req: HttpRequest, body: web::Bytes, app_ha } } +async fn run_http_server(handle: tauri::AppHandle, secret: String) { + for port in 5354..65535 { + let handle_copy = handle.clone(); + let secret_copy = secret.clone(); + let res = HttpServer::new(move || { + App::new() + .wrap( + Cors::default() + .allow_any_origin() + .allow_any_method() + .allow_any_header() + .max_age(3600) + ) + .app_data(web::PayloadConfig::new(1024 * 1024 * 1024)) // 1 GB + .app_data(web::Data::new(handle_copy.clone())) + .app_data(web::Data::new(secret_copy.clone())) + .service(write_binary_file_to_appdata) + }) + .bind(("127.0.0.1", port)); + match res { + Ok(server) => { + handle.manage(HttpPort(Mutex::new(port))); + server.run().await; + break; + } + Err(e) => { + eprintln!("Failed to bind to port {}: {}", port, e); + } + } + } +} + fn main() { tauri::Builder::default() + .manage(HttpSecret(uuid::Uuid::new_v4().to_string().into())) .invoke_handler(tauri::generate_handler![ greet, native_request, @@ -492,28 +544,17 @@ fn main() { post_py_install, run_py_server, install_py_dependencies, - streamed_fetch + streamed_fetch, + get_http_secret, + get_http_port ]) .setup(|app| { let handle = app.handle().clone(); - tauri::async_runtime::spawn(async move { - HttpServer::new(move || { - App::new() - .wrap( - Cors::default() - .allow_any_origin() - .allow_any_method() - .allow_any_header() - .max_age(3600) - ) - .app_data(web::PayloadConfig::new(1024 * 1024 * 1024)) // 1 GB - .app_data(web::Data::new(handle.clone())) - .service(write_binary_file_to_appdata) - }) - .bind(("127.0.0.1", 5354)) - .expect("Failed to bind to port 5354") - .run() - .await; + let secret_state: State = app.state(); + let secret = secret_state.0.lock().unwrap().clone(); + std::thread::spawn(move || { + let rt = actix_rt::Runtime::new().unwrap(); + rt.block_on(run_http_server(handle.clone(), secret.clone())); }); Ok(()) }) diff --git a/src/ts/storage/globalApi.ts b/src/ts/storage/globalApi.ts index 69d07051..0fed4f7e 100644 --- a/src/ts/storage/globalApi.ts +++ b/src/ts/storage/globalApi.ts @@ -1,4 +1,5 @@ import { writeBinaryFile,BaseDirectory, readBinaryFile, exists, createDir, readDir, removeFile } from "@tauri-apps/api/fs" + import { changeFullscreen, checkNullish, findCharacterbyId, sleep } from "../util" import { convertFileSrc, invoke } from "@tauri-apps/api/tauri" import { v4 as uuidv4, v4 } from 'uuid'; @@ -56,12 +57,16 @@ interface fetchLog{ let fetchLog:fetchLog[] = [] async function writeBinaryFileFast(appPath: string, data: Uint8Array) { - const apiUrl = `http://127.0.0.1:5354/?path=${encodeURIComponent(appPath)}`; + const secret = await invoke('get_http_secret') as string; + const port = await invoke('get_http_port') as number; + + const apiUrl = `http://127.0.0.1:${port}/?path=${encodeURIComponent(appPath)}`; const response = await fetch(apiUrl, { method: 'POST', headers: { - 'Content-Type': 'application/octet-stream' + 'Content-Type': 'application/octet-stream', + 'x-tauri-secret': secret }, body: new Blob([data]) }); diff --git a/src/ts/storage/risuSave.ts b/src/ts/storage/risuSave.ts index 472bedbb..255f0c9d 100644 --- a/src/ts/storage/risuSave.ts +++ b/src/ts/storage/risuSave.ts @@ -16,7 +16,7 @@ const magicCompressedHeader = new Uint8Array([0, 82, 73, 83, 85, 83, 65, 86, 69, export function encodeRisuSave(data:any, compression:'noCompression'|'compression' = 'noCompression'){ let encoded:Uint8Array = packr.encode(data) - if(isTauri || compression === 'compression'){ + if(compression === 'compression'){ encoded = fflate.compressSync(encoded) const result = new Uint8Array(encoded.length + magicCompressedHeader.length); result.set(magicCompressedHeader, 0)