use std::{collections::{BTreeMap, HashMap}, error::Error, fmt::Write, io::ErrorKind}; use std::path::PathBuf; use chrono::{DateTime, Utc}; use serde::{Serialize, Deserialize}; use log::{debug, warn}; use super::version::{*, manifest::*}; mod constants; use constants::*; #[derive(Serialize, Deserialize, Debug)] struct RemoteVersionIndexEntry { last_update: DateTime } #[derive(Serialize, Deserialize, Debug)] struct RemoteVersionIndex { versions: BTreeMap } impl Default for RemoteVersionIndex { fn default() -> Self { RemoteVersionIndex { versions: BTreeMap::new() } } } struct RemoteVersionList { versions: HashMap, index: RemoteVersionIndex } impl RemoteVersionList { async fn new(home: &str) -> Result> { let text = reqwest::get(URL_VERSION_MANIFEST).await?.error_for_status()?.text().await?; let manifest: VersionManifest = serde_json::from_str(text.as_str())?; let fname = format!("{home}/.remote.json"); let index: RemoteVersionIndex = match tokio::fs::read_to_string(fname).await { Ok(s) => serde_json::from_str(s.as_str())?, Err(e) => { if e.kind() == ErrorKind::NotFound { RemoteVersionIndex::default() } else { return Err(Box::new(e)); } } }; let mut versions = HashMap::new(); for v in manifest.versions { versions.insert(v.id.clone(), v); } Ok(RemoteVersionList { versions, index }) } } struct LocalVersionList { versions: BTreeMap } impl LocalVersionList { async fn load_version(path: &PathBuf) -> Result> { serde_json::from_str(tokio::fs::read_to_string(path).await?)?; } async fn load_versions(home: &str, skip: impl Fn(&str) -> bool) -> Result> { let mut rd = tokio::fs::read_dir(home).await?; let versions = BTreeMap::new(); while let Some(ent) = rd.next_entry().await? { // when the code is fugly let path = match ent.file_name().to_str() { Some(s) => { if skip(s) { debug!("Skipping local version {s} because (I assume) it is remotely tracked."); continue } /* FIXME: once https://github.com/rust-lang/rust/issues/127292 is closed, * use add_extension to avoid extra heap allocations (they hurt my feelings) */ let mut path = ent.path(); // can't use set_extension since s might contain a . (like 1.8.9) path.push(format!("{s}.json")); }, /* We just ignore directories with names that contain invalid unicode. Unfortunately, the laucher * will not be supporting such custom versions. Name your version something sensible please. */ None => { warn!("Ignoring a local version because its id contains invalid unicode."); continue } }; match Self::load_version(path).await { Ok(v) => { versions.insert(v.id.clone(), v); }, Err(e) => { // FIXME: just display the filename without to_string_lossy when https://github.com/rust-lang/rust/issues/120048 is closed warn!("Ignoring local version {}: {e}", ent.file_name().to_string_lossy()); } } } Ok(LocalVersionList { versions }) } } struct VersionList { offline: bool, remote: Option, local: LocalVersionList } impl VersionList { pub async fn online(home: &str) -> Result> { let remote = RemoteVersionList::new(home).await?; let local = LocalVersionList::load_versions(home, |s| remote.versions.contains_key(s)).await?; Ok(VersionList { offline: false, remote: Some(remote), local }) } pub async fn offline(home: &str) -> Result> { let local = LocalVersionList::load_versions(home, |_| false).await?; Ok(VersionList { offline: true, remote: None, local }) } }