From 5d50f446a1a4612c0c931bdbc61f945760392f29 Mon Sep 17 00:00:00 2001 From: fxqnlr Date: Fri, 4 Nov 2022 23:41:21 +0100 Subject: "finished" update, added some tests --- src/apis/modrinth.rs | 23 +++++++ src/commands/modification.rs | 11 +--- src/commands/update.rs | 140 ++++++++++++++++++++++++++++++++++++------- src/config.rs | 1 + src/db.rs | 32 +++++++++- src/input.rs | 10 +++- 6 files changed, 183 insertions(+), 34 deletions(-) (limited to 'src') diff --git a/src/apis/modrinth.rs b/src/apis/modrinth.rs index 0c3eca5..c71b47f 100644 --- a/src/apis/modrinth.rs +++ b/src/apis/modrinth.rs @@ -1,3 +1,5 @@ +use std::io::{Error, ErrorKind}; +use chrono::{DateTime, FixedOffset}; use serde::Deserialize; use crate::{Modloader, List}; @@ -153,3 +155,24 @@ pub async fn versions(api: String, id: String, list: List) -> Vec { serde_json::from_slice(&data.await.unwrap()).unwrap() } + +pub fn extract_current_version(versions: Vec) -> Result> { + match versions.len() { + 0 => Err(Box::new(Error::new(ErrorKind::NotFound, "NO_VERSIONS_AVAILABLE"))), + //TODO compare publish dates + 1.. => { + let mut times: Vec<(String, DateTime)> = vec![]; + for ver in versions { + let stamp = DateTime::parse_from_rfc3339(&ver.date_published)?; + times.push((ver.id, stamp)) + } + dbg!(×); + times.sort_by_key(|t| t.1); + times.reverse(); + dbg!(×); + println!("CW: {}", times[0].0); + Ok(times[0].0.to_string()) + }, + _ => panic!("available_versions should never be negative"), + } +} diff --git a/src/commands/modification.rs b/src/commands/modification.rs index 43e2180..b90c82c 100644 --- a/src/commands/modification.rs +++ b/src/commands/modification.rs @@ -1,6 +1,6 @@ use std::io::{Error, ErrorKind}; -use crate::{modrinth::{project, versions, Version}, config::Cfg, db::{insert_mod, remove_mod_from_list, get_mod_id, insert_mod_in_list, get_mods, get_mods_from_list}, input::Input, get_current_list}; +use crate::{modrinth::{project, versions, extract_current_version}, config::Cfg, db::{insert_mod, remove_mod_from_list, get_mod_id, insert_mod_in_list, get_mods, get_mods_from_list}, input::Input, get_current_list}; pub async fn modification(config: Cfg, args: Option>) -> Result<(), Box> { @@ -77,12 +77,3 @@ fn remove(config: Cfg, args: Vec) -> Result<(), Box Ok(()), } } - -fn extract_current_version(versions: Vec) -> Result> { - match versions.len() { - 0 => Err(Box::new(Error::new(ErrorKind::NotFound, "NO_VERSIONS_AVAILABLE"))), - //TODO compare publish dates - 1.. => Ok(versions[0].id.to_string()), - _ => panic!("available_versions should never be negative"), - } -} diff --git a/src/commands/update.rs b/src/commands/update.rs index 14c37ec..6275bce 100644 --- a/src/commands/update.rs +++ b/src/commands/update.rs @@ -1,40 +1,138 @@ -use std::io::{Error, ErrorKind}; +use std::{io::{Error, ErrorKind, Write}, fs::File}; -use crate::{config::Cfg, modrinth::projects, get_current_list, db::{get_mods_from_list, get_versions}}; +use reqwest::Client; + +use futures_util::StreamExt; + +use crate::{config::Cfg, modrinth::{projects, Project, versions, extract_current_version, Version}, get_current_list, db::{get_mods_from_list, get_versions, get_list_version, change_list_versions}, List}; pub async fn update(config: Cfg) -> Result<(), Box> { let current_list = get_current_list(config.clone())?; - let mods = get_mods_from_list(config.clone(), current_list)?; + let mods = get_mods_from_list(config.clone(), current_list.clone())?; + + let mut versions = get_versions(config.clone(), mods.clone())?; + versions.sort_by_key(|ver| ver.mod_id.clone()); - let mut projects = projects(String::from(&config.apis.modrinth), mods.clone()).await; + let mut projects = projects(String::from(&config.apis.modrinth), mods).await; + projects.sort_by_key(|pro| pro.id.clone()); - let mut versions = get_versions(config, mods)?; + let mut updatestack: Vec = vec![]; + for (index, project) in projects.into_iter().enumerate() { + let current_version = &versions[index]; + + let p_id = String::from(&project.id); + let v_id = ¤t_version.mod_id; + + if &p_id != v_id { return Err(Box::new(Error::new(ErrorKind::Other, "SORTING_ERROR"))) }; + + if project.versions.join("|") != current_version.versions { + updatestack.push(match specific_update(config.clone(), current_list.clone(), project).await { + Ok(ver) => ver, + //TODO handle errors (only continue on "NO_UPDATE_AVAILABLE") + Err(_) => { continue; }, + }); + }; + }; + //println!("{:?}", updatestack); + + //download_updates(config, updatestack).await?; + + Ok(()) +} + +async fn specific_update(config: Cfg, list: List, project: Project) -> Result> { + print!("Checking update for '{}' in {}", project.title, list.id); - projects.sort_by_key(|p| p.id.clone()); + let applicable_versions = versions(String::from(&config.apis.modrinth), String::from(&project.id), list.clone()).await; + + let mut versions: Vec = vec![]; - versions.sort_by_key(|v| v.mod_id.clone()); + for ver in &applicable_versions { + versions.push(String::from(&ver.id)); + } - let mut update_stack: Vec = vec![]; + let mut current: Vec = vec![]; + if versions.join("|") != get_list_version(config.clone(), list.clone(), String::from(&project.id))? { + //get new versions + print!(" | getting new version"); + let current_str = extract_current_version(applicable_versions.clone())?; + current.push(applicable_versions.into_iter().find(|ver| ver.id == current_str).unwrap()); + change_list_versions(config, list, current_str, versions, project.id)?; + } - for (index, project) in projects.iter().enumerate() { + if current.is_empty() { return Err(Box::new(Error::new(ErrorKind::NotFound, "NO_UPDATE_AVAILABLE"))) }; + + println!(" | ✔️"); + Ok(current[0].clone()) +} - let cmp_version = &versions[index]; +async fn download_updates(config: Cfg, versions: Vec) -> Result> { - let p_id = &project.id; - let v_id = &cmp_version.mod_id; + let dl_path = String::from(&config.downloads); - if p_id != v_id { return Err(Box::new(Error::new(ErrorKind::Other, "COMPARE_SORTING_ERR"))); }; - println!("{}:{}", p_id, v_id); + for ver in versions { + let primary_file = ver.files.into_iter().find(|file| file.primary).unwrap(); + let dl_path_file = format!("{}/{}", config.downloads, primary_file.filename); + println!("Downloading {}", primary_file.url); - if project.versions.join("|") != cmp_version.versions { - update_stack.push(String::from(&project.id)); - }; - }; + let res = Client::new() + .get(String::from(&primary_file.url)) + .send() + .await + .or(Err(format!("Failed to GET from '{}'", &primary_file.url)))?; + + // download chunks + let mut file = File::create(String::from(&dl_path_file)).or(Err(format!("Failed to create file '{}'", dl_path_file)))?; + let mut stream = res.bytes_stream(); - //TODO UPDATE - dbg!(update_stack); + while let Some(item) = stream.next().await { + let chunk = item.or(Err("Error while downloading file"))?; + file.write_all(&chunk) + .or(Err("Error while writing to file"))?; + } + } - Ok(()) + Ok(dl_path) +} + +#[tokio::test] +async fn download_updates_test() { + + use crate::{modrinth::{Version, VersionFile, Hash, VersionType}, config::{Cfg, Apis}}; + + let config = Cfg { data: "...".to_string(), clean_remove: false, downloads: "./dl".to_string(), apis: Apis { modrinth: "...".to_string() } }; + + let versions = vec![Version { + id: "dEqtGnT9".to_string(), + project_id: "kYuIpRLv".to_string(), + author_id: "Qnt13hO8".to_string(), + featured: true, + name: "1.2.2-1.19 - Fabric".to_string(), + version_number: "1.2.2-1.19".to_string(), + changelog: None, + date_published: "2022-11-02T17:41:43.072267Z".to_string(), + downloads: 58, + version_type: VersionType::release, + files: vec![VersionFile { + hashes: Hash { + sha1: "fdc6dc39427fc92cc1d7ad8b275b5b83325e712b".to_string(), + sha512: "5b372f00d6e5d6a5ef225c3897826b9f6a2be5506905f7f71b9e939779765b41be6f2a9b029cfc752ad0751d0d2d5f8bb4544408df1363eebdde15641e99a849".to_string() + }, + url: "https://cdn.modrinth.com/data/kYuIpRLv/versions/dEqtGnT9/waveycapes-fabric-1.2.2-mc1.19.2.jar".to_string(), + filename: "waveycapes-fabric-1.2.2-mc1.19.2.jar".to_string(), + primary: true, + size: 323176 + }], + game_versions: vec![ + "1.19".to_string(), + "1.19.1".to_string(), + "1.19.2".to_string() + ], + loaders: vec![ + "fabric".to_string() + ] + }]; + assert_eq!(download_updates(config, versions).await.unwrap(), "./dl") } diff --git a/src/config.rs b/src/config.rs index 58d399a..ad59963 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,7 @@ use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] pub struct Cfg { pub data: String, + pub downloads: String, pub clean_remove: bool, pub apis: Apis, } diff --git a/src/db.rs b/src/db.rs index 33d8344..497cc15 100644 --- a/src/db.rs +++ b/src/db.rs @@ -2,7 +2,7 @@ use std::io::ErrorKind; use crate::{Modloader, config::Cfg, List, modrinth::Version, get_modloader}; -//TODO use prepared statements +//TODO use prepared statements / change to rusqlite //MODS pub fn insert_mod(config: Cfg, id: String, name: String, versions: Vec) -> Result<(), sqlite::Error> { @@ -151,6 +151,27 @@ pub fn get_versions(config: Cfg, mods: Vec) -> Result Result> { + let data = format!("{}/data.db", config.data); + let connection = sqlite::open(data).unwrap(); + + let sql = format!("SELECT applicable_versions FROM {} WHERE mod_id = '{}'", list.id, mod_id); + + //TODO catch sql errors better + let mut version: String = String::new(); + connection.iterate(sql, |ver| { + if ver.is_empty() { return false; }; + for &(_column, value) in ver.iter() { + version = String::from(value.unwrap()); + } + true + }).unwrap(); + + if version.is_empty() { return Err(Box::new(std::io::Error::new(ErrorKind::Other, "NO_MODS_ON_LIST"))); }; + + Ok(version) +} + //LIST pub fn insert_list(config: Cfg, id: String, mc_version: String, mod_loader: Modloader) -> Result<(), sqlite::Error> { @@ -217,6 +238,15 @@ pub fn get_list(config: Cfg, id: String) -> Result, mod_id: String) -> Result<(), sqlite::Error> { + let data = format!("{}/data.db", config.data); + let connection = sqlite::open(data).unwrap(); + + let sql = format!("UPDATE {} SET current_version = '{}', applicable_versions = '{}' WHERE mod_id = '{}'", list.id, current_version, versions.join("|"), mod_id); + + connection.execute(sql) +} + //config pub fn change_list(config: Cfg, id: String) -> Result<(), sqlite::Error> { let data = format!("{}/data.db", config.data); diff --git a/src/input.rs b/src/input.rs index 0c13e67..e0c9ae9 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,6 +1,7 @@ use std::io::{stdin, Error, ErrorKind}; use crate::{config::Cfg, list, modification, update}; +#[derive(Debug, PartialEq, Eq)] pub struct Input { pub command: String, pub args: Option>, @@ -27,8 +28,6 @@ impl Input { }, _ => { panic!("This should never happen") } } - - } } @@ -53,3 +52,10 @@ pub async fn get_input(config: Cfg) -> Result<(), Box> { _ => Err(Box::new(Error::new(ErrorKind::InvalidInput, "UNKNOWN_COMMAND"))), } } + +#[test] +fn input_from() { + let string = String::from("list add test 1.19.2 fabric"); + let input = Input { command: String::from("list"), args: Some(vec![String::from("add"), String::from("test"), String::from("1.19.2"), String::from("fabric")]) }; + assert_eq!(Input::from(string).unwrap(), input); +} -- cgit v1.2.3