use crate::error::{CobbleError, CobbleResult};
use crate::minecraft::models::{AssetIndex, VersionData};
use crate::Instance;
use flate2::read::GzDecoder;
use flate2::write::GzEncoder;
use std::fs::{create_dir_all, remove_file, File};
use std::path::PathBuf;
pub const INSTANCE_MARKER_FILE: &str = ".cobble_instance.json";
pub const INSTANCE_FOLDER: &str = "instance";
pub const LIBRARIES_FOLDER: &str = "libraries";
pub const ASSETS_FOLDER: &str = "assets";
pub const LOG_CONFIGS_FOLDER: &str = "assets/log_configs";
impl Instance {
#[instrument(
name = "export_instance",
level = "trace",
skip_all,
fields(
instance_path = %self.instance_path.to_string_lossy(),
offline,
dest,
compression,
)
)]
#[cfg_attr(doc_cfg, doc(cfg(all(feature = "backup", not(feature = "fabric")))))]
#[cfg(not(feature = "fabric"))]
pub async fn export(
&self,
dest: impl AsRef<std::path::Path>,
offline: bool,
compression: u32,
) -> CobbleResult<()> {
if !self.installed {
return Err(CobbleError::NotInstalled);
}
let this = self.clone();
let dest = PathBuf::from(dest.as_ref());
trace!("Getting version data");
let version_data = self.read_version_data().await?;
trace!("Getting asset index");
let asset_index = version_data.asset_index.fetch_index().await?;
tokio::task::spawn_blocking(move || {
this.export_blocking(version_data, asset_index, dest, offline, compression)
})
.await?
}
#[instrument(name = "import_instance", level = "trace", skip_all, fields())]
#[cfg_attr(doc_cfg, doc(cfg(feature = "backup")))]
pub async fn import(
src: impl AsRef<std::path::Path>,
instance_path: impl AsRef<std::path::Path>,
libraries_path: impl AsRef<std::path::Path>,
assets_path: impl AsRef<std::path::Path>,
offline: bool,
) -> CobbleResult<Self> {
let src = PathBuf::from(src.as_ref());
let instance_path = PathBuf::from(instance_path.as_ref());
let libraries_path = PathBuf::from(libraries_path.as_ref());
let assets_path = PathBuf::from(assets_path.as_ref());
tokio::task::spawn_blocking(move || {
Self::import_blocking(src, instance_path, libraries_path, assets_path, offline)
})
.await?
}
#[instrument(
name = "export_instance_blocking",
level = "trace",
skip_all,
fields(
instance_path = %self.instance_path.to_string_lossy(),
offline,
dest,
compression,
)
)]
#[cfg(not(feature = "fabric"))]
fn export_blocking(
mut self,
version_data: VersionData,
asset_index: AssetIndex,
dest: PathBuf,
offline: bool,
compression: u32,
) -> CobbleResult<()> {
trace!("Creating archive file");
let archive_file = File::create(dest)?;
let encoder = GzEncoder::new(archive_file, flate2::Compression::new(compression));
let mut tar = tar::Builder::new(encoder);
append_instance_information(&mut self, offline, &mut tar)?;
append_instance_folder(&self, &mut tar)?;
if offline {
append_libraries(&self, &version_data, &mut tar)?;
append_assets(&self, &asset_index, &version_data, &mut tar)?;
append_log_config(&self, &version_data, &mut tar)?;
}
trace!("Flushing archive");
let mut encoder = tar.into_inner()?;
encoder.try_finish()?;
Ok(())
}
#[instrument(name = "import_instance_blocking", level = "trace", skip_all, fields())]
fn import_blocking(
src: PathBuf,
instance_path: PathBuf,
libraries_path: PathBuf,
assets_path: PathBuf,
offline: bool,
) -> CobbleResult<Self> {
let mut instance = read_instance_information(&src)?;
instance.instance_path = instance_path;
instance.libraries_path = libraries_path;
instance.assets_path = assets_path;
instance.installed = offline;
trace!("Opening source archive file");
let archive_file = File::open(src)?;
let decoder = GzDecoder::new(archive_file);
let mut tar = tar::Archive::new(decoder);
trace!("Extracting files");
for entry_result in tar.entries()? {
let mut entry = entry_result?;
let archive_path = entry.path()?;
if archive_path.starts_with(INSTANCE_FOLDER) {
let mut path = instance.instance_path();
path.push(archive_path.components().skip(1).collect::<PathBuf>());
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
}
entry.unpack(path)?;
} else if archive_path.starts_with(LIBRARIES_FOLDER) && offline {
let mut path = instance.libraries_path();
path.push(archive_path.components().skip(1).collect::<PathBuf>());
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
}
entry.unpack(path)?;
} else if archive_path.starts_with(ASSETS_FOLDER) && offline {
let mut path = instance.assets_path();
path.push(archive_path.components().skip(1).collect::<PathBuf>());
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
}
entry.unpack(path)?;
} else if archive_path.starts_with(LOG_CONFIGS_FOLDER) && offline {
let mut path = instance.log_configs_path();
path.push(archive_path.components().skip(1).collect::<PathBuf>());
if let Some(parent) = path.parent() {
create_dir_all(parent)?;
}
entry.unpack(path)?;
}
}
Ok(instance)
}
}
pub(crate) fn append_instance_information(
instance: &mut Instance,
offline: bool,
tar: &mut tar::Builder<GzEncoder<File>>,
) -> CobbleResult<()> {
trace!("Checking if instance is installed for offline export");
if offline && !instance.installed {
return Err(CobbleError::NotInstalled);
}
instance.installed = offline;
trace!("Writing instance JSON to disk");
let mut json_path = instance.instance_path();
json_path.push(INSTANCE_MARKER_FILE);
let json_file = File::create(&json_path)?;
serde_json::to_writer_pretty(json_file, &instance)?;
trace!("Adding instance JSON to archive");
let mut json_file = File::open(&json_path)?;
tar.append_file(INSTANCE_MARKER_FILE, &mut json_file)?;
trace!("Removing instance JSON from disk");
remove_file(json_path)?;
Ok(())
}
pub(crate) fn append_instance_folder(
instance: &Instance,
tar: &mut tar::Builder<GzEncoder<File>>,
) -> CobbleResult<()> {
trace!("Adding instance folder to archive");
tar.append_dir_all(INSTANCE_FOLDER, instance.instance_path())?;
Ok(())
}
pub(crate) fn append_libraries(
instance: &Instance,
version_data: &VersionData,
tar: &mut tar::Builder<GzEncoder<File>>,
) -> CobbleResult<()> {
trace!("Adding libraries to archive");
let libraries_path = instance.libraries_path();
version_data
.needed_libraries()
.into_iter()
.try_for_each(|library| -> CobbleResult<()> {
let file_path = library.jar_path(&libraries_path);
let mut file = File::open(file_path)?;
let mut relative_path = PathBuf::from(LIBRARIES_FOLDER);
relative_path.push(library.relative_jar_path());
trace!("Adding library {} to archive", &library.name);
tar.append_file(relative_path, &mut file)?;
Ok(())
})?;
Ok(())
}
pub(crate) fn append_assets(
instance: &Instance,
asset_index: &AssetIndex,
version_data: &VersionData,
tar: &mut tar::Builder<GzEncoder<File>>,
) -> CobbleResult<()> {
trace!("Adding assets to archive");
let assets_path = instance.assets_path();
asset_index
.objects
.values()
.try_for_each(|asset| -> CobbleResult<()> {
let file_path = asset.asset_path(&assets_path);
let mut file = File::open(file_path)?;
let mut relative_path = PathBuf::from(ASSETS_FOLDER);
relative_path.push(asset.relative_asset_path());
trace!("Adding asset {} to archive", &asset.hash);
tar.append_file(relative_path, &mut file)?;
Ok(())
})?;
trace!("Adding asset index to archive");
let mut asset_index_path = instance.asset_indexes_path();
asset_index_path.push(format!("{}.json", &version_data.assets));
let mut file = File::open(asset_index_path)?;
let mut relative_path = PathBuf::from(ASSETS_FOLDER);
relative_path.push(format!("indexes/{}.json", &version_data.assets));
tar.append_file(relative_path, &mut file)?;
Ok(())
}
pub(crate) fn append_log_config(
instance: &Instance,
version_data: &VersionData,
tar: &mut tar::Builder<GzEncoder<File>>,
) -> CobbleResult<()> {
if let Some(logging_info) = &version_data.logging {
let file_name = logging_info
.client
.file
.id
.as_ref()
.expect("Logging Info has no ID");
let mut file_path = instance.log_configs_path();
file_path.push(file_name);
let mut file = File::open(file_path)?;
let mut relative_path = PathBuf::from(LOG_CONFIGS_FOLDER);
relative_path.push(file_name);
trace!("Adding log_config to archive");
tar.append_file(relative_path, &mut file)?;
}
Ok(())
}
fn read_instance_information(src: impl AsRef<std::path::Path>) -> CobbleResult<Instance> {
trace!("Opening source archive file for reading instance JSON");
let archive_file = File::open(src)?;
let decoder = GzDecoder::new(archive_file);
let mut tar = tar::Archive::new(decoder);
trace!("Searching for instance JSON file");
let marker_path = PathBuf::from(INSTANCE_MARKER_FILE);
for entry_result in tar.entries()? {
let entry = entry_result?;
let path = PathBuf::from(entry.path()?);
if path != marker_path {
continue;
}
trace!("Parsing instance JSON");
let instance = serde_json::from_reader::<_, Instance>(entry)?;
return Ok(instance);
}
Err(CobbleError::MissingMarkerFile)
}