feat: Downloading entire game works now

This commit is contained in:
LynithDev 2025-04-09 18:44:26 +02:00
parent 81ed2de0f5
commit 2a0f53e107
14 changed files with 607 additions and 73 deletions

45
Cargo.lock generated
View file

@ -2453,12 +2453,6 @@ dependencies = [
"digest",
]
[[package]]
name = "md5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memchr"
version = "2.7.4"
@ -2942,13 +2936,14 @@ dependencies = [
"base64 0.22.1",
"bytes",
"chrono",
"digest",
"dirs",
"discord-rich-presence",
"dunce",
"futures",
"indicatif",
"interpulse",
"md5",
"md-5",
"murmur2",
"onelauncher_entity",
"onelauncher_gamemode",
@ -2958,13 +2953,14 @@ dependencies = [
"rand 0.8.5",
"regex",
"reqwest",
"sanitize-filename",
"sea-orm",
"serde",
"serde_ini",
"serde_json",
"serde_repr",
"serde_with",
"sha1_smol",
"sha1",
"sha2",
"specta",
"sysinfo",
@ -3730,14 +3726,14 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.6"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.7",
"regex-syntax 0.8.4",
"regex-automata 0.4.9",
"regex-syntax 0.8.5",
]
[[package]]
@ -3751,13 +3747,13 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.7"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.4",
"regex-syntax 0.8.5",
]
[[package]]
@ -3768,9 +3764,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rend"
@ -4007,6 +4003,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "sanitize-filename"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc984f4f9ceb736a7bb755c3e3bd17dc56370af2600c9780dcc48c66453da34d"
dependencies = [
"regex",
]
[[package]]
name = "schemars"
version = "0.8.21"
@ -4404,12 +4409,6 @@ dependencies = [
"digest",
]
[[package]]
name = "sha1_smol"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]]
name = "sha2"
version = "0.10.8"

View file

