Add tests and example for modpack support

This commit is contained in:
Daniel Hutzley
2021-11-20 11:56:44 -08:00
parent a204df5e11
commit 52ed070b5b
5 changed files with 459 additions and 60 deletions

45
Cargo.lock generated
View File

@@ -17,6 +17,35 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "argh"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f023c76cd7975f9969f8e29f0e461decbdc7f51048ce43427107a3d192f1c9bf"
dependencies = [
"argh_derive",
"argh_shared",
]
[[package]]
name = "argh_derive"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48ad219abc0c06ca788aface2e3a1970587e3413ab70acd20e54b6ec524c1f8f"
dependencies = [
"argh_shared",
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "argh_shared"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38de00daab4eac7d753e97697066238d67ce9d7e2d823ab4f72fe14af29f3f33"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.0.1" version = "1.0.1"
@@ -329,6 +358,15 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.19" version = "0.1.19"
@@ -1000,6 +1038,7 @@ dependencies = [
name = "theseus" name = "theseus"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"argh",
"bytes", "bytes",
"chrono", "chrono",
"daedalus", "daedalus",
@@ -1175,6 +1214,12 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.2" version = "0.2.2"

View File

@@ -29,3 +29,9 @@ tokio = { version = "1", features = ["full"] }
futures = "0.3" futures = "0.3"
sys-info = "0.9.0" sys-info = "0.9.0"
[dev-dependencies]
argh = "0.1.6"
[[example]]
name = "download-pack"

View File

@@ -0,0 +1,53 @@
use std::{path::PathBuf, time::Instant};
use argh::FromArgs;
use theseus::modpack::{fetch_modpack, manifest::ModpackSide};
#[derive(FromArgs)]
/// Simple modpack download
struct ModpackDownloader {
/// where to download to
#[argh(positional)]
url: String,
/// where to put the resulting pack
#[argh(option, short = 'o')]
output: Option<PathBuf>,
/// the sha1 hash, if you want it checked
#[argh(option, short = 'c')]
hash: Option<String>,
/// use verbose logging
#[argh(switch, short = 'v')]
verbose: bool,
}
// Simple logging helper
fn debug(msg: &str, verbose: bool) {
if verbose {
println!("{}", msg);
}
}
#[tokio::main]
pub async fn main() {
let args = argh::from_env::<ModpackDownloader>();
let dest = args.output.unwrap_or(PathBuf::from("./pack-download/"));
debug(
&format!(
"Downloading pack {} to {}",
args.url,
dest.to_str().unwrap_or("?")
),
args.verbose,
);
let start = Instant::now();
fetch_modpack(&args.url, args.hash.as_deref(), &dest, ModpackSide::Client).await;
let end = start.elapsed();
println!("Download completed in {} seconds", end.as_secs_f32());
debug("Done!", args.verbose);
}

View File

@@ -1,13 +1,18 @@
use std::{convert::TryFrom, path::{Path, PathBuf}, str::FromStr}; use std::{
convert::TryFrom,
path::{Path, PathBuf},
str::FromStr,
};
use daedalus::download_file_mirrors; use daedalus::download_file_mirrors;
use tokio::fs;
use futures::future; use futures::future;
use tokio::fs;
use crate::launcher::ModLoader; use crate::launcher::ModLoader;
use super::ModpackError; use super::ModpackError;
#[derive(Debug, Clone, PartialEq)]
pub struct Manifest { pub struct Manifest {
format_version: u64, format_version: u64,
game: ModpackGame, game: ModpackGame,
@@ -22,27 +27,36 @@ pub struct Manifest {
impl Manifest { impl Manifest {
/// Download a modpack's files for a given side to a given destination /// Download a modpack's files for a given side to a given destination
/// Assumes the destination exists and is a directory /// Assumes the destination exists and is a directory
pub async fn download_files(&self, dest: &Path, side: &ModpackSide) -> Result<(), ModpackError> { pub async fn download_files(&self, dest: &Path, side: ModpackSide) -> Result<(), ModpackError> {
let handles = self.files.clone().into_iter() let handles = self.files.clone().into_iter().map(move |file| {
.map(move |file| { let (dest, side) = (dest.to_owned(), side);
let (dest, side) = (dest.to_owned(), *side); tokio::spawn(async move { file.fetch(&dest, side).await })
tokio::spawn(async move { });
file.fetch(&dest, &side).await future::try_join_all(handles)
}) .await?
}); .into_iter()
future::try_join_all(handles).await?.into_iter().collect::<Result<_, ModpackError>>()?; .collect::<Result<_, ModpackError>>()?;
// TODO Integrate instance format to save other metadata // TODO Integrate instance format to save other metadata
Ok(()) Ok(())
} }
} }
fn try_get<'r, F, T>(manifest: &'r serde_json::Map<String, serde_json::Value>, field: &str, caster: F) -> Result<T, ModpackError> fn try_get<'r, F, T>(
where manifest: &'r serde_json::Map<String, serde_json::Value>,
F: Fn(&'r serde_json::Value) -> Option<T> { field: &str,
manifest.get(field) caster: F,
) -> Result<T, ModpackError>
where
F: Fn(&'r serde_json::Value) -> Option<T>,
{
manifest
.get(field)
.and_then(caster) .and_then(caster)
.ok_or(ModpackError::ManifestError(format!("Invalid or missing field: {}", field))) .ok_or(ModpackError::ManifestError(format!(
"Invalid or missing field: {}",
field
)))
} }
impl TryFrom<serde_json::Value> for Manifest { impl TryFrom<serde_json::Value> for Manifest {
@@ -51,8 +65,9 @@ impl TryFrom<serde_json::Value> for Manifest {
fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> { fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
use ModpackError::ManifestError; use ModpackError::ManifestError;
let value = value.as_object() let value = value.as_object().ok_or(ManifestError(String::from(
.ok_or(ManifestError(String::from("Manifest is not a JSON object!")))?; "Manifest is not a JSON object!",
)))?;
let game = ModpackGame::new( let game = ModpackGame::new(
try_get(value, "game", serde_json::Value::as_str)?, try_get(value, "game", serde_json::Value::as_str)?,
@@ -60,42 +75,68 @@ impl TryFrom<serde_json::Value> for Manifest {
)?; )?;
let files = try_get(value, "files", serde_json::Value::as_array)? let files = try_get(value, "files", serde_json::Value::as_array)?
.iter().map(|it| -> Result<ModpackFile, ModpackError> { .iter()
let file = it.as_object() .map(|it| -> Result<ModpackFile, ModpackError> {
let file = it
.as_object()
.ok_or(ManifestError(String::from("Malformed file: not an object")))?; .ok_or(ManifestError(String::from("Malformed file: not an object")))?;
let path = Path::new(try_get(file, "path", serde_json::Value::as_str)?); let path = Path::new(try_get(file, "path", serde_json::Value::as_str)?);
let hashes = ModpackFileHashes::try_from(try_get(file, "hashes", serde_json::Value::as_object)?)?; let hashes = ModpackFileHashes::try_from(try_get(
let sources = try_get(file, "sources", serde_json::Value::as_array)?.iter() file,
"hashes",
serde_json::Value::as_object,
)?)?;
let downloads = try_get(file, "downloads", serde_json::Value::as_array)?
.iter()
.map(serde_json::Value::as_str) .map(serde_json::Value::as_str)
.map(|it| it.map(String::from)) .map(|it| it.map(String::from))
.collect::<Option<Vec<String>>>() .collect::<Option<Vec<String>>>()
.ok_or(ManifestError(format!("Invalid source for path {}", path.to_str().unwrap_or("?"))))?; .ok_or(ManifestError(format!(
let env: Option<[ModpackEnv;2]> = if let Some(env) = file.get("env") { "Invalid source for path {}",
if !env.is_object() { path.to_str().unwrap_or("?")
return Err(ManifestError(String::from("Env is provided, but is not an object!"))); )))?;
} let env: Option<[ModpackEnv; 2]> = if let Some(env) = file.get("env") {
Some([ModpackEnv::from_str(env.get("client").and_then(serde_json::Value::as_str).unwrap_or_default())?, if !env.is_object() {
ModpackEnv::from_str(env.get("server").and_then(serde_json::Value::as_str).unwrap_or_default())?]) return Err(ManifestError(String::from(
} else { "Env is provided, but is not an object!",
None )));
}; }
Some([
ModpackEnv::from_str(
env.get("client")
.and_then(serde_json::Value::as_str)
.unwrap_or_default(),
)?,
ModpackEnv::from_str(
env.get("server")
.and_then(serde_json::Value::as_str)
.unwrap_or_default(),
)?,
])
} else {
None
};
ModpackFile::new(path, hashes, env, sources.as_slice()) ModpackFile::new(path, hashes, env, &downloads)
} })
).collect::<Result<Vec<ModpackFile>, ModpackError>>()?; .collect::<Result<Vec<ModpackFile>, ModpackError>>()?;
Ok(Self { Ok(Self {
format_version: try_get(value, "formatVersion", serde_json::Value::as_u64)?, format_version: try_get(value, "formatVersion", serde_json::Value::as_u64)?,
game, game,
version_id: String::from(try_get(value, "versionId", serde_json::Value::as_str)?), version_id: String::from(try_get(value, "versionId", serde_json::Value::as_str)?),
name: String::from(try_get(value, "name", serde_json::Value::as_str)?), name: String::from(try_get(value, "name", serde_json::Value::as_str)?),
summary: value.get("summary").and_then(serde_json::Value::as_str).map(String::from), summary: value
files .get("summary")
.and_then(serde_json::Value::as_str)
.map(String::from),
files,
}) })
} }
} }
#[derive(Debug, Clone, PartialEq)]
pub enum ModpackGame { pub enum ModpackGame {
// TODO: Currently, the launcher does not support specifying mod loader versions, so I just // TODO: Currently, the launcher does not support specifying mod loader versions, so I just
// store the loader here. // store the loader here.
@@ -103,7 +144,10 @@ pub enum ModpackGame {
} }
impl ModpackGame { impl ModpackGame {
pub fn new(game: &str, deps: &serde_json::Map<String, serde_json::Value>) -> Result<Self, ModpackError> { pub fn new(
game: &str,
deps: &serde_json::Map<String, serde_json::Value>,
) -> Result<Self, ModpackError> {
match game { match game {
"minecraft" => { "minecraft" => {
let game_version = String::from( let game_version = String::from(
@@ -135,15 +179,15 @@ impl ModpackGame {
} }
} }
#[derive(Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct ModpackFile { pub struct ModpackFile {
path: PathBuf, path: PathBuf,
hashes: ModpackFileHashes, hashes: ModpackFileHashes,
envs: Option<[ModpackEnv; 2]>, envs: Option<[ModpackEnv; 2]>,
sources: Vec<String>, downloads: Vec<String>,
} }
#[derive(Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum ModpackSide { pub enum ModpackSide {
Client = 0, Client = 0,
Server = 1, Server = 1,
@@ -153,25 +197,28 @@ impl ModpackFile {
pub fn new( pub fn new(
path: &Path, path: &Path,
hashes: ModpackFileHashes, hashes: ModpackFileHashes,
envs: Option<[ModpackEnv;2]>, envs: Option<[ModpackEnv; 2]>,
sources: &[String], downloads: &[String],
) -> Result<Self, ModpackError> { ) -> Result<Self, ModpackError> {
if !path.is_dir() { if path.is_dir() {
return Err(ModpackError::ManifestError(format!("Modpack file {} is a directory!", path.to_str().unwrap_or("?")))); return Err(ModpackError::ManifestError(format!(
"Modpack file {} is a directory!",
path.to_str().unwrap_or("?")
)));
} }
Ok(Self { Ok(Self {
path: PathBuf::from(path), path: PathBuf::from(path),
hashes, hashes,
envs, envs,
sources: Vec::from(sources), downloads: Vec::from(downloads),
}) })
} }
pub async fn fetch(&self, dest: &Path, side: &ModpackSide) -> Result<(), ModpackError> { pub async fn fetch(&self, dest: &Path, side: ModpackSide) -> Result<(), ModpackError> {
if let Some(envs) = &self.envs { if let Some(envs) = &self.envs {
if envs[*side as usize] == ModpackEnv::Unsupported if envs[side as usize] == ModpackEnv::Unsupported
|| envs[(*side as usize + 1) % 2] == ModpackEnv::Required || envs[(side as usize + 1) % 2] == ModpackEnv::Required
{ {
return Ok(()); return Ok(());
} }
@@ -181,14 +228,24 @@ impl ModpackFile {
// HACK: Since Daedalus appends a file name to all mirrors and the manifest supplies full // HACK: Since Daedalus appends a file name to all mirrors and the manifest supplies full
// URLs, I'm supplying it with an empty string to avoid reinventing the wheel. // URLs, I'm supplying it with an empty string to avoid reinventing the wheel.
let bytes = download_file_mirrors("", &self.sources.iter().map(|it| it.as_str()).collect::<Vec<&str>>().as_slice(), Some(&self.hashes.sha1)).await?; let bytes = download_file_mirrors(
"",
&self
.downloads
.iter()
.map(|it| it.as_str())
.collect::<Vec<&str>>()
.as_slice(),
Some(&self.hashes.sha1),
)
.await?;
fs::create_dir_all(output.parent().unwrap()).await?; fs::create_dir_all(output.parent().unwrap()).await?;
fs::write(output, bytes).await?; fs::write(output, bytes).await?;
Ok(()) Ok(())
} }
} }
#[derive(Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct ModpackFileHashes { pub struct ModpackFileHashes {
sha1: String, sha1: String,
} }
@@ -202,7 +259,7 @@ impl TryFrom<&serde_json::Map<String, serde_json::Value>> for ModpackFileHashes
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum ModpackEnv { pub enum ModpackEnv {
Required, Required,
Optional, Optional,
@@ -218,7 +275,10 @@ impl FromStr for ModpackEnv {
"required" => Ok(Required), "required" => Ok(Required),
"optional" => Ok(Optional), "optional" => Ok(Optional),
"unsupported" => Ok(Unsupported), "unsupported" => Ok(Unsupported),
_ => Err(ModpackError::ManifestError(format!("Invalid environment support: {}", s))), _ => Err(ModpackError::ManifestError(format!(
"Invalid environment support: {}",
s
))),
} }
} }
} }
@@ -228,3 +288,195 @@ impl Default for ModpackEnv {
Self::Optional Self::Optional
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple() -> Result<(), ModpackError> {
const PACK_JSON: &'static str = r#"
{
"formatVersion": 1,
"game": "minecraft",
"versionId": "deadbeef",
"name": "Example Pack",
"files": [],
"dependencies": {
"minecraft": "1.17.1"
}
}
"#;
let expected_manifest = Manifest {
format_version: 1,
game: ModpackGame::Minecraft(String::from("1.17.1"), ModLoader::Vanilla),
version_id: String::from("deadbeef"),
name: String::from("Example Pack"),
summary: None,
files: Vec::new(),
};
let manifest_json: serde_json::Value =
serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
let manifest = Manifest::try_from(manifest_json)?;
assert_eq!(expected_manifest, manifest);
Ok(())
}
#[test]
fn parse_forge() -> Result<(), ModpackError> {
const PACK_JSON: &'static str = r#"
{
"formatVersion": 1,
"game": "minecraft",
"versionId": "deadbeef",
"name": "Example Pack",
"files": [
{
"path": "mods/testmod.jar",
"hashes": {
"sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"downloads": [
"https://example.com/testmod.jar"
]
}
],
"dependencies": {
"minecraft": "1.17.1",
"forge": "37.0.110"
}
}
"#;
let expected_manifest = Manifest {
format_version: 1,
game: ModpackGame::Minecraft(String::from("1.17.1"), ModLoader::Forge),
version_id: String::from("deadbeef"),
name: String::from("Example Pack"),
summary: None,
files: vec![ModpackFile::new(
Path::new("mods/testmod.jar"),
ModpackFileHashes {
sha1: String::from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
},
None,
&[String::from("https://example.com/testmod.jar")],
)?],
};
let manifest_json: serde_json::Value =
serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
let manifest = Manifest::try_from(manifest_json)?;
assert_eq!(expected_manifest, manifest);
Ok(())
}
#[test]
fn parse_fabric() -> Result<(), ModpackError> {
const PACK_JSON: &'static str = r#"
{
"formatVersion": 1,
"game": "minecraft",
"versionId": "deadbeef",
"name": "Example Pack",
"files": [
{
"path": "mods/testmod.jar",
"hashes": {
"sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"downloads": [
"https://example.com/testmod.jar"
]
}
],
"dependencies": {
"minecraft": "1.17.1",
"fabric-loader": "0.9.0"
}
}
"#;
let expected_manifest = Manifest {
format_version: 1,
game: ModpackGame::Minecraft(String::from("1.17.1"), ModLoader::Fabric),
version_id: String::from("deadbeef"),
name: String::from("Example Pack"),
summary: None,
files: vec![ModpackFile::new(
Path::new("mods/testmod.jar"),
ModpackFileHashes {
sha1: String::from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
},
None,
&[String::from("https://example.com/testmod.jar")],
)?],
};
let manifest_json: serde_json::Value =
serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
let manifest = Manifest::try_from(manifest_json)?;
assert_eq!(expected_manifest, manifest);
Ok(())
}
#[test]
fn parse_complete() -> Result<(), ModpackError> {
const PACK_JSON: &'static str = r#"
{
"formatVersion": 1,
"game": "minecraft",
"versionId": "deadbeef",
"name": "Example Pack",
"summary": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"files": [
{
"path": "mods/testmod.jar",
"hashes": {
"sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
},
"env": {
"client": "required",
"server": "unsupported"
},
"downloads": [
"https://example.com/testmod.jar"
]
}
],
"dependencies": {
"minecraft": "1.17.1",
"forge": "37.0.110"
}
}
"#;
let expected_manifest = Manifest {
format_version: 1,
game: ModpackGame::Minecraft(String::from("1.17.1"), ModLoader::Forge),
version_id: String::from("deadbeef"),
name: String::from("Example Pack"),
summary: Some(String::from("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")),
files: vec![
ModpackFile::new(
Path::new("mods/testmod.jar"),
ModpackFileHashes {
sha1: String::from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
},
Some([ ModpackEnv::Required, ModpackEnv::Unsupported ]),
&[ String::from("https://example.com/testmod.jar") ],
)?
],
};
let manifest_json: serde_json::Value =
serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON");
let manifest = Manifest::try_from(manifest_json)?;
assert_eq!(expected_manifest, manifest);
Ok(())
}
}

View File

@@ -1,12 +1,15 @@
//! Provides utilties for downloading and parsing modpacks //! Provides utilties for downloading and parsing modpacks
use std::{convert::TryFrom, io, path::Path}; use daedalus::download_file;
use fs_extra::dir::CopyOptions; use fs_extra::dir::CopyOptions;
use std::{borrow::Borrow, convert::TryFrom, env, io, path::Path};
use tokio::fs; use tokio::fs;
use uuid::Uuid;
use zip::ZipArchive;
use self::manifest::Manifest; use self::manifest::Manifest;
mod manifest; pub mod manifest;
pub const MANIFEST_PATH: &'static str = "index.json"; pub const MANIFEST_PATH: &'static str = "index.json";
pub const OVERRIDES_PATH: &'static str = "overrides/"; pub const OVERRIDES_PATH: &'static str = "overrides/";
@@ -19,6 +22,12 @@ pub enum ModpackError {
#[error("I/O error while reading modpack: {0}")] #[error("I/O error while reading modpack: {0}")]
FSExtraError(#[from] fs_extra::error::Error), FSExtraError(#[from] fs_extra::error::Error),
#[error("Error extracting archive: {0}")]
ZipError(#[from] zip::result::ZipError),
#[error("Invalid modpack format: {0}")]
FormatError(String),
#[error("Invalid output directory: {0}")] #[error("Invalid output directory: {0}")]
InvalidDirectory(String), InvalidDirectory(String),
@@ -35,13 +44,44 @@ pub enum ModpackError {
JoinError(#[from] tokio::task::JoinError), JoinError(#[from] tokio::task::JoinError),
} }
/// Realise a modpack from a given URL
pub async fn fetch_modpack(
url: &str,
sha1: Option<&str>,
dest: &Path,
side: manifest::ModpackSide,
) -> Result<(), ModpackError> {
let bytes = download_file(url, sha1).await?;
let mut archive = ZipArchive::new(io::Cursor::new(&bytes as &[u8]))?;
realise_modpack_zip(&mut archive, dest, side).await
}
/// Realise a given modpack from a zip archive
pub async fn realise_modpack_zip(
archive: &mut ZipArchive<impl io::Read + io::Seek>,
dest: &Path,
side: manifest::ModpackSide,
) -> Result<(), ModpackError> {
let tmp = env::temp_dir().join(format!("theseus-{}/", Uuid::new_v4()));
archive.extract(&tmp)?;
realise_modpack(&tmp, dest, side).await
}
/// Realise a given modpack into an instance /// Realise a given modpack into an instance
pub async fn realise_modpack(dir: &Path, dest: &Path, side: &manifest::ModpackSide) -> Result<(), ModpackError> { pub async fn realise_modpack(
dir: &Path,
dest: &Path,
side: manifest::ModpackSide,
) -> Result<(), ModpackError> {
if dest.is_file() { if dest.is_file() {
return Err(ModpackError::InvalidDirectory(String::from("Output is not a directory"))); return Err(ModpackError::InvalidDirectory(String::from(
"Output is not a directory",
)));
} }
if dest.exists() && std::fs::read_dir(dest).map_or(false, |it| it.count() != 0) { if dest.exists() && std::fs::read_dir(dest).map_or(false, |it| it.count() != 0) {
return Err(ModpackError::InvalidDirectory(String::from("Output directory is non-empty"))); return Err(ModpackError::InvalidDirectory(String::from(
"Output directory is non-empty",
)));
} }
if !dest.exists() { if !dest.exists() {
fs::create_dir_all(dest).await?; fs::create_dir_all(dest).await?;
@@ -57,9 +97,12 @@ pub async fn realise_modpack(dir: &Path, dest: &Path, side: &manifest::ModpackSi
// NOTE: I'm using standard files here, since Serde does not support async readers // NOTE: I'm using standard files here, since Serde does not support async readers
let manifest_path = Some(dir.join(MANIFEST_PATH)) let manifest_path = Some(dir.join(MANIFEST_PATH))
.filter(|it| it.exists() && it.is_file()) .filter(|it| it.exists() && it.is_file())
.ok_or(ModpackError::ManifestError(String::from("Manifest missing or is not a file")))?; .ok_or(ModpackError::ManifestError(String::from(
"Manifest missing or is not a file",
)))?;
let manifest_file = std::fs::File::open(manifest_path)?; let manifest_file = std::fs::File::open(manifest_path)?;
let manifest_json: serde_json::Value = serde_json::from_reader(io::BufReader::new(manifest_file))?; let manifest_json: serde_json::Value =
serde_json::from_reader(io::BufReader::new(manifest_file))?;
let manifest = Manifest::try_from(manifest_json)?; let manifest = Manifest::try_from(manifest_json)?;
// Realise manifest // Realise manifest