From ddde9204c72dd867f920f07f6483be03dda7cf68 Mon Sep 17 00:00:00 2001 From: fxqnlr Date: Mon, 28 Nov 2022 22:55:14 +0100 Subject: basically update impl; added "good" download; auto dl on new mod; db to 0.4; etc --- src/apis/modrinth.rs | 24 ++++++--- src/commands/download.rs | 17 +++++++ src/commands/modification.rs | 25 +++++----- src/commands/setup.rs | 12 ++++- src/commands/update.rs | 67 +++++++++++++++++-------- src/db.rs | 114 ++++++++++++++++++++++++++++++++++++++----- src/files.rs | 44 +++++++++++++++++ src/input.rs | 21 ++++++-- src/lib.rs | 25 +--------- 9 files changed, 271 insertions(+), 78 deletions(-) create mode 100644 src/files.rs (limited to 'src') diff --git a/src/apis/modrinth.rs b/src/apis/modrinth.rs index c99cfbf..7b322cb 100644 --- a/src/apis/modrinth.rs +++ b/src/apis/modrinth.rs @@ -1,5 +1,6 @@ use std::io::{Error, ErrorKind}; use chrono::{DateTime, FixedOffset}; +use reqwest::Client; use serde::Deserialize; use crate::{Modloader, List}; @@ -113,18 +114,21 @@ pub struct Hash { async fn get(api: String, path: String) -> Result, Box> { let url = format!(r#"{}{}"#, api, path); - dbg!(&url); - - let data = reqwest::get(url) - .await? - .bytes() - .await? - .to_vec(); + let client = Client::builder() + .user_agent(format!("fxqnlr/modlistcli/{} (fxqnlr@gmail.com)", env!("CARGO_PKG_VERSION"))) + .build()?; + let data = client.get(url) + .send() + .await? + .bytes() + .await? + .to_vec(); Ok(data) } pub async fn project(api: String, name: &str) -> Project { + println!("!!!PROJECT"); let url = format!("project/{}", name); let data = get(api, url); @@ -132,9 +136,11 @@ pub async fn project(api: String, name: &str) -> Project { } pub async fn projects(api: String, ids: Vec) -> Vec { + //println!("!!!PROJECTS"); + println!("Getting versions for all mods from modrinth"); let all = ids.join(r#"",""#); let url = format!(r#"projects?ids=["{}"]"#, all); - println!("{}", url); + //println!("{}", url); let data = get(api, url); @@ -142,6 +148,7 @@ pub async fn projects(api: String, ids: Vec) -> Vec { } pub async fn versions(api: String, id: String, list: List) -> Vec { + println!("!!!VERSIONS"); let loaderstr = match list.modloader { Modloader::Forge => String::from("forge"), Modloader::Fabric => String::from("fabric"), @@ -155,6 +162,7 @@ pub async fn versions(api: String, id: String, list: List) -> Vec { } pub async fn get_raw_versions(api: String, versions: Vec) -> Vec { + println!("!!!RAWVERSIONS"); println!("Getting versions {}", &versions.join(", ")); let url = format!(r#"versions?ids=["{}"]"#, versions.join(r#"",""#)); diff --git a/src/commands/download.rs b/src/commands/download.rs index 13ba0e1..b0efdc2 100644 --- a/src/commands/download.rs +++ b/src/commands/download.rs @@ -1,3 +1,4 @@ +use crate::{modrinth::Version, files::download_file}; #[allow(unused_imports)] use crate::{List, get_current_list, config::Cfg, db::userlist_get_all_downloads, input::Input}; @@ -32,3 +33,19 @@ async fn download_links(_config: Cfg, _input: Input, _current_list: List, _links Ok(String::new()) } + +pub async fn download_versions(current_list: List, versions: Vec) -> Result> { + + let dl_path = String::from(¤t_list.download_folder); + + for ver in versions { + let primary_file = ver.files.into_iter().find(|file| file.primary).unwrap(); + let mut splitname: Vec<&str> = primary_file.filename.split('.').collect(); + let extension = splitname.pop().ok_or("NO_FILE_EXTENSION")?; + let filename = format!("{}.mr{}.{}", splitname.join("."), ver.id, extension); + download_file(primary_file.url, current_list.clone().download_folder, filename).await?; + } + + Ok(dl_path) +} + diff --git a/src/commands/modification.rs b/src/commands/modification.rs index 519a0cb..8e39d11 100644 --- a/src/commands/modification.rs +++ b/src/commands/modification.rs @@ -1,12 +1,12 @@ use std::io::{Error, ErrorKind}; -use crate::{modrinth::{project, versions, extract_current_version}, config::Cfg, db::{mods_insert, userlist_remove, mods_get_id, userlist_insert, mods_get_all_ids, userlist_get_all_ids}, input::{Input, Subcmd}, get_current_list}; +use crate::{modrinth::{project, versions, extract_current_version}, config::Cfg, db::{mods_insert, userlist_remove, mods_get_id, userlist_insert, mods_get_all_ids, userlist_get_all_ids}, input::{Input, Subcmd}, get_current_list, download_versions}; pub async fn modification(config: Cfg, input: Input) -> Result<(), Box> { - match input.subcommand.ok_or("")? { + match input.subcommand.as_ref().ok_or("")? { Subcmd::Add => { - add(config, input.args.ok_or("")?).await + add(config, input).await }, Subcmd::Remove => { remove(config, input.args.ok_or("")?) @@ -15,23 +15,23 @@ pub async fn modification(config: Cfg, input: Input) -> Result<(), Box) -> Result<(), Box> { - //TODO! DO NOT PANIC IF MOD IS ALREADY IN MODS DB +async fn add(config: Cfg, input: Input) -> Result<(), Box> { + + let args = input.args.ok_or("")?; + if args.is_empty() { return Err(Box::new(Error::new(ErrorKind::InvalidInput, "TOO_FEW_ARGUMENTS"))); }; let current_list = get_current_list(config.clone())?; let project = project(String::from(&config.apis.modrinth), &args[0]).await; - if project.versions.is_empty() { panic!("This should never happen"); }; - let available_versions = versions(String::from(&config.apis.modrinth), String::from(&project.id), current_list.clone()).await; let current_id = extract_current_version(available_versions.clone())?; let current_version = available_versions.clone().into_iter().find(|v| v.id == current_id).unwrap(); - let file = current_version.files.into_iter().find(|f| f.primary).unwrap().url; + let file = current_version.clone().files.into_iter().find(|f| f.primary).unwrap().url; let mut available_versions_vec: Vec = Vec::new(); for ver in available_versions { @@ -43,16 +43,16 @@ async fn add(config: Cfg, args: Vec) -> Result<(), Box userlist_insert(config.clone(), current_list.id, String::from(&project.id), current_version.id, available_versions_vec, file)?, + Err(..) => userlist_insert(config.clone(), String::from(¤t_list.id), String::from(&project.id), String::from(¤t_version.id), available_versions_vec, file)?, }; match mods_get_all_ids(config.clone()) { Ok(mods) => { if mods.contains(&project.id) { - return Err(Box::new(Error::new(ErrorKind::Other, "MOD_ALREADY_IN_DATABASE"))) + //return Err(Box::new(Error::new(ErrorKind::Other, "MOD_ALREADY_IN_DATABASE"))) } else { mods_insert(config.clone(), String::from(&project.id), String::from(&project.title), project.versions)?; } @@ -61,6 +61,9 @@ async fn add(config: Cfg, args: Vec) -> Result<(), Box Result<(), Box> { Ok(ver) => { match ver.as_str() { "0.2" => to_03(config)?, + "0.3" => to_04(config)?, _ => return Err(Box::new(Error::new(ErrorKind::Other, "UNKNOWN_VERSION"))) } }, @@ -33,7 +34,7 @@ async fn to_02(config: Cfg) -> Result<(), Box> { for list in lists { println!("Updating {}", list); - s_insert_column(config.clone(), String::from(&list), String::from("current_download"), String::from("TEXT"))?; + s_insert_column(config.clone(), String::from(&list), String::from("current_download"), String::from("TEXT"), None)?; let full_list = lists_get(config.clone(), String::from(&list))?; @@ -53,6 +54,13 @@ async fn to_02(config: Cfg) -> Result<(), Box> { } fn to_03(config: Cfg) -> Result<(), Box> { - s_insert_column(config.clone(), String::from("lists"), String::from("download_folder"), String::from("TEXT"))?; + s_insert_column(config.clone(), String::from("lists"), String::from("download_folder"), String::from("TEXT"), None)?; s_config_update_version(config, String::from("0.3")) } + +fn to_04(config: Cfg) -> Result<(), Box> { + for list_id in lists_get_all_ids(config.clone())? { + s_insert_column(config.clone(), list_id, String::from("disabled_versions"), String::from("TEXT"), Some(String::from("NONE")))?; + } + s_config_update_version(config, String::from("0.4")) +} diff --git a/src/commands/update.rs b/src/commands/update.rs index 42d19aa..c8f0880 100644 --- a/src/commands/update.rs +++ b/src/commands/update.rs @@ -1,9 +1,9 @@ -use std::io::{Error, ErrorKind}; +use std::{io::{Error, ErrorKind}, fs::{rename, remove_file}}; -use crate::{config::Cfg, modrinth::{projects, Project, versions, extract_current_version, Version}, get_current_list, db::{userlist_get_all_ids, mods_get_versions, userlist_get_applicable_versions, userlist_change_versions, lists_get_all_ids, lists_get}, List, input::Input, download_file}; +use crate::{config::Cfg, modrinth::{projects, Project, versions, extract_current_version, Version}, get_current_list, db::{userlist_get_all_ids, mods_get_versions, userlist_get_applicable_versions, userlist_change_versions, lists_get_all_ids, lists_get, userlist_get_current_version, userlist_add_disabled_versions, mods_change_versions}, List, input::Input, files::get_file_path, download_versions}; pub async fn update(config: Cfg, input: Input) -> Result<(), Box> { - + let mut liststack: Vec = vec![]; if input.all_lists { let list_ids = lists_get_all_ids(config.clone())?; @@ -11,18 +11,22 @@ pub async fn update(config: Cfg, input: Input) -> Result<(), Box = vec![]; let mut versions = mods_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).await; projects.sort_by_key(|pro| pro.id.clone()); - + let mut updatestack: Vec = vec![]; for (index, project) in projects.into_iter().enumerate() { //Get versions for project and check if they match up @@ -30,14 +34,26 @@ pub async fn update(config: Cfg, input: Input) -> Result<(), Box ver, + Ok(ver) => { + current_versions.push((disable_version, p_id)); + ver + }, //TODO handle errors (only continue on "NO_UPDATE_AVAILABLE") - Err(_) => { println!("({}) No new version found for the specified minecraft version", project.title); continue; }, + Err(e) => { + //Updating versions in modlist for no repeating version calls + mods_change_versions(config.clone(), version_db_string, project.id)?; + println!("({}) No new version found for the specified minecraft version({})", project.title, e); + continue; + }, }); } else { println!("({}) No new version found", project.title); @@ -53,14 +69,19 @@ pub async fn update(config: Cfg, input: Input) -> Result<(), Box Result> { - print!("Checking update for '{}' in {}", project.title, list.id); + println!("Checking update for '{}' in {}", project.title, list.id); let applicable_versions = versions(String::from(&config.apis.modrinth), String::from(&project.id), list.clone()).await; @@ -88,17 +109,25 @@ async fn specific_update(config: Cfg, input: Input, list: List, project: Project Ok(current[0].clone()) } -async fn download_updates(current_list: List, versions: Vec) -> Result> { +fn disable_old(config: Cfg, current_list: List, versionid: String, mod_id: String) -> Result<(), Box> { + println!("Disabling version {} for mod {}", versionid, mod_id); + let file = get_file_path(current_list.clone(), String::from(&versionid))?; + let disabled = format!("{}.disabled", file); - let dl_path = String::from(¤t_list.download_folder); + rename(file, disabled)?; - for ver in versions { - let primary_file = ver.files.into_iter().find(|file| file.primary).unwrap(); - download_file(primary_file.url, current_list.clone().download_folder, primary_file.filename).await?; - } + userlist_add_disabled_versions(config, current_list.id, versionid, mod_id)?; + + Ok(()) +} +fn delete_old(current_list: List, versionid: String, mod_id: String) -> Result<(), Box> { + println!("Deleting version {} for mod {}", versionid, mod_id); + let file = get_file_path(current_list, String::from(&versionid))?; + + remove_file(file)?; - Ok(dl_path) + Ok(()) } #[tokio::test] @@ -138,5 +167,5 @@ async fn download_updates_test() { "fabric".to_string() ] }]; - assert!(download_updates(current_list, versions).await.is_ok()) + assert!(download_versions(current_list, versions).await.is_ok()) } diff --git a/src/db.rs b/src/db.rs index 6b1e3ab..542c162 100644 --- a/src/db.rs +++ b/src/db.rs @@ -32,7 +32,7 @@ pub fn mods_get_all_ids(config: Cfg) -> Result, Box Result Result Result> { + let data = format!("{}/data.db", config.data); + let connection = Connection::open(data)?; + + let mut mod_name = String::new(); + let mut stmt = connection.prepare("SELECT name FROM mods WHERE id = ?")?; + let name_iter = stmt.query_map([id], |row| { + row.get::(0) + })?; + + for name in name_iter { + //println!("Found id {:?}", id.as_ref().unwrap()); + mod_name = name?; + }; + + match mod_name.is_empty() { + true => Err(Box::new(Error::new(ErrorKind::NotFound, "MOD_NOT_FOUND"))), + false => Ok(mod_name), + } +} + +pub fn mods_change_versions(config: Cfg, versions: String, mod_id: String) -> Result<(), Box> { + + println!("Updating versions for {} with \n {}", mod_id, versions); + + let data = format!("{}/data.db", config.data); + let connection = Connection::open(data)?; + + connection.execute("UPDATE mods SET versions = ?1 WHERE id = ?2", [versions, mod_id])?; + Ok(()) +} + pub fn mods_remove(config: Cfg, id: String) -> Result<(), Box> { println!("Removing mod {} from database", id); @@ -95,14 +127,15 @@ pub fn mods_get_versions(config: Cfg, mods: Vec) -> Result = Vec::new(); - let mut stmt = connection.prepare(dbg!(format!("SELECT id, versions FROM mods {}", wherestr).as_str()))?; + let mut stmt = connection.prepare(format!("SELECT id, versions, name FROM mods {}", wherestr).as_str())?; let id_iter = stmt.query_map([], |row| { - Ok(vec![row.get::(0)?, row.get::(1)?]) + Ok(vec![row.get::(0)?, row.get::(1)?, row.get::(2)?]) })?; for ver in id_iter { let version = ver?; - println!("Found versions {} for mod {}", version[1], version[0]); + println!("Getting versions for {} from the database", String::from(&version[2])); + //println!("Found versions {} for mod {}", version[1], version[0]); versionmaps.push(DBModlistVersions { mod_id: String::from(&version[0]), versions: String::from(&version[1]) }) }; @@ -120,7 +153,7 @@ pub fn userlist_insert(config: Cfg, list_id: String, mod_id: String, current_ver let connection = Connection::open(data)?; - connection.execute(format!("INSERT INTO {} VALUES (?1, ?2, ?3, ?4)", list_id).as_str(), [mod_id, current_version, applicable_versions.join("|"), current_link])?; + connection.execute(format!("INSERT INTO {} VALUES (?1, ?2, ?3, ?4, 'NONE')", list_id).as_str(), [mod_id, current_version, applicable_versions.join("|"), current_link])?; Ok(()) } @@ -136,7 +169,7 @@ pub fn userlist_get_all_ids(config: Cfg, list_id: String) -> Result, })?; for id in id_iter { - println!("Found id {:?}", id.as_ref().unwrap()); + //println!("Found id {:?}", id.as_ref().unwrap()); mod_ids.push(id?) }; @@ -167,7 +200,26 @@ pub fn userlist_get_applicable_versions(config: Cfg, list_id: String, mod_id: St })?; for ver in ver_iter { - println!("Found id {:?}", ver); + version = ver?; + }; + + match version.is_empty() { + true => Err(Box::new(Error::new(ErrorKind::NotFound, "MOD_NOT_FOUND"))), + false => Ok(version), + } +} + +pub fn userlist_get_current_version(config: Cfg, list_id: String, mod_id: String) -> Result> { + let data = format!("{}/data.db", config.data); + let connection = Connection::open(data).unwrap(); + + let mut version: String = String::new(); + let mut stmt = connection.prepare(format!("SELECT current_version FROM {} WHERE mod_id = ?", list_id).as_str())?; + let ver_iter = stmt.query_map([&mod_id], |row| { + row.get::(0) + })?; + + for ver in ver_iter { version = ver?; }; @@ -204,6 +256,40 @@ pub fn userlist_change_versions(config: Cfg, list_id: String, current_version: S Ok(()) } +pub fn userlist_add_disabled_versions(config: Cfg, list_id: String, disabled_version: String, mod_id: String) -> Result<(), Box> { + let data = format!("{}/data.db", config.data); + let connection = Connection::open(data)?; + + let currently_disabled_versions = userlist_get_disabled_versions(config, String::from(&list_id), String::from(&mod_id))?; + let disabled_versions = match currently_disabled_versions == "NONE" { + true => disabled_version, + false => format!("{}|{}", currently_disabled_versions, disabled_version), + }; + + connection.execute(format!("UPDATE {} SET disabled_versions = ?1 WHERE mod_id = ?2", list_id).as_str(), [disabled_versions, mod_id])?; + Ok(()) +} + +pub fn userlist_get_disabled_versions(config:Cfg, list_id: String, mod_id: String) -> Result> { + let data = format!("{}/data.db", config.data); + let connection = Connection::open(data).unwrap(); + + let mut version: String = String::new(); + let mut stmt = connection.prepare(format!("SELECT disabled_versions FROM {} WHERE mod_id = ?", list_id).as_str())?; + let ver_iter = stmt.query_map([mod_id], |row| { + row.get::(0) + })?; + + for ver in ver_iter { + version = ver?; + }; + + match version.is_empty() { + true => Err(Box::new(Error::new(ErrorKind::NotFound, "MOD_NOT_FOUND"))), + false => Ok(version), + } +} + pub fn userlist_get_all_downloads(config: Cfg, list_id: String) -> Result, Box> { let data = format!("{}/data.db", config.data); let connection = Connection::open(data).unwrap(); @@ -233,7 +319,7 @@ pub fn lists_insert(config: Cfg, id: String, mc_version: String, mod_loader: Mod let connection = Connection::open(data)?; connection.execute("INSERT INTO lists VALUES (?1, ?2, ?3, ?4)", [id.clone(), mc_version, mod_loader.stringify(), download_folder])?; - connection.execute(format!("CREATE TABLE {}( 'mod_id' TEXT, 'current_version' TEXT, 'applicable_versions' BLOB, 'current_download' TEXT )", id).as_str(), [])?; + connection.execute(format!("CREATE TABLE {}( 'mod_id' TEXT, 'current_version' TEXT, 'applicable_versions' BLOB, 'current_download' TEXT, 'disabled_versions' TEXT DEFAULT 'NONE' )", id).as_str(), [])?; Ok(()) } @@ -360,11 +446,17 @@ pub fn s_config_get_version(config: Cfg) -> Result Result<(), Box> { +pub fn s_insert_column(config: Cfg, table: String, column: String, c_type: String, default: Option) -> Result<(), Box> { let data = format!("{}/data.db", config.data); let connection = Connection::open(data)?; - connection.execute(format!("ALTER TABLE {} ADD '{}' {}", table, column, c_type).as_str(), ())?; + let mut sql = format!("ALTER TABLE {} ADD '{}' {}", table, column, c_type); + + if default.is_some() { + sql = format!("{} DEFAULT {}", sql, default.unwrap()); + } + + connection.execute(sql.as_str(), ())?; Ok(()) } diff --git a/src/files.rs b/src/files.rs new file mode 100644 index 0000000..1c0b13c --- /dev/null +++ b/src/files.rs @@ -0,0 +1,44 @@ +use std::{fs::{File, read_dir}, io::Write, collections::HashMap}; +use futures_util::StreamExt; +use reqwest::Client; + +use crate::List; + +pub async fn download_file(url: String, path: String, name: String) -> Result<(), Box> { + println!("Downloading {}", url); + let dl_path_file = format!("{}/{}", path, name); + let res = Client::new() + .get(String::from(&url)) + .send() + .await?; + + // download chunks + let mut file = File::create(String::from(&dl_path_file))?; + let mut stream = res.bytes_stream(); + + while let Some(item) = stream.next().await { + let chunk = item?; + file.write_all(&chunk)?; + } + + Ok(()) +} + +pub fn get_file_path(list: List, versionid: String) -> Result> { + let mut names: HashMap = HashMap::new(); + for file in read_dir(list.download_folder)? { + let path = file?.path(); + if path.is_file() { + let pathstr = path.to_str().ok_or("BAH")?; + let namesplit: Vec<&str> = pathstr.split('.').collect(); + let ver_id = namesplit[namesplit.len() - 2]; + names.insert(String::from(ver_id), String::from(pathstr)); + } + }; + + let api_versionid = format!("mr{}", versionid); + + let filename = names.get(&api_versionid).ok_or("VERSION_NOT_FOUND_IN_FILES")?; + + Ok(filename.to_owned()) +} diff --git a/src/input.rs b/src/input.rs index 0946971..b2b4f1b 100644 --- a/src/input.rs +++ b/src/input.rs @@ -10,6 +10,7 @@ pub struct Input { pub all_lists: bool, pub delete_old: bool, pub clean: bool, + pub disable_download: bool, } impl Input { @@ -20,7 +21,9 @@ impl Input { let mut all_lists = false; let mut delete_old = false; let mut clean = false; - + let mut disable_download = false; + + let mut toremove: Vec = vec![]; for (i, input) in split.clone().into_iter().enumerate() { if input.starts_with("--") { match input { @@ -28,12 +31,17 @@ impl Input { "--all-lists" => all_lists = true, "--delete-old" => delete_old = true, "--clean" => clean = true, + "--disable-download" => disable_download = true, _ => continue, } - split.remove(i); + toremove.push(i) } } + for rem in toremove.into_iter().rev() { + split.remove(rem); + } + let command = Cmd::from(split.remove(0))?; let subcommand = match split.is_empty() { false => Some(Subcmd::from(split.remove(0))?), @@ -51,7 +59,7 @@ impl Input { } }; - Ok(Self { command, subcommand, args, direct_download, all_lists, delete_old, clean }) + Ok(Self { command, subcommand, args, direct_download, all_lists, delete_old, clean, disable_download }) } } @@ -127,6 +135,11 @@ pub async fn get_input(config: Cfg) -> Result<(), Box> { #[test] fn input_from() { let string = "list add test 1.19.2 fabric"; - let input = Input{ command: Cmd::List, subcommand: Some(Subcmd::Add), args: Some(vec![String::from("test"), String::from("1.19.2"), String::from("fabric")]), direct_download: false, all_lists: false, clean: false, delete_old: false }; + let input = Input{ command: Cmd::List, subcommand: Some(Subcmd::Add), args: Some(vec![String::from("test"), String::from("1.19.2"), String::from("fabric")]), direct_download: false, all_lists: false, clean: false, delete_old: false, disable_download: false }; + assert_eq!(Input::from(string).unwrap(), input); + + let string = "update --direct-download --delete-old"; + let input = Input{ command: Cmd::Update, subcommand: None, args: None, direct_download: true, all_lists: false, clean: false, delete_old: true, disable_download: false }; assert_eq!(Input::from(string).unwrap(), input); + } diff --git a/src/lib.rs b/src/lib.rs index e4ebf76..971f544 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,13 +4,12 @@ pub mod commands; pub mod input; pub mod db; pub mod error; +pub mod files; -use std::{io::{Error, ErrorKind, Write}, fs::File}; +use std::io::{Error, ErrorKind}; pub use apis::*; pub use commands::*; -use futures_util::StreamExt; -use reqwest::Client; #[derive(Debug, Clone, PartialEq, Eq)] pub enum Modloader { @@ -34,23 +33,3 @@ impl Modloader { } } } - -pub async fn download_file(url: String, path: String, name: String) -> Result<(), Box> { - println!("Downloading {}", url); - let dl_path_file = format!("{}/{}", path, name); - let res = Client::new() - .get(String::from(&url)) - .send() - .await?; - - // download chunks - let mut file = File::create(String::from(&dl_path_file))?; - let mut stream = res.bytes_stream(); - - while let Some(item) = stream.next().await { - let chunk = item?; - file.write_all(&chunk)?; - } - - Ok(()) -} -- cgit v1.2.3