cobble_core/minecraft/install/
assets.rs

1use crate::error::InstallationResult;
2use crate::minecraft::install::InstallationUpdate;
3use crate::minecraft::models::AssetInfo;
4use crate::minecraft::InstallOptions;
5use crate::utils::{download, download_progress_channel, Download, DownloadProgress};
6use futures::future::try_join_all;
7use futures::join;
8use std::path::Path;
9use tokio::fs::{self, create_dir_all, File};
10use tokio::io::AsyncWriteExt;
11use tokio::sync::mpsc::{Receiver, Sender};
12
13/// Installs assets as defined in the asset index.
14///
15/// This function provides updates during installation.
16#[instrument(
17    name = "install_assets",
18    level = "trace",
19    skip_all,
20    fields(
21        version = &options.version_data.id,
22        assets_path = %options.assets_path.display(),
23        minecraft_path = %options.minecraft_path.display(),
24        map_to_resources = &options.asset_index.map_to_resources,
25        parallel_downloads = options.parallel_downloads,
26        download_retries = options.download_retries,
27        verify_downloads = options.verify_downloads,
28    )
29)]
30pub async fn install_assets(
31    options: &InstallOptions,
32    update_sender: Sender<InstallationUpdate>,
33) -> InstallationResult<()> {
34    trace!("Building downloads for assets");
35    let downloads = options
36        .asset_index
37        .objects
38        .values()
39        .map(|a| build_download(a, &options.assets_path))
40        .collect::<Result<Vec<_>, _>>()?;
41
42    trace!("Preparing futures for downloading and channel translation");
43    let (tx, rx) = download_progress_channel(500);
44    let download_future = download(
45        downloads,
46        Some(tx),
47        options.parallel_downloads,
48        options.download_retries,
49        options.verify_downloads,
50    );
51    let map_future = map_progress(update_sender.clone(), rx);
52
53    trace!("Starting downloads");
54    join!(download_future, map_future).0?;
55
56    if options.asset_index.map_to_resources {
57        trace!("Preparing mapping to resources");
58        let symlinks = options.asset_index.objects.iter().map(|(key, asset)| {
59            create_symlink(
60                key,
61                asset,
62                &options.assets_path,
63                &options.minecraft_path,
64                &update_sender,
65            )
66        });
67
68        trace!("Starting creating symlinks");
69        try_join_all(symlinks).await?;
70    }
71
72    trace!("Saving asset index to disk");
73    let asset_index_json = serde_json::to_vec_pretty(&options.asset_index)?;
74
75    let mut asset_index_path = options.assets_path.clone();
76    asset_index_path.push("indexes");
77    create_dir_all(&asset_index_path).await?;
78    asset_index_path.push(format!("{}.json", &options.version_data.assets));
79
80    let mut file = File::create(asset_index_path).await?;
81    file.write_all(&asset_index_json).await?;
82    file.sync_all().await?;
83
84    Ok(())
85}
86
87#[instrument(
88    name = "create_asset_symlink",
89    level = "trace",
90    skip_all,
91    fields(
92        asset = key,
93        assets_path,
94        minecraft_path,
95    )
96)]
97async fn create_symlink(
98    key: &str,
99    asset: &AssetInfo,
100    assets_path: impl AsRef<Path> + Send,
101    minecraft_path: impl AsRef<Path> + Send,
102    update_sender: &Sender<InstallationUpdate>,
103) -> InstallationResult<()> {
104    trace!("Sending progress");
105    let name = asset
106        .asset_path(&assets_path)
107        .file_name()
108        .map(|s| s.to_string_lossy().to_string())
109        .unwrap_or_default();
110    update_sender
111        .send(InstallationUpdate::Asset((
112            name,
113            AssetInstallationUpdate::Symlink,
114        )))
115        .await
116        .ok();
117
118    let asset_path = asset.asset_path(assets_path);
119    let resource_path = AssetInfo::resource_path(key, &minecraft_path);
120    let parent_dir = resource_path.parent().unwrap();
121
122    trace!("Creating parent directory for symlink");
123    fs::create_dir_all(parent_dir).await?;
124
125    trace!(
126        "Creating symlink: {} => {}",
127        resource_path.to_string_lossy(),
128        asset_path.to_string_lossy()
129    );
130
131    #[cfg(unix)]
132    fs::symlink(asset_path, resource_path).await?;
133
134    #[cfg(windows)]
135    fs::symlink_file(asset_path, resource_path).await?;
136
137    Ok(())
138}
139
140async fn map_progress(
141    sender: Sender<InstallationUpdate>,
142    mut receiver: Receiver<DownloadProgress>,
143) {
144    while let Some(p) = receiver.recv().await {
145        let name = p
146            .file
147            .file_name()
148            .map(|s| s.to_string_lossy().to_string())
149            .unwrap_or_default();
150
151        let send_result = sender
152            .send(InstallationUpdate::Asset((
153                name,
154                AssetInstallationUpdate::Downloading(p),
155            )))
156            .await;
157
158        if send_result.is_err() {
159            debug!("Failed to translate DownloadProgress to InstallationUpdate");
160            break;
161        }
162    }
163}
164
165fn build_download(
166    asset: &AssetInfo,
167    assets_path: impl AsRef<Path>,
168) -> InstallationResult<Download> {
169    let sha1 = hex::decode(&asset.hash)?;
170
171    Ok(Download {
172        url: asset.download_url(),
173        file: asset.asset_path(assets_path),
174        sha1: Some(sha1),
175    })
176}
177
178/// Update of asset installation
179#[derive(Clone, Debug)]
180pub enum AssetInstallationUpdate {
181    /// Download status
182    Downloading(DownloadProgress),
183    /// Symlink
184    Symlink,
185}