Use a new folder layout for updates and improve a lot of code (read desc)

Switched from a layout like this:

downloads/
  1.4.0.zip (temp)
updates/
  1.4.0/
    lncvrt-games-launcher

to

.version
bin/
  lncvrt-games-launcher
This commit is contained in:
2026-01-07 16:42:16 -07:00
parent 15978c8d6b
commit e46c6e50dd
3 changed files with 100 additions and 126 deletions

View File

@@ -9,11 +9,13 @@
"core:default", "core:default",
"core:window:allow-start-dragging", "core:window:allow-start-dragging",
"fs:default", "fs:default",
"fs:allow-applocaldata-read",
"fs:allow-applocaldata-write",
"fs:allow-exists",
"os:default", "os:default",
"opener:default", "opener:default",
"dialog:default" "dialog:default",
"fs:allow-write-text-file",
{
"identifier": "fs:scope",
"allow": [{ "path": "$APPLOCALDATA/.version" }]
}
] ]
} }

View File

@@ -1,7 +1,7 @@
use sha2::{Digest, Sha512}; use sha2::{Digest, Sha512};
use std::{ use std::{
fs::{File, create_dir_all}, fs::{self, remove_dir_all},
io::{BufReader, copy}, io::Cursor,
path::PathBuf, path::PathBuf,
process::Command, process::Command,
}; };
@@ -9,37 +9,25 @@ use tauri::{AppHandle, Manager};
use tauri_plugin_os::platform; use tauri_plugin_os::platform;
use zip::ZipArchive; use zip::ZipArchive;
#[cfg(target_os = "linux")] fn unzip_to_dir(bytes: &[u8], target: &PathBuf) -> std::io::Result<()> {
use std::{fs, os::unix::fs::PermissionsExt}; let reader = Cursor::new(bytes);
let mut zip = ZipArchive::new(reader).unwrap();
async fn unzip_to_dir(zip_path: PathBuf, out_dir: PathBuf) -> String { for i in 0..zip.len() {
let res = tauri::async_runtime::spawn_blocking(move || { let mut file = zip.by_index(i).unwrap();
let file = File::open(zip_path)?; let outpath = target.join(file.mangled_name());
let mut archive = ZipArchive::new(BufReader::new(file))?;
for i in 0..archive.len() { if file.name().ends_with('/') {
let mut file = archive.by_index(i)?; fs::create_dir_all(&outpath)?;
let outpath = out_dir.join(file.name());
if file.is_dir() {
create_dir_all(&outpath)?;
} else { } else {
if let Some(parent) = outpath.parent() { if let Some(p) = outpath.parent() {
create_dir_all(parent)?; fs::create_dir_all(p)?;
} }
let mut outfile = File::create(&outpath)?; let mut outfile = fs::File::create(&outpath)?;
copy(&mut file, &mut outfile)?; std::io::copy(&mut file, &mut outfile)?;
} }
} }
Ok(())
Ok::<(), zip::result::ZipError>(())
})
.await;
match res {
Ok(Ok(())) => "1".into(),
_ => "-1".into(),
}
} }
fn get_sha512_hash(data: &[u8]) -> String { fn get_sha512_hash(data: &[u8]) -> String {
@@ -50,20 +38,7 @@ fn get_sha512_hash(data: &[u8]) -> String {
} }
#[tauri::command] #[tauri::command]
async fn check_latest_ver(app: AppHandle, version: String) -> String { async fn download(app: AppHandle, url: String, hash: String) -> String {
let updates_path = app.path().app_local_data_dir().unwrap().join("updates");
if updates_path.exists()
&& updates_path.is_dir()
&& updates_path.join(&version).exists()
&& updates_path.join(&version).is_dir()
{
return "1".to_string();
}
return "-1".to_string();
}
#[tauri::command]
async fn download(app: AppHandle, url: String, name: String, hash: String) -> String {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let resp = match client.get(&url).send().await { let resp = match client.get(&url).send().await {
Ok(r) => r, Ok(r) => r,
@@ -79,108 +54,88 @@ async fn download(app: AppHandle, url: String, name: String, hash: String) -> St
return "-2".to_string(); return "-2".to_string();
} }
let downloads_path = app.path().app_local_data_dir().unwrap().join("downloads"); let bin_path = app.path().app_local_data_dir().unwrap().join("bin");
let updates_path = app.path().app_local_data_dir().unwrap().join("updates"); let _ = tokio::fs::create_dir_all(&bin_path).await;
if let Err(_) = unzip_to_dir(&bytes, &bin_path) {
let download_part_path = downloads_path.join(format!("{}.part", name)); return "-3".to_string();
let download_zip_path = downloads_path.join(format!("{}.zip", name));
let _ = tokio::fs::create_dir_all(&downloads_path).await;
if let Ok(true) = tokio::fs::try_exists(&updates_path.join(name.clone())).await {
let _ = tokio::fs::remove_dir_all(&updates_path.join(name.clone())).await;
}
if updates_path.exists() {
if let Ok(mut entries) = tokio::fs::read_dir(&updates_path).await {
while let Ok(Some(entry)) = entries.next_entry().await {
let _ = tokio::fs::remove_dir_all(entry.path()).await;
}
}
let _ = tokio::fs::create_dir_all(updates_path.join(&name)).await;
}
if download_part_path.exists() {
let _ = tokio::fs::remove_file(&download_part_path).await;
} }
if tokio::fs::write(&download_part_path, bytes).await.is_err() { #[cfg(any(target_os = "linux", target_os = "macos"))]
return "-1".to_string();
}
if tokio::fs::rename(&download_part_path, &download_zip_path)
.await
.is_err()
{ {
return "-1".to_string(); use std::{fs, os::unix::fs::PermissionsExt};
}
let unzip_res = unzip_to_dir(download_zip_path.clone(), updates_path.join(&name)).await; let executable_path = if cfg!(target_os = "linux") {
tokio::fs::remove_file(download_zip_path.clone()) bin_path.join("lncvrt-games-launcher")
.await } else {
.unwrap(); bin_path
if unzip_res == "-1" {
return "-1".to_string();
}
#[cfg(target_os = "linux")]
{
let executable_path = updates_path.join(&name).join("lncvrt-games-launcher");
let mut perms = fs::metadata(&executable_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(executable_path, perms).unwrap();
}
#[cfg(target_os = "macos")]
{
use std::fs;
use std::os::unix::fs::PermissionsExt;
let macos_app_path = updates_path
.join(&name)
.join("Lncvrt Games Launcher.app") .join("Lncvrt Games Launcher.app")
.join("Contents") .join("Contents")
.join("MacOS") .join("MacOS")
.join("lncvrt-games-launcher"); .join("lncvrt-games-launcher")
};
let mut perms = fs::metadata(&macos_app_path).unwrap().permissions(); let mut perms = fs::metadata(&executable_path).unwrap().permissions();
perms.set_mode(0o755); perms.set_mode(0o755);
fs::set_permissions(&macos_app_path, perms).unwrap(); fs::set_permissions(&executable_path, perms).unwrap();
} }
return "1".to_string(); return "1".to_string();
} }
#[allow(unused_variables)] #[allow(unused_variables)]
#[tauri::command] #[tauri::command]
fn load(app: AppHandle, name: String) { fn load(app: AppHandle) {
let update_path = app let bin_path = app.path().app_local_data_dir().unwrap().join("bin");
.path() if !bin_path.exists() {
.app_local_data_dir()
.unwrap()
.join("updates")
.join(&name);
if !update_path.exists() {
return; return;
} }
if platform() == "macos" { if platform() == "macos" {
Command::new("open") Command::new("open")
.arg("Lncvrt Games Launcher.app") .arg("Lncvrt Games Launcher.app")
.current_dir(&update_path) .current_dir(&bin_path)
.spawn() .spawn()
.unwrap(); .unwrap();
} else if platform() == "linux" { } else if platform() == "linux" {
Command::new("./lncvrt-games-launcher") Command::new("./lncvrt-games-launcher")
.current_dir(&update_path) .current_dir(&bin_path)
.spawn() .spawn()
.unwrap(); .unwrap();
} else if platform() == "windows" { } else if platform() == "windows" {
Command::new(&update_path.join("lncvrt-games-launcher.exe")) Command::new(&bin_path.join("lncvrt-games-launcher.exe"))
.current_dir(&update_path) .current_dir(&bin_path)
.spawn() .spawn()
.unwrap(); .unwrap();
} }
app.exit(0); app.exit(0);
} }
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.setup(|app| {
let app_local_data_dir = app.path().app_local_data_dir().unwrap();
let downloads_dir = app_local_data_dir.join("downloads");
let updates_dir = app_local_data_dir.join("updates");
let bin_dir = app_local_data_dir.join("bin");
let version_file = app_local_data_dir.join(".version");
if downloads_dir.exists() {
let _ = remove_dir_all(downloads_dir);
}
if updates_dir.exists() {
let _ = remove_dir_all(&updates_dir);
}
if bin_dir.exists() && bin_dir.is_file() {
let _ = remove_dir_all(&bin_dir);
}
if version_file.exists() && !bin_dir.is_file() {
let _ = remove_dir_all(&version_file);
}
Ok(())
})
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
let _ = app let _ = app
.get_webview_window("main") .get_webview_window("main")
@@ -191,7 +146,7 @@ pub fn run() {
.plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.invoke_handler(tauri::generate_handler![check_latest_ver, download, load]) .invoke_handler(tauri::generate_handler![download, load])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@@ -9,6 +9,13 @@ import { invoke } from '@tauri-apps/api/core'
import { LauncherUpdate } from './types/LauncherUpdate' import { LauncherUpdate } from './types/LauncherUpdate'
import { openUrl } from '@tauri-apps/plugin-opener' import { openUrl } from '@tauri-apps/plugin-opener'
import { arch, platform } from '@tauri-apps/plugin-os' import { arch, platform } from '@tauri-apps/plugin-os'
import {
BaseDirectory,
create,
exists,
readTextFile,
writeTextFile
} from '@tauri-apps/plugin-fs'
export default function Home () { export default function Home () {
const [state, setState] = useState<string>('Loading...') const [state, setState] = useState<string>('Loading...')
@@ -47,12 +54,17 @@ export default function Home () {
return return
} }
const isLatest = await invoke('check_latest_ver', { const options = {
version: launcherLatestRequest.data baseDir: BaseDirectory.AppLocalData
}) }
if (isLatest == '1') {
setState('Starting...') let latest = false
} else { if (await exists('.version', options))
latest =
(await readTextFile('.version', options)) ==
launcherLatestRequest.data
if (!latest) {
setState('Downloading new update...') setState('Downloading new update...')
try { try {
const launcherUpdateRequest = await axios.get( const launcherUpdateRequest = await axios.get(
@@ -63,10 +75,12 @@ export default function Home () {
setState('Failed. Check internet connection.') setState('Failed. Check internet connection.')
return return
} }
if (!launcherUpdateData) return if (!launcherUpdateData) {
setState('Failed. Check internet connection.')
return
}
const downloadResult = await invoke('download', { const downloadResult = await invoke('download', {
url: launcherUpdateData.downloadUrl, url: launcherUpdateData.downloadUrl,
name: launcherLatestRequest.data,
hash: launcherUpdateData.sha512sum hash: launcherUpdateData.sha512sum
}) })
if (downloadResult == '-1') { if (downloadResult == '-1') {
@@ -75,13 +89,16 @@ export default function Home () {
} else if (downloadResult == '-2') { } else if (downloadResult == '-2') {
setState('File integrity check failed.') setState('File integrity check failed.')
return return
} else if (downloadResult == '-3') {
setState('Failed to unzip update.')
return
} }
setState('Starting...') if (await exists('.version', options)) await create('.version', options)
await writeTextFile('.version', launcherLatestRequest.data, options)
} }
invoke('load', { setState('Starting...')
name: launcherLatestRequest.data invoke('load')
})
})() })()
}, []) }, [])