refactor: move logic away from tauri commands

This commit is contained in:
2025-02-12 23:10:18 +01:00
parent 047b2e9f4a
commit fdf3679fbe
12 changed files with 181 additions and 156 deletions

1
rust/Cargo.lock generated
View File

@ -4199,6 +4199,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
name = "startliner" name = "startliner"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"async-compression", "async-compression",
"derive_builder", "derive_builder",
"directories", "directories",

View File

@ -33,4 +33,5 @@ log = "0.4.25"
regex = "1.11.1" regex = "1.11.1"
zip = "2.2.2" zip = "2.2.2"
tauri-plugin-dialog = "2" tauri-plugin-dialog = "2"
anyhow = "1.0.95"

View File

@ -1,22 +1,19 @@
use log; use log;
use std::fs::{self, File}; use std::path::PathBuf;
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::pkg_remote; use crate::pkg_remote;
use crate::pkg_local; use crate::pkg_local;
use crate::util; use crate::profile::Profile;
use crate::{types, AppData}; use crate::AppData;
use crate::types::{local, Package}; use crate::model::Package;
use tauri::State; use tauri::State;
#[tauri::command] #[tauri::command]
pub async fn startline( pub async fn startline(
state: State<'_, Mutex<AppData>>, state: State<'_, Mutex<AppData>>,
) -> Result<Option<local::Profile>, ()> { ) -> Result<Option<Profile>, ()> {
log::debug!("invoke: startline"); log::debug!("invoke: startline");
let appd = state.lock().await; let appd = state.lock().await;
@ -25,75 +22,34 @@ pub async fn startline(
} }
#[tauri::command] #[tauri::command]
pub async fn download_package(pkg: Package) { pub async fn download_package(pkg: Package) -> Result<(), String> {
log::debug!("invoke: download_package"); log::debug!("invoke: download_package");
use futures::StreamExt; pkg_remote::download_package(pkg).await.map_err(|e| e.to_string())
let zip_path = util::get_dirs().cache_dir().join(format!(
"{}-{}-{}.zip",
pkg.namespace, pkg.name, pkg.version
));
if !zip_path.exists() {
// let zip_path_part = zip_path.add_extension("part");
let mut zip_path_part = zip_path.clone();
zip_path_part.set_extension("zip.part");
let mut cache_file_w = tokio::fs::File::create(&zip_path_part).await.unwrap();
let mut byte_stream = reqwest::get(&pkg.download_url)
.await
.unwrap()
.bytes_stream();
log::info!("downloading: {}", pkg.download_url);
while let Some(item) = byte_stream.next().await {
let i = item.unwrap();
cache_file_w.write_all(&mut i.as_ref()).await.unwrap();
}
cache_file_w.sync_all().await.unwrap();
tokio::fs::rename(&zip_path_part, &zip_path).await.unwrap();
}
let cache_file_r = File::open(&zip_path).unwrap();
let mut archive = zip::ZipArchive::new(cache_file_r).unwrap();
delete_package(pkg.namespace.clone(), pkg.name.clone())
.await
.unwrap();
let path = util::get_dirs()
.data_dir()
.join("pkg")
.join(format!("{}-{}", pkg.namespace, pkg.name));
fs::create_dir(&path).unwrap();
archive.extract(path).unwrap();
} }
#[tauri::command] #[tauri::command]
pub async fn delete_package(namespace: String, name: String) -> Result<(), String> { pub async fn delete_package(namespace: String, name: String) -> Result<(), String> {
log::debug!("invoke: download_package"); log::debug!("invoke: download_package");
let path = util::get_dirs() pkg_local::delete_package(namespace, name).await.map_err(|e| e.to_string())
.data_dir()
.join("pkg")
.join(format!("{}-{}", namespace, name));
if path.exists() && path.join("manifest.json").exists() {
log::debug!("rm -r'ing {}", path.to_string_lossy());
return tokio::fs::remove_dir_all(&path)
.await
.map_err(|e| e.to_string());
}
Ok(())
} }
#[tauri::command] #[tauri::command]
pub async fn reload_packages(state: State<'_, tokio::sync::Mutex<AppData>>) -> Result<(), ()> { pub async fn reload_packages(state: State<'_, tokio::sync::Mutex<AppData>>) -> Result<(), String> {
log::debug!("invoke: reload_packages"); log::debug!("invoke: reload_packages");
let mut appd = state.lock().await; let mut appd = state.lock().await;
// todo: this should only fetch new things // todo: this should only fetch new things
appd.mods_local = pkg_local::walk_packages(false).await; match pkg_local::walk_packages(false).await {
Ok(m) => {
Ok(()) appd.mods_local = m;
Ok(())
}
Err(e) => {
Err(e.to_string())
}
}
} }
#[tauri::command] #[tauri::command]
@ -101,7 +57,6 @@ pub async fn get_packages(state: State<'_, Mutex<AppData>>) -> Result<Vec<Packag
log::debug!("invoke: get_packages"); log::debug!("invoke: get_packages");
let appd = state.lock().await; let appd = state.lock().await;
log::debug!("Returning {} packages", appd.mods_local.len());
Ok(appd.mods_local.clone()) Ok(appd.mods_local.clone())
} }
@ -110,30 +65,20 @@ pub async fn get_packages(state: State<'_, Mutex<AppData>>) -> Result<Vec<Packag
pub async fn get_listings(state: State<'_, Mutex<AppData>>) -> Result<Vec<Package>, String> { pub async fn get_listings(state: State<'_, Mutex<AppData>>) -> Result<Vec<Package>, String> {
log::debug!("invoke: get_listings"); log::debug!("invoke: get_listings");
let should_fetch; let mut appd = state.lock().await;
{ match pkg_remote::get_listings(&mut appd).await {
let appd = state.lock().await; Ok(l) => Ok(l.clone()),
should_fetch = appd.mods_store.len() == 0; Err(e) => Err(e.to_string())
}
if should_fetch {
let listings = pkg_remote::fetch_listings().await?;
let mut appd = state.lock().await;
appd.mods_store = listings;
Ok(appd.mods_store.clone())
} else {
let appd = state.lock().await;
Ok(appd.mods_store.clone())
} }
} }
#[tauri::command] #[tauri::command]
pub async fn get_current_profile( pub async fn get_current_profile(
state: State<'_, Mutex<AppData>>, state: State<'_, Mutex<AppData>>,
) -> Result<Option<local::Profile>, ()> { ) -> Result<Option<Profile>, ()> {
log::debug!("invoke: get_current_profile"); log::debug!("invoke: get_current_profile");
let appd = state.lock().await; let appd = state.lock().await;
Ok(appd.profile.clone()) Ok(appd.profile.clone())
} }
@ -144,14 +89,10 @@ pub async fn save_profile(
log::debug!("invoke: save_profile"); log::debug!("invoke: save_profile");
let appd = state.lock().await; let appd = state.lock().await;
if let Some(p) = &appd.profile {
if let Some(profile) = &appd.profile { p.save().await;
let path = util::get_dirs().config_dir().join("profile-ongeki-default.json");
let s = serde_json::to_string_pretty(profile).unwrap();
tokio::fs::write(&path, s).await.unwrap();
log::info!("Written to {}", path.to_string_lossy());
} else { } else {
log::error!("No profile to save"); log::warn!("No profile to save");
} }
Ok(()) Ok(())
@ -160,23 +101,15 @@ pub async fn save_profile(
#[tauri::command] #[tauri::command]
pub async fn init_profile( pub async fn init_profile(
state: State<'_, Mutex<AppData>>, state: State<'_, Mutex<AppData>>,
path: String path: PathBuf
) -> Result<local::Profile, ()> { ) -> Result<Profile, String> {
log::debug!("invoke: init_profile"); log::debug!("invoke: init_profile");
let new_profile = local::Profile { let mut appd = state.lock().await;
game: types::misc::Game::Ongeki, let new_profile = Profile::new(path);
path: path,
name: "ongeki-default".to_owned(),
mods: [].to_vec()
};
{ new_profile.save().await;
let mut appd = state.lock().await; appd.profile = Some(new_profile.clone());
appd.profile = Some(new_profile.clone());
}
save_profile(state).await.unwrap();
Ok(new_profile) Ok(new_profile)
} }

View File

@ -1,19 +1,19 @@
mod cfg; mod cfg;
mod types; mod model;
mod cmd; mod cmd;
mod pkg_remote; mod pkg_remote;
mod pkg_local; mod pkg_local;
mod util; mod util;
mod profile;
use tokio::{fs, try_join}; use tokio::{fs, try_join};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use model::Package;
use types::{local, Package};
use tauri::Manager; use tauri::Manager;
use profile::Profile;
struct AppData { struct AppData {
profile: Option<local::Profile>, profile: Option<Profile>,
mods_local: Vec<Package>, mods_local: Vec<Package>,
mods_store: Vec<Package>, mods_store: Vec<Package>,
} }
@ -35,7 +35,7 @@ pub async fn run(args: Vec<String>) {
let app_data = AppData { let app_data = AppData {
profile: pkg_local::load_config(), profile: pkg_local::load_config(),
mods_local: pkg_local::walk_packages(true).await, mods_local: pkg_local::walk_packages(true).await.expect("Unable to scan local packages"),
mods_store: [].to_vec(), mods_store: [].to_vec(),
}; };

11
rust/src/model/local.rs Normal file
View File

@ -0,0 +1,11 @@
use serde::Deserialize;
// manifest.json
#[derive(Deserialize)]
#[allow(dead_code)]
pub struct PackageManifest {
pub name: String,
pub version_number: String,
pub description: String,
}

View File

@ -1,8 +1,9 @@
use anyhow::Result;
use std::fs::{self, File}; use std::fs::{self, File};
use crate::{pkg_remote, types::{self, local, Package}, util}; use crate::{pkg_remote, profile::Profile, model::{self, local, Package}, util};
pub fn load_config() -> Option<local::Profile> { pub fn load_config() -> Option<Profile> {
let path = util::get_dirs().config_dir().join("profile-ongeki-default.json"); let path = util::get_dirs().config_dir().join("profile-ongeki-default.json");
if let Ok(s) = fs::read_to_string(path) { if let Ok(s) = fs::read_to_string(path) {
Some(serde_json::from_str(&s).expect("Invalid profile json")) Some(serde_json::from_str(&s).expect("Invalid profile json"))
@ -11,16 +12,17 @@ pub fn load_config() -> Option<local::Profile> {
} }
} }
pub async fn walk_packages(fetch_remote: bool) -> Vec<Package> { pub async fn walk_packages(fetch_remote: bool) -> Result<Vec<Package>> {
let mut res = [].to_vec(); let mut res = [].to_vec();
let packages = fs::read_dir(util::get_dirs().data_dir().join("pkg")).unwrap(); let packages = fs::read_dir(util::get_dirs().data_dir().join("pkg"))?;
for package in packages { for package in packages {
let dir = package.unwrap().path(); let dir = package.unwrap().path();
let mft: local::PackageManifest = let mft: local::PackageManifest = serde_json::from_reader(
serde_json::from_reader(File::open(dir.join("manifest.json")).unwrap()).unwrap(); File::open(dir.join("manifest.json"))?
let regex = regex::Regex::new(r"([A-Za-z0-9_]+)-([A-Za-z0-9_]+)$").unwrap(); )?;
let regex = regex::Regex::new(r"([A-Za-z0-9_]+)-([A-Za-z0-9_]+)$")?;
let dir_name = dir.file_name().to_owned().unwrap().to_str().unwrap(); let dir_name = dir.file_name().to_owned().unwrap().to_str().unwrap();
let namespace; let namespace;
@ -38,7 +40,7 @@ pub async fn walk_packages(fetch_remote: bool) -> Vec<Package> {
continue; continue;
} }
let mut builder = types::PackageBuilder::default(); let mut builder = model::PackageBuilder::default();
builder builder
.name(mft.name.to_owned()) .name(mft.name.to_owned())
@ -69,7 +71,7 @@ pub async fn walk_packages(fetch_remote: bool) -> Vec<Package> {
.deprecated(false); .deprecated(false);
if fetch_remote == true { if fetch_remote == true {
if let Ok(rem) = pkg_remote::get_remote(namespace, &mft.name).await { if let Ok(rem) = pkg_remote::get_remote_meta(namespace, &mft.name).await {
builder.version_available(rem.latest.version_number); builder.version_available(rem.latest.version_number);
builder.download_url(rem.latest.download_url); builder.download_url(rem.latest.download_url);
builder.deprecated(rem.is_deprecated); builder.deprecated(rem.is_deprecated);
@ -86,5 +88,19 @@ pub async fn walk_packages(fetch_remote: bool) -> Vec<Package> {
} }
} }
res Ok(res)
}
pub async fn delete_package(namespace: String, name: String) -> Result<(), tokio::io::Error> {
let path = util::get_dirs()
.data_dir()
.join("pkg")
.join(format!("{}-{}", namespace, name));
if path.exists() && path.join("manifest.json").exists() {
log::debug!("rm -r'ing {}", path.to_string_lossy());
tokio::fs::remove_dir_all(&path).await
} else {
Ok(())
}
} }

View File

@ -1,45 +1,56 @@
use crate::types::{rainy, Package}; use anyhow::Result;
use tokio::fs::{self, File};
use crate::{pkg_local, util, AppData};
use crate::model::{rainy, Package};
pub async fn fetch_listings() -> Result<Vec<Package>, String> { async fn fetch_listings() -> Result<Vec<Package>> {
use async_compression::futures::bufread::GzipDecoder; use async_compression::futures::bufread::GzipDecoder;
use futures::{ use futures::{
io::{self, BufReader, ErrorKind}, io::{self, BufReader, ErrorKind},
prelude::*, prelude::*,
}; };
let response = reqwest::get("https://rainy.patafour.zip/c/ongeki/api/v1/package/") let response = reqwest::get("https://rainy.patafour.zip/c/ongeki/api/v1/package/")
.await .await?;
.unwrap();
let reader = response let reader = response
.bytes_stream() .bytes_stream()
.map_err(|e| io::Error::new(ErrorKind::Other, e)) .map_err(|e| io::Error::new(ErrorKind::Other, e))
.into_async_read(); .into_async_read();
let mut decoder = GzipDecoder::new(BufReader::new(reader)); let mut decoder = GzipDecoder::new(BufReader::new(reader));
let mut data = String::new(); let mut data = String::new();
decoder.read_to_string(&mut data).await.unwrap(); decoder.read_to_string(&mut data).await?;
let listings: Vec<rainy::V1Package> = serde_json::from_str(&data).expect("Fuck2"); let listings: Vec<rainy::V1Package> = serde_json::from_str(&data).expect("Fuck2");
let mut res: Vec<Package> = [].to_vec(); let mut res: Vec<Package> = [].to_vec();
for l in listings { for l in listings {
let v = l.versions.last().unwrap(); if let Some(v) = l.versions.last() {
let mut p = Package::default(); let mut p = Package::default();
p.name = v.name.clone(); p.name = v.name.clone();
p.namespace = l.owner.clone(); p.namespace = l.owner.clone();
p.description = v.description.clone(); p.description = v.description.clone();
p.version = v.version_number.clone(); p.version = v.version_number.clone();
p.icon = v.icon.clone(); p.icon = v.icon.clone();
p.package_url = l.package_url.clone(); p.package_url = l.package_url.clone();
p.download_url = v.download_url.clone(); p.download_url = v.download_url.clone();
res.push(p); res.push(p);
}
} }
Ok(res) Ok(res)
} }
pub async fn get_remote( pub async fn get_listings(appd: &mut AppData) -> Result<&Vec<Package>> {
if appd.mods_store.len() == 0 {
let listings = fetch_listings().await?;
appd.mods_store = listings;
}
Ok(&appd.mods_store)
}
pub async fn get_remote_meta(
namespace: &str, namespace: &str,
name: &str, name: &str,
) -> Result<rainy::V0Package, Box<dyn std::error::Error>> { ) -> Result<rainy::V0Package> {
let url = format!( let url = format!(
"https://rainy.patafour.zip/api/experimental/package/{}/{}/", "https://rainy.patafour.zip/api/experimental/package/{}/{}/",
namespace, name namespace, name
@ -49,3 +60,46 @@ pub async fn get_remote(
Ok(package) Ok(package)
} }
pub async fn download_package(pkg: Package) -> Result<()> {
use futures::StreamExt;
use tokio::io::AsyncWriteExt;
let zip_path = util::cache_dir().join(format!(
"{}-{}-{}.zip",
pkg.namespace, pkg.name, pkg.version
));
if !zip_path.exists() {
// let zip_path_part = zip_path.add_extension("part");
let mut zip_path_part = zip_path.clone();
zip_path_part.set_extension("zip.part");
let mut cache_file_w = File::create(&zip_path_part).await?;
let mut byte_stream = reqwest::get(&pkg.download_url)
.await?
.bytes_stream();
log::info!("downloading: {}", pkg.download_url);
while let Some(item) = byte_stream.next().await {
let i = item?;
cache_file_w.write_all(&mut i.as_ref()).await?;
}
cache_file_w.sync_all().await?;
tokio::fs::rename(&zip_path_part, &zip_path).await?;
}
let cache_file_r = std::fs::File::open(&zip_path)?;
let mut archive = zip::ZipArchive::new(cache_file_r)?;
pkg_local::delete_package(pkg.namespace.clone(), pkg.name.clone()).await?;
let path = util::get_dirs()
.data_dir()
.join("pkg")
.join(format!("{}-{}", pkg.namespace, pkg.name));
fs::create_dir(&path).await?;
archive.extract(path)?;
Ok(())
}

33
rust/src/profile.rs Normal file
View File

@ -0,0 +1,33 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tokio::fs;
use crate::{model::misc, util};
// {game}-profile-{name}.json
#[derive(Deserialize, Serialize, Clone)]
#[allow(dead_code)]
pub struct Profile {
pub game: misc::Game,
pub path: PathBuf,
pub name: String,
pub mods: Vec<String>,
}
impl Profile {
pub fn new(path: PathBuf) -> Profile {
Profile {
game: misc::Game::Ongeki,
path: path,
name: "ongeki-default".to_owned(),
mods: [].to_vec()
}
}
pub async fn save(&self) {
let path = util::get_dirs().config_dir().join("profile-ongeki-default.json");
let s = serde_json::to_string_pretty(self).unwrap();
fs::write(&path, s).await.unwrap();
log::info!("Written to {}", path.to_string_lossy());
}
}

View File

@ -1,24 +0,0 @@
use serde::{Deserialize, Serialize};
use super::misc;
// manifest.json
#[derive(Deserialize)]
#[allow(dead_code)]
pub struct PackageManifest {
pub name: String,
pub version_number: String,
pub description: String,
}
// {game}-profile-{name}.json
#[derive(Deserialize, Serialize, Clone)]
#[allow(dead_code)]
pub struct Profile {
pub game: misc::Game,
pub path: String,
pub name: String,
pub mods: Vec<String>,
}