@ -117,12 +117,14 @@ async_zip = { version = "0.0.17", features = [ "full" ] }
async-compression = { version = "0.4.12", features = [ "flate2", "tokio" ] }
flate2 = { version = "1.0" }
bytes = { version = "1.7" }
sanitize-filename = { version = "0.6" }
# crypto
sha1_smol = { version = "1.0", features = [ "std" ] }
digest = { version = "0.10" }
sha1 = { version = "0.10" }
sha2 = { version = "0.10" }
md-5 = { version = "0.10" }
murmur2 = { version = "0.1.0" }
md5 = { version = "0.7.0" }
# db
sea-orm = { version = "1.1", features = [

View file

@ -70,10 +70,11 @@ paste = { workspace = true }
discord-rich-presence = { workspace = true }
# crypto
sha1_smol = { workspace = true }
digest = { workspace = true }
sha1 = { workspace = true }
sha2 = { workspace = true }
murmur2 = { workspace = true }
md5 = { workspace = true }
md-5 = { workspace = true }
# io
zip = { workspace = true }
@ -84,6 +85,7 @@ base64 = { workspace = true }
async-compression = { workspace = true }
dunce = { workspace = true }
tempfile = { workspace = true }
sanitize-filename = { workspace = true }
# features[tauri]
tauri = { workspace = true, optional = true }

View file

@ -10,10 +10,10 @@ async fn main() -> LauncherResult<()> {
let mut metadata = state.metadata.write().await;
let versions = metadata.get_vanilla(GameLoader::Vanilla).await?;
let version = versions.versions.iter().find(|v| v.id == "1.8.9").ok_or_else(|| anyhow!("couldn't find 1.8.9 in version list"))?;
let version = versions.versions.iter().find(|v| v.id == "1.20.4").ok_or_else(|| anyhow!("couldn't find 1.8.9 in version list"))?;
let version_info = download_version_info(version, None, None, Some(true)).await?;
download_minecraft_ingressed(&version_info, Some(true)).await?;
download_minecraft_ingressed(&version_info, "x86", Some(true)).await?;
Ok(())
}

View file

@ -1,17 +1,25 @@
use std::path::PathBuf;
use futures::{StreamExt, stream};
use interpulse::api::minecraft::{AssetsIndex, Version, VersionInfo};
use interpulse::api::minecraft::{AssetsIndex, DownloadType, Library, Os, OsRule, Rule, RuleAction, Version, VersionInfo};
use interpulse::api::modded::LoaderVersion;
use regex::Regex;
use reqwest::Method;
use crate::api::ingress::{init_ingress, send_ingress_ref_opt};
use crate::error::LauncherResult;
use crate::error::{LauncherError, LauncherResult};
use crate::store::ingress::{IngressRef, IngressType};
use crate::store::{Core, Dirs};
use crate::utils::crypto::HashAlgorithm;
use crate::utils::http::{self, fetch_json_advanced};
use crate::utils::ext::OsExt;
use crate::utils::http;
use crate::utils::io;
pub async fn download_minecraft_ingressed(version: &VersionInfo, force: Option<bool>) -> LauncherResult<()> {
pub async fn download_minecraft_ingressed(
version: &VersionInfo,
java_arch: &str,
force: Option<bool>
) -> LauncherResult<()> {
let id = init_ingress(
IngressType::MinecraftDownload,
&format!("Downloading Minecraft {}", version.id),
@ -21,20 +29,27 @@ pub async fn download_minecraft_ingressed(version: &VersionInfo, force: Option<b
let ingress_ref = IngressRef::new(&id, 100.0);
download_minecraft(version, Some(&ingress_ref), force).await
download_minecraft(version, java_arch, Some(&ingress_ref), force).await
}
// MARK: Main
#[tracing::instrument(skip_all)]
pub async fn download_minecraft(version: &VersionInfo, ingress_ref: Option<&IngressRef<'_>>, force: Option<bool>) -> LauncherResult<()> {
pub async fn download_minecraft(
version: &VersionInfo,
java_arch: &str,
ingress_ref: Option<&IngressRef<'_>>,
force: Option<bool>
) -> LauncherResult<()> {
const TASKS: f64 = 2.0;
const TASKS: f64 = 4.0;
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / TASKS));
let ingress_ref = ingress_ref.as_ref();
let asset_index = download_assets_index(version, ingress_ref, force).await?;
tokio::try_join! {
download_assets(version.assets == "legacy", &asset_index, ingress_ref, force)
download_assets(version.assets == "legacy", &asset_index, ingress_ref, force),
download_client(version, ingress_ref, force),
download_libraries(&version.id, &version.libraries, java_arch, ingress_ref, force),
}?;
Ok(())
@ -48,6 +63,7 @@ pub async fn download_version_info(
ingress_ref: Option<&IngressRef<'_>>,
force: Option<bool>,
) -> LauncherResult<VersionInfo> {
tracing::debug!("loading version info for version {}", version.id);
let version_id = loader.map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id));
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 3.33));
@ -70,11 +86,11 @@ pub async fn download_version_info(
);
let mut info =
fetch_json_advanced(Method::GET, &version.url, None, None, None, ingress_ref).await?;
http::fetch_json_advanced(Method::GET, &version.url, None, None, None, ingress_ref).await?;
if let Some(loader) = loader {
let partial: interpulse::api::modded::PartialVersionInfo =
fetch_json_advanced(Method::GET, &loader.url, None, None, None, ingress_ref)
http::fetch_json_advanced(Method::GET, &loader.url, None, None, None, ingress_ref)
.await?;
info = interpulse::api::modded::merge_partial_version(partial, info);
@ -100,6 +116,7 @@ pub async fn download_assets_index(
ingress_ref: Option<&IngressRef<'_>>,
force: Option<bool>,
) -> LauncherResult<AssetsIndex> {
tracing::debug!("loading assets index for version {}", version.id);
let path = Dirs::get_assets_index_dir()
.await?
.join(version.asset_index.id.clone());
@ -115,7 +132,7 @@ pub async fn download_assets_index(
return Ok(data);
}
Err(err) => tracing::error!(
"failed to read assets index from cache: {err:?}, downloading again",
"failed to read assets index from cache: {err:?}, downloading...",
),
}
}
@ -135,13 +152,15 @@ pub async fn download_assets_index(
}
// MARK: Assets
/// Downloads the assets for a given asset index, returns the number of failed downloads
#[tracing::instrument(skip_all)]
pub async fn download_assets(
legacy: bool,
assets_index: &AssetsIndex,
ingress_ref: Option<&IngressRef<'_>>,
force: Option<bool>,
) -> LauncherResult<()> {
) -> LauncherResult<usize> {
tracing::debug!("loading asssets");
let len = assets_index.objects.len();
let dir = if legacy {
@ -176,13 +195,253 @@ pub async fn download_assets(
)
.await.map(|_| ())
} else {
// TODO: Possibly check hash? (not sure if this is a good idea here)
Ok(())
}
}))
.buffer_unordered(Core::get().fetch_attempts.min(7))
.collect::<Vec<_>>();
requests.await;
let mut failed = 0;
for res in requests.await {
if let Err(err) = res {
tracing::error!("failed to download asset: {err:?}");
failed += 1;
}
}
Ok(())
tracing::debug!("loaded assets");
Ok(failed)
}
// MARK: Client
pub async fn download_client(
version: &VersionInfo,
ingress_ref: Option<&IngressRef<'_>>,
force: Option<bool>,
) -> LauncherResult<PathBuf> {
tracing::debug!("loading client for version {}", version.id);
let client = version.downloads
.get(&DownloadType::Client)
.ok_or_else(|| anyhow::anyhow!("no client downloads exist for {}", &version.id))?;
let path = Dirs::get_versions_dir().await?.join(version.id.clone()).join(format!("{}.jar", version.id));
if path.exists() && !force.unwrap_or(false) {
let calculated_hash = HashAlgorithm::Sha1
.hash_file(&path)
.await
.map_err(|_| anyhow::anyhow!("failed to hash file"))?;
if client.sha1 == calculated_hash {
tracing::debug!("client already exists, skipping download");
return Ok(path);
}
tracing::warn!("client hash mismatch, redownloading");
}
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / 2.0));
let ingress_ref = ingress_ref.as_ref();
http::download(Method::GET, &client.url, &path, Some((HashAlgorithm::Sha1, &client.sha1)), ingress_ref).await?;
Ok(path)
}
// MARK: Libraries
/// Downloads the libraries and returns the number of failed downloads
pub async fn download_libraries(
version: &str,
libraries: &[Library],
java_arch: &str,
ingress_ref: Option<&IngressRef<'_>>,
force: Option<bool>,
) -> LauncherResult<usize> {
tracing::debug!("loading libraries for version {}", version);
let lib_dir = Dirs::get_libraries_dir().await?;
let natives_dir = Dirs::get_natives_dir().await?;
io::create_dir_all(&lib_dir).await?;
io::create_dir_all(&natives_dir).await?;
let num_files = libraries.len();
let ingress_ref = ingress_ref.map(|i| i.with_increment(i.increment_by / num_files as f64));
let ingress_ref = ingress_ref.as_ref();
let requests = stream::iter(libraries.iter().map(|lib| async {
if let Some(rules) = &lib.rules {
if !validate_rules(rules, java_arch, lib.natives.is_some()) {
tracing::debug!("skipping library {} due to rules", lib.name);
return Ok::<(), LauncherError>(());
}
}
if !lib.downloadable {
tracing::debug!("skipping library {} due to downloadability", lib.name);
return Ok(());
}
tokio::try_join! {
async {
let artifact_path = interpulse::utils::get_path_from_artifact(&lib.name)?;
let path = lib_dir.join(&artifact_path);
if path.exists() && !force.unwrap_or(false) {
return Ok(());
}
if let Some(interpulse::api::minecraft::LibraryDownloads {
artifact: Some(ref artifact), ..
}) = lib.downloads {
if !artifact.url.is_empty() {
http::download(
Method::GET,
&artifact.url,
&path,
Some((HashAlgorithm::Sha1, &artifact.sha1)),
ingress_ref,
).await?;
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
return Ok::<_, LauncherError>(());
}
}
let url = [lib.url.as_deref().unwrap_or("https://libraries.minecraft.net/"), &artifact_path].concat();
http::download(
Method::GET,
&url,
&path,
None,
ingress_ref,
).await?;
tracing::trace!("fetched library {} to path {:?}", &lib.name, &path);
Ok::<_, LauncherError>(())
},
async {
if let Some((os_key, classifiers)) = None.or_else(|| Some((
lib.natives.as_ref()?.get(&Os::native_arch(java_arch))?,
lib.downloads.as_ref()?.classifiers.as_ref()?
))) {
let parsed = os_key.replace("${arch}", crate::constants::ARCH_WIDTH);
if let Some(native) = classifiers.get(&parsed) {
tracing::trace!("found native library {}", &lib.name);
let data = http::fetch_advanced(
Method::GET,
&native.url,
None,
Some((HashAlgorithm::Sha1, &native.sha1)),
None,
ingress_ref,
).await?;
let dest = natives_dir.join(version);
io::unzip_bytes(data.to_vec(), &dest).await?;
tracing::trace!("extracted native {} to path {:?}", &lib.name, &dest);
}
}
Ok(())
}
}?;
Ok(())
})).buffer_unordered(7).collect::<Vec<_>>();
let mut failed = 0;
for res in requests.await {
if let Err(err) = res {
tracing::error!("failed to download asset: {err:?}");
failed += 1;
}
}
Ok(failed)
}
// MARK: Rules
#[tracing::instrument(level = "debug")]
fn validate_rules(rules: &[Rule], java_version: &str, updated: bool) -> bool {
let mut rule = rules
.iter()
.map(|r| validate_rule(r, java_version, updated))
.collect::<Vec<Option<bool>>>();
if rules
.iter()
.all(|r| matches!(r.action, RuleAction::Disallow))
{
rule.push(Some(true));
}
!(rule.iter().any(|r| r == &Some(false)) || rule.iter().all(Option::is_none))
}
/// Parses a Minecraft library feature or OS rule.
/// Is disallowed -> Don't include it
/// Is not allowed -> Don't include it
/// Is allowed -> Include it
#[tracing::instrument(level = "debug")]
fn validate_rule(rule: &Rule, java_version: &str, updated: bool) -> Option<bool> {
let result = match rule {
Rule {
os: Some(os), ..
} => validate_os_rule(os, java_version, updated),
Rule {
features: Some(features),
..
} => {
!features.is_demo_user.unwrap_or(true)
|| features.has_custom_resolution.unwrap_or(false)
|| !features.has_quick_plays_support.unwrap_or(true)
|| !features.is_quick_play_multiplayer.unwrap_or(true)
|| !features.is_quick_play_realms.unwrap_or(true)
|| !features.is_quick_play_singleplayer.unwrap_or(true)
}
_ => return Some(true),
};
match rule.action {
RuleAction::Allow => {
if result {
Some(true)
} else {
Some(false)
}
}
RuleAction::Disallow => {
if result {
Some(false)
} else {
None
}
}
}
}
#[must_use]
fn validate_os_rule(rule: &OsRule, java_arch: &str, updated: bool) -> bool {
let mut rule_match = true;
if let Some(ref arch) = rule.arch {
rule_match &= !matches!(arch.as_str(), "x86" | "arm");
}
if let Some(name) = &rule.name {
if updated && (name != &Os::LinuxArm64 || name != &Os::LinuxArm32) {
rule_match &= &Os::native() == name || &Os::native_arch(java_arch) == name;
} else {
rule_match &= &Os::native_arch(java_arch) == name;
}
}
if let Some(version) = &rule.version {
if let Ok(regex) = Regex::new(version.as_str()) {
rule_match &= regex.is_match(&sysinfo::System::os_version().unwrap_or_default());
}
}
rule_match
}

View file

@ -25,6 +25,8 @@ pub enum LauncherError {
DbError(#[from] sea_orm::DbErr),
#[error(transparent)]
ReqwestError(#[from] reqwest::Error),
#[error(transparent)]
InterpulseError(#[from] interpulse::Error),
#[error("couldn't acquire semaphore: {0}")]
SemaphoreError(#[from] tokio::sync::AcquireError),

View file

@ -1,4 +1,5 @@
#![feature(let_chains)]
#![feature(slice_as_array)]
use api::proxy::LauncherProxy;
use store::{proxy::ProxyState, semaphore::SemaphoreStore, Core, CoreOptions, Dirs, State};

View file

@ -99,6 +99,9 @@ pub struct IngressRef<'a> {
pub increment_by: f64,
}
unsafe impl Send for IngressRef<'_> {}
unsafe impl Sync for IngressRef<'_> {}
impl<'a> IngressRef<'a> {
#[must_use]
pub const fn new(ingress_id: &'a IngressId, increment_by: f64) -> Self {

View file

@ -177,21 +177,19 @@ impl Metadata {
($(($var:tt, $is_modded:tt)),*) => {
let ($($var),*) = tokio::join!(
$(
tokio::spawn(async move {
async move {
if let Ok(loader) = GameLoader::from_str(stringify!(minecraft)) {
fetch_manifest!(loader, $is_modded).map_err(|e| $crate::send_error!("couldn't fetch {loader} manifest: {e}")).ok()
} else {
tracing::error!("failed to fetch {} manifest", stringify!($var));
None
}
}),
},
)*
);
$(
if let Ok(data) = $var {
self.inner.$var = data;
}
self.inner.$var = $var;
)*
};
}

View file

@ -1,4 +1,11 @@
use std::{fmt::Display, str::FromStr};
use std::fmt::{Display, Write};
use std::io::Read;
use std::str::FromStr;
use digest::DynDigest;
use tokio::io::AsyncReadExt;
use super::io::IOError;
#[derive(Debug, thiserror::Error)]
pub enum CryptoError {
@ -9,20 +16,89 @@ pub enum CryptoError {
actual: String,
},
#[error("invalid hash algorithm")]
InvalidAlgorithm
InvalidAlgorithm,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HashAlgorithm {
Sha1,
Sha256,
Md5,
}
const BUFFER_SIZE: usize = 1024 * 256; // 16 KiB
impl HashAlgorithm {
#[must_use]
pub fn hash(&self, data: &[u8]) -> String {
match self {
Self::Sha1 => sha1_smol::Sha1::from(data).digest().to_string(),
}
pub fn hasher_sync<F>(&self, closure: F) -> Result<String, IOError>
where
F: Fn(&mut (dyn DynDigest)) -> Result<(), IOError>,
{
let mut hasher = get_inner(self);
closure(&mut *hasher)?;
Ok(to_hex(&hasher.finalize()))
}
pub async fn hasher<F>(&self, closure: F) -> Result<String, IOError>
where
F: AsyncFn(&mut (dyn DynDigest)) -> Result<(), IOError>,
{
let mut hasher = get_inner(self);
closure(&mut *hasher).await?;
Ok(to_hex(&hasher.finalize()))
}
pub async fn hash(&self, data: &[u8]) -> Result<String, IOError> {
self.hasher(async move |hasher| {
hasher.update(data);
Ok(())
}).await
}
pub fn hash_sync(&self, data: &[u8]) -> Result<String, IOError> {
self.hasher_sync(move |hasher| {
hasher.update(data);
Ok(())
})
}
pub async fn hash_file(&self, path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
self.hasher(async |hasher: &mut dyn DynDigest| {
let path = path.as_ref();
let file = tokio::fs::File::open(path).await?;
let mut reader = tokio::io::BufReader::new(file);
let mut buffer = vec![0; BUFFER_SIZE].into_boxed_slice();
loop {
let bytes_read = reader.read(&mut buffer).await?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(())
}).await
}
pub fn hash_file_sync(&self, path: impl AsRef<std::path::Path>) -> Result<String, IOError> {
self.hasher_sync(|hasher: &mut dyn DynDigest| {
let path = path.as_ref();
let file = std::fs::File::open(path)?;
let mut reader = std::io::BufReader::new(file);
let mut buffer = vec![0; BUFFER_SIZE].into_boxed_slice();
loop {
let bytes_read = reader.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
Ok(())
})
}
}
@ -30,6 +106,8 @@ impl Display for HashAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Sha1 => write!(f, "sha1"),
Self::Sha256 => write!(f, "sha256"),
Self::Md5 => write!(f, "md5"),
}
}
}
@ -39,8 +117,25 @@ impl FromStr for HashAlgorithm {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"sha1" => Ok(Self::Sha1),
"sha1" | "sha-1" => Ok(Self::Sha1),
"sha256" | "sha-256" => Ok(Self::Sha256),
"md5" | "md-5" => Ok(Self::Md5),
_ => Err(CryptoError::InvalidAlgorithm),
}
}
}
fn get_inner(algorithm: &HashAlgorithm) -> Box<dyn DynDigest> {
match algorithm {
HashAlgorithm::Sha1 => Box::new(sha1::Sha1::default()),
HashAlgorithm::Sha256 => Box::new(sha2::Sha256::default()),
HashAlgorithm::Md5 => Box::new(md5::Md5::default())
}
}
fn to_hex(data: &[u8]) -> String {
data.iter().fold(String::new(), |mut output, b| {
let _ = write!(output, "{b:02x}");
output
})
}

View file

@ -0,0 +1,48 @@
use interpulse::api::minecraft::Os;
/// An extension of [`Os`] for utilities on converting the current system's operating system
/// into an [`Os`] structure for simple management.
pub trait OsExt {
/// Get the [`Os`] of the current system
fn native() -> Self;
/// Get the [`Os`] of the current system along with it's arch from the `java_arch`.
fn native_arch(java_arch: &str) -> Self;
}
impl OsExt for Os {
fn native_arch(java_arch: &str) -> Self {
if std::env::consts::OS == "windows" {
if java_arch == "aarch64" {
Self::WindowsArm64
} else {
Self::Windows
}
} else if std::env::consts::OS == "linux" {
if java_arch == "aarch64" {
Self::LinuxArm64
} else if java_arch == "arm" {
Self::LinuxArm32
} else {
Self::Linux
}
} else if std::env::consts::OS == "macos" {
if java_arch == "aarch64" {
Self::OsxArm64
} else {
Self::Osx
}
} else {
Self::Unknown
}
}
fn native() -> Self {
match std::env::consts::OS {
"windows" => Self::Windows,
"macos" => Self::Osx,
"linux" => Self::Linux,
_ => Self::Unknown,
}
}
}

View file

@ -123,15 +123,21 @@ pub async fn fetch_advanced(
if let Some((ref algorithm, expected_hash)) = hash {
let expected_hash = expected_hash.to_string();
let calculated_hash = algorithm.hash(&bytes);
if *calculated_hash != expected_hash {
return Err(CryptoError::InvalidHash {
algorithm: algorithm.clone(),
expected: expected_hash,
actual: calculated_hash,
match algorithm.hash(&bytes).await {
Ok(calculated_hash) => {
if *calculated_hash != expected_hash {
return Err(CryptoError::InvalidHash {
algorithm: algorithm.clone(),
expected: expected_hash,
actual: calculated_hash,
}
.into());
}
},
Err(err) => {
tracing::error!("failed to calculate hash for {url}: {err}");
return Err(err.into());
}
.into());
}
}

View file

@ -1,13 +1,16 @@
use std::path::PathBuf;
use async_compression::tokio::bufread::GzipDecoder;
use serde::{de::DeserializeOwned, Serialize};
use serde::Serialize;
use serde::de::DeserializeOwned;
use tempfile::TempDir;
use tokio::io::AsyncReadExt;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
/// A wrapper around generic and unhelpful [`std::io::Error`] messages.
#[derive(Debug, thiserror::Error)]
pub enum IOError {
#[error("invalid absolute path '{0}'")]
InvalidAbsolutePath(std::path::PathBuf),
InvalidAbsolutePath(PathBuf),
#[error("error acessing path: {source}, path: {path}")]
IOErrorWrapper {
@ -21,6 +24,8 @@ pub enum IOError {
IOError(#[from] std::io::Error),
#[error("json deserialization error: {0}")]
DeserializeError(#[from] serde_json::Error),
#[error(transparent)]
AsyncZipError(#[from] async_zip::error::ZipError),
}
impl<P: AsRef<std::path::Path>> From<(P, std::io::Error)> for IOError {
@ -37,12 +42,12 @@ impl<P: AsRef<std::path::Path>> From<(P, std::io::Error)> for IOError {
pub struct NonUtf8PathError(pub Box<std::path::Path>);
/// Attempts to parse a path taken from an environment variable. Returns `None` if the variable is not set.
pub fn env_path(name: &str) -> Option<std::path::PathBuf> {
std::env::var_os(name).map(std::path::PathBuf::from)
pub fn env_path(name: &str) -> Option<PathBuf> {
std::env::var_os(name).map(PathBuf::from)
}
/// An OS specific wrapper of [`std::fs::canonicalize`], but on Windows it outputs the most compatible form of a path instead of UNC.
pub fn canonicalize(path: impl AsRef<std::path::Path>) -> Result<std::path::PathBuf, IOError> {
pub fn canonicalize(path: impl AsRef<std::path::Path>) -> Result<PathBuf, IOError> {
let path = path.as_ref();
dunce::canonicalize(path).map_err(|e| IOError::IOErrorWrapper {
source: e,
@ -134,11 +139,13 @@ pub async fn read(path: impl AsRef<std::path::Path>) -> Result<Vec<u8>, IOError>
}
/// Asynchronously read a file as JSON and return the deserialized object
pub async fn read_json<T: DeserializeOwned>(path: impl AsRef<std::path::Path>) -> Result<T, IOError> {
pub async fn read_json<T: DeserializeOwned>(
path: impl AsRef<std::path::Path>,
) -> Result<T, IOError> {
Ok(serde_json::from_slice(&read(path).await?)?)
}
/// Asynchrously write to a tempfile that is then transferred to an official [`AsRef<Path>`].
/// Asynchrously write to a file.
pub async fn write(
path: impl AsRef<std::path::Path>,
data: impl AsRef<[u8]>,
@ -152,10 +159,26 @@ pub async fn write(
})
}
/// Asynchronously write buffered data to a file, creating it if it does not exist.
pub async fn write_buf<F>(path: impl AsRef<std::path::Path>, f: F) -> Result<(), IOError>
where
F: AsyncFnOnce(&mut tokio::io::BufWriter<tokio::fs::File>) -> Result<(), IOError>,
{
let path = path.as_ref();
let file = tokio::fs::File::create(path).await?;
let mut writer = tokio::io::BufWriter::new(file);
f(&mut writer).await?;
writer.flush().await?;
Ok(())
}
/// Asynchronously write json to a file, creating it if it does not exist.
pub async fn write_json<T: Serialize>(
path: impl AsRef<std::path::Path>,
data: T
data: T,
) -> Result<(), IOError> {
write(path, serde_json::to_vec(&data)?).await
}
@ -210,3 +233,98 @@ pub fn tempdir() -> Result<TempDir, IOError> {
pub fn tempfile() -> Result<std::fs::File, IOError> {
Ok(tempfile::tempfile()?)
}
/// Unzips a zip archive from a byte array
pub async fn unzip_bytes(
data: Vec<u8>,
dest_path: impl AsRef<std::path::Path>
) -> Result<(), IOError> {
let reader = async_zip::base::read::mem::ZipFileReader::new(data).await?;
let entries = reader.file().entries();
for index in 0..entries.len() {
let entry = entries.get(index).expect("expected more zip entries");
let path = dest_path.as_ref().join(sanitize_path(entry.filename().as_str()?));
let is_dir = entry.dir()?;
let mut entry_reader = reader
.reader_without_entry(index)
.await
.expect("Failed to read ZipEntry");
if is_dir {
create_dir_all(path).await?;
} else {
if let Some(parent) = path.parent() {
create_dir_all(parent).await?;
}
let file = tokio::fs::File::create(&path).await?;
let mut writer = tokio::io::BufWriter::new(file);
let mut buf = vec![0; 8192];
loop {
let n = futures::AsyncReadExt::read(&mut entry_reader, &mut buf).await?;
if n == 0 {
break;
}
writer.write_all(&buf[..n]).await?;
}
}
}
Ok(())
}
/// Unzips a zip archive from a file
pub async fn unzip_file(
zip_path: impl AsRef<std::path::Path>,
dest_path: impl AsRef<std::path::Path>,
) -> Result<(), IOError> {
let zip_path = zip_path.as_ref();
let dest_path = dest_path.as_ref();
let reader = async_zip::tokio::read::fs::ZipFileReader::new(zip_path).await?;
let entries = reader.file().entries();
for index in 0..entries.len() {
let entry = entries
.get(index)
.expect("expected more zip entries");
let path = dest_path.join(sanitize_path(entry.filename().as_str()?));
let is_dir = entry.dir()?;
if is_dir {
create_dir_all(&path).await?;
} else {
if let Some(parent) = path.parent() {
create_dir_all(parent).await?;
}
let file = tokio::fs::File::create(&path).await?;
let mut writer = tokio::io::BufWriter::new(file);
let mut entry_reader = reader.reader_without_entry(index).await?;
let mut buf = vec![0; 8192];
loop {
let n = futures::AsyncReadExt::read(&mut entry_reader, &mut buf).await?;
if n == 0 {
break;
}
writer.write_all(&buf[..n]).await?;
}
}
}
Ok(())
}
pub fn sanitize_path(path: impl AsRef<std::path::Path>) -> PathBuf {
path.as_ref()
.to_string_lossy()
.replace('\\', "/")
.split('/')
.map(sanitize_filename::sanitize)
.collect()
}

View file

@ -1,3 +1,4 @@
pub mod crypto;
pub mod ext;
pub mod http;
pub mod io;