cobble-core 1.2.0

Library for managing, installing and launching Minecraft instances and more.
Documentation
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 {
    /// Exports the instance using a gzip compressed tar archive.
    /// The `compression` is a level is an integer from 0-9 where 0 means "no
    /// compression" and 9 means "take as long as you'd like".
    ///
    /// The `offline` parameter determines whether all needed assets are packaged as well.
    #[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?
    }

    /// Imports an instance from an archive file.
    // TODO: Tracing fields
    #[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(())
    }

    // TODO: Tracing fields
    #[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)?;

        // Set paths and installed state of instance
        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) {
                // Instance
                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 {
                // Library
                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 {
                // Asset
                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 {
                // Log Config
                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<()> {
            // File on disk
            let file_path = library.jar_path(&libraries_path);
            let mut file = File::open(file_path)?;

            // Path in archive
            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<()> {
            // File on disk
            let file_path = asset.asset_path(&assets_path);
            let mut file = File::open(file_path)?;

            // Path in archive
            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");
    // File on disk
    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)?;

    // Path in archive
    let mut relative_path = PathBuf::from(ASSETS_FOLDER);
    relative_path.push(format!("indexes/{}.json", &version_data.assets));

    // Append
    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 {
        // File name
        let file_name = logging_info
            .client
            .file
            .id
            .as_ref()
            .expect("Logging Info has no ID");

        // File on disk
        let mut file_path = instance.log_configs_path();
        file_path.push(file_name);
        let mut file = File::open(file_path)?;

        // Path in archive
        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;
        }

        // Parsing information
        trace!("Parsing instance JSON");
        let instance = serde_json::from_reader::<_, Instance>(entry)?;

        return Ok(instance);
    }

    Err(CobbleError::MissingMarkerFile)
}