cobble-core 1.2.0

Library for managing, installing and launching Minecraft instances and more.
Documentation
use crate::error::InstallationResult;
use crate::minecraft::install::InstallationUpdate;
use crate::minecraft::models::AssetInfo;
use crate::minecraft::InstallOptions;
use crate::utils::{download, download_progress_channel, Download, DownloadProgress};
use futures::future::try_join_all;
use futures::join;
use std::path::Path;
use tokio::fs::{self, create_dir_all, File};
use tokio::io::AsyncWriteExt;
use tokio::sync::mpsc::{Receiver, Sender};

/// Installs assets as defined in the asset index.
///
/// This function provides updates during installation.
#[instrument(
    name = "install_assets",
    level = "trace",
    skip_all,
    fields(
        version = &options.version_data.id,
        assets_path = %options.assets_path.display(),
        minecraft_path = %options.minecraft_path.display(),
        map_to_resources = &options.asset_index.map_to_resources,
        parallel_downloads = options.parallel_downloads,
        download_retries = options.download_retries,
        verify_downloads = options.verify_downloads,
    )
)]
pub async fn install_assets(
    options: &InstallOptions,
    update_sender: Sender<InstallationUpdate>,
) -> InstallationResult<()> {
    trace!("Building downloads for assets");
    let downloads = options
        .asset_index
        .objects
        .values()
        .map(|a| build_download(a, &options.assets_path))
        .collect::<Result<Vec<_>, _>>()?;

    trace!("Preparing futures for downloading and channel translation");
    let (tx, rx) = download_progress_channel(500);
    let download_future = download(
        downloads,
        Some(tx),
        options.parallel_downloads,
        options.download_retries,
        options.verify_downloads,
    );
    let map_future = map_progress(update_sender.clone(), rx);

    trace!("Starting downloads");
    join!(download_future, map_future).0?;

    if options.asset_index.map_to_resources {
        trace!("Preparing mapping to resources");
        let symlinks = options.asset_index.objects.iter().map(|(key, asset)| {
            create_symlink(
                key,
                asset,
                &options.assets_path,
                &options.minecraft_path,
                &update_sender,
            )
        });

        trace!("Starting creating symlinks");
        try_join_all(symlinks).await?;
    }

    trace!("Saving asset index to disk");
    let asset_index_json = serde_json::to_vec_pretty(&options.asset_index)?;

    let mut asset_index_path = options.assets_path.clone();
    asset_index_path.push("indexes");
    create_dir_all(&asset_index_path).await?;
    asset_index_path.push(format!("{}.json", &options.version_data.assets));

    let mut file = File::create(asset_index_path).await?;
    file.write_all(&asset_index_json).await?;
    file.sync_all().await?;

    Ok(())
}

#[instrument(
    name = "create_asset_symlink",
    level = "trace",
    skip_all,
    fields(
        asset = key,
        assets_path,
        minecraft_path,
    )
)]
async fn create_symlink(
    key: &str,
    asset: &AssetInfo,
    assets_path: impl AsRef<Path> + Send,
    minecraft_path: impl AsRef<Path> + Send,
    update_sender: &Sender<InstallationUpdate>,
) -> InstallationResult<()> {
    trace!("Sending progress");
    let name = asset
        .asset_path(&assets_path)
        .file_name()
        .map(|s| s.to_string_lossy().to_string())
        .unwrap_or_default();
    update_sender
        .send(InstallationUpdate::Asset((
            name,
            AssetInstallationUpdate::Symlink,
        )))
        .await
        .ok();

    let asset_path = asset.asset_path(assets_path);
    let resource_path = AssetInfo::resource_path(key, &minecraft_path);
    let parent_dir = resource_path.parent().unwrap();

    trace!("Creating parent directory for symlink");
    fs::create_dir_all(parent_dir).await?;

    trace!(
        "Creating symlink: {} => {}",
        resource_path.to_string_lossy(),
        asset_path.to_string_lossy()
    );

    #[cfg(unix)]
    fs::symlink(asset_path, resource_path).await?;

    #[cfg(windows)]
    fs::symlink_file(asset_path, resource_path).await?;

    Ok(())
}

async fn map_progress(
    sender: Sender<InstallationUpdate>,
    mut receiver: Receiver<DownloadProgress>,
) {
    while let Some(p) = receiver.recv().await {
        let name = p
            .file
            .file_name()
            .map(|s| s.to_string_lossy().to_string())
            .unwrap_or_default();

        let send_result = sender
            .send(InstallationUpdate::Asset((
                name,
                AssetInstallationUpdate::Downloading(p),
            )))
            .await;

        if send_result.is_err() {
            debug!("Failed to translate DownloadProgress to InstallationUpdate");
            break;
        }
    }
}

fn build_download(
    asset: &AssetInfo,
    assets_path: impl AsRef<Path>,
) -> InstallationResult<Download> {
    let sha1 = hex::decode(&asset.hash)?;

    Ok(Download {
        url: asset.download_url(),
        file: asset.asset_path(assets_path),
        sha1: Some(sha1),
    })
}

/// Update of asset installation
#[derive(Clone, Debug)]
pub enum AssetInstallationUpdate {
    /// Download status
    Downloading(DownloadProgress),
    /// Symlink
    Symlink,
}