diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 0287e31..91ecb0d 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -4199,6 +4199,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" name = "startliner" version = "0.1.0" dependencies = [ + "anyhow", "async-compression", "derive_builder", "directories", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 301be8e..40e4b38 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -33,4 +33,5 @@ log = "0.4.25" regex = "1.11.1" zip = "2.2.2" tauri-plugin-dialog = "2" +anyhow = "1.0.95" diff --git a/rust/src/cmd.rs b/rust/src/cmd.rs index 428d025..37bed1d 100644 --- a/rust/src/cmd.rs +++ b/rust/src/cmd.rs @@ -1,22 +1,19 @@ - - use log; -use std::fs::{self, File}; -use tokio::io::AsyncWriteExt; +use std::path::PathBuf; use tokio::sync::Mutex; use crate::pkg_remote; use crate::pkg_local; -use crate::util; -use crate::{types, AppData}; -use crate::types::{local, Package}; +use crate::profile::Profile; +use crate::AppData; +use crate::model::Package; use tauri::State; #[tauri::command] pub async fn startline( state: State<'_, Mutex>, -) -> Result, ()> { +) -> Result, ()> { log::debug!("invoke: startline"); let appd = state.lock().await; @@ -25,75 +22,34 @@ pub async fn startline( } #[tauri::command] -pub async fn download_package(pkg: Package) { +pub async fn download_package(pkg: Package) -> Result<(), String> { log::debug!("invoke: download_package"); - use futures::StreamExt; - - 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(); + pkg_remote::download_package(pkg).await.map_err(|e| e.to_string()) } #[tauri::command] pub async fn delete_package(namespace: String, name: String) -> Result<(), String> { log::debug!("invoke: download_package"); - 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()); - return tokio::fs::remove_dir_all(&path) - .await - .map_err(|e| e.to_string()); - } - - Ok(()) + pkg_local::delete_package(namespace, name).await.map_err(|e| e.to_string()) } #[tauri::command] -pub async fn reload_packages(state: State<'_, tokio::sync::Mutex>) -> Result<(), ()> { +pub async fn reload_packages(state: State<'_, tokio::sync::Mutex>) -> Result<(), String> { log::debug!("invoke: reload_packages"); let mut appd = state.lock().await; // todo: this should only fetch new things - appd.mods_local = pkg_local::walk_packages(false).await; - - Ok(()) + match pkg_local::walk_packages(false).await { + Ok(m) => { + appd.mods_local = m; + Ok(()) + } + Err(e) => { + Err(e.to_string()) + } + } } #[tauri::command] @@ -101,7 +57,6 @@ pub async fn get_packages(state: State<'_, Mutex>) -> Result>) -> Result>) -> Result, String> { log::debug!("invoke: get_listings"); - let should_fetch; - { - let appd = state.lock().await; - should_fetch = appd.mods_store.len() == 0; - } - 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()) + let mut appd = state.lock().await; + match pkg_remote::get_listings(&mut appd).await { + Ok(l) => Ok(l.clone()), + Err(e) => Err(e.to_string()) } } #[tauri::command] pub async fn get_current_profile( state: State<'_, Mutex>, -) -> Result, ()> { +) -> Result, ()> { log::debug!("invoke: get_current_profile"); let appd = state.lock().await; - Ok(appd.profile.clone()) } @@ -144,14 +89,10 @@ pub async fn save_profile( log::debug!("invoke: save_profile"); let appd = state.lock().await; - - if let Some(profile) = &appd.profile { - 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()); + if let Some(p) = &appd.profile { + p.save().await; } else { - log::error!("No profile to save"); + log::warn!("No profile to save"); } Ok(()) @@ -160,23 +101,15 @@ pub async fn save_profile( #[tauri::command] pub async fn init_profile( state: State<'_, Mutex>, - path: String -) -> Result { + path: PathBuf +) -> Result { log::debug!("invoke: init_profile"); - let new_profile = local::Profile { - game: types::misc::Game::Ongeki, - path: path, - name: "ongeki-default".to_owned(), - mods: [].to_vec() - }; + let mut appd = state.lock().await; + let new_profile = Profile::new(path); - { - let mut appd = state.lock().await; - appd.profile = Some(new_profile.clone()); - } - - save_profile(state).await.unwrap(); + new_profile.save().await; + appd.profile = Some(new_profile.clone()); Ok(new_profile) } \ No newline at end of file diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 7b71fba..cfffe4c 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,19 +1,19 @@ mod cfg; -mod types; +mod model; mod cmd; mod pkg_remote; mod pkg_local; mod util; +mod profile; use tokio::{fs, try_join}; use tokio::sync::Mutex; - -use types::{local, Package}; - +use model::Package; use tauri::Manager; +use profile::Profile; struct AppData { - profile: Option, + profile: Option, mods_local: Vec, mods_store: Vec, } @@ -35,7 +35,7 @@ pub async fn run(args: Vec) { let app_data = AppData { 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(), }; diff --git a/rust/src/model/local.rs b/rust/src/model/local.rs new file mode 100644 index 0000000..93a3362 --- /dev/null +++ b/rust/src/model/local.rs @@ -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, +} \ No newline at end of file diff --git a/rust/src/types/misc.rs b/rust/src/model/misc.rs similarity index 100% rename from rust/src/types/misc.rs rename to rust/src/model/misc.rs diff --git a/rust/src/types/mod.rs b/rust/src/model/mod.rs similarity index 100% rename from rust/src/types/mod.rs rename to rust/src/model/mod.rs diff --git a/rust/src/types/rainy.rs b/rust/src/model/rainy.rs similarity index 100% rename from rust/src/types/rainy.rs rename to rust/src/model/rainy.rs diff --git a/rust/src/pkg_local.rs b/rust/src/pkg_local.rs index 58e2b2c..9955b5d 100644 --- a/rust/src/pkg_local.rs +++ b/rust/src/pkg_local.rs @@ -1,8 +1,9 @@ +use anyhow::Result; 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 { +pub fn load_config() -> Option { let path = util::get_dirs().config_dir().join("profile-ongeki-default.json"); if let Ok(s) = fs::read_to_string(path) { Some(serde_json::from_str(&s).expect("Invalid profile json")) @@ -11,16 +12,17 @@ pub fn load_config() -> Option { } } -pub async fn walk_packages(fetch_remote: bool) -> Vec { +pub async fn walk_packages(fetch_remote: bool) -> Result> { 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 { let dir = package.unwrap().path(); - let mft: local::PackageManifest = - serde_json::from_reader(File::open(dir.join("manifest.json")).unwrap()).unwrap(); - let regex = regex::Regex::new(r"([A-Za-z0-9_]+)-([A-Za-z0-9_]+)$").unwrap(); + let mft: local::PackageManifest = serde_json::from_reader( + File::open(dir.join("manifest.json"))? + )?; + 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 namespace; @@ -38,7 +40,7 @@ pub async fn walk_packages(fetch_remote: bool) -> Vec { continue; } - let mut builder = types::PackageBuilder::default(); + let mut builder = model::PackageBuilder::default(); builder .name(mft.name.to_owned()) @@ -69,7 +71,7 @@ pub async fn walk_packages(fetch_remote: bool) -> Vec { .deprecated(false); 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.download_url(rem.latest.download_url); builder.deprecated(rem.is_deprecated); @@ -86,5 +88,19 @@ pub async fn walk_packages(fetch_remote: bool) -> Vec { } } - 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(()) + } } \ No newline at end of file diff --git a/rust/src/pkg_remote.rs b/rust/src/pkg_remote.rs index 311cfc6..a51bcda 100644 --- a/rust/src/pkg_remote.rs +++ b/rust/src/pkg_remote.rs @@ -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, String> { +async fn fetch_listings() -> Result> { use async_compression::futures::bufread::GzipDecoder; use futures::{ io::{self, BufReader, ErrorKind}, prelude::*, }; let response = reqwest::get("https://rainy.patafour.zip/c/ongeki/api/v1/package/") - .await - .unwrap(); + .await?; let reader = response .bytes_stream() .map_err(|e| io::Error::new(ErrorKind::Other, e)) .into_async_read(); let mut decoder = GzipDecoder::new(BufReader::new(reader)); let mut data = String::new(); - decoder.read_to_string(&mut data).await.unwrap(); + decoder.read_to_string(&mut data).await?; let listings: Vec = serde_json::from_str(&data).expect("Fuck2"); let mut res: Vec = [].to_vec(); for l in listings { - let v = l.versions.last().unwrap(); - let mut p = Package::default(); - p.name = v.name.clone(); - p.namespace = l.owner.clone(); - p.description = v.description.clone(); - p.version = v.version_number.clone(); - p.icon = v.icon.clone(); - p.package_url = l.package_url.clone(); - p.download_url = v.download_url.clone(); - res.push(p); + if let Some(v) = l.versions.last() { + let mut p = Package::default(); + p.name = v.name.clone(); + p.namespace = l.owner.clone(); + p.description = v.description.clone(); + p.version = v.version_number.clone(); + p.icon = v.icon.clone(); + p.package_url = l.package_url.clone(); + p.download_url = v.download_url.clone(); + res.push(p); + } } Ok(res) } -pub async fn get_remote( +pub async fn get_listings(appd: &mut AppData) -> Result<&Vec> { + 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, name: &str, -) -> Result> { +) -> Result { let url = format!( "https://rainy.patafour.zip/api/experimental/package/{}/{}/", namespace, name @@ -48,4 +59,47 @@ pub async fn get_remote( let package: rainy::V0Package = serde_json::from_str(&res)?; 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(()) } \ No newline at end of file diff --git a/rust/src/profile.rs b/rust/src/profile.rs new file mode 100644 index 0000000..7727ec5 --- /dev/null +++ b/rust/src/profile.rs @@ -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, +} + +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()); + } +} \ No newline at end of file diff --git a/rust/src/types/local.rs b/rust/src/types/local.rs deleted file mode 100644 index 4913e32..0000000 --- a/rust/src/types/local.rs +++ /dev/null @@ -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, -} \ No newline at end of file