cobble_core/minecraft/install/
assets.rs1use 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#[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#[derive(Clone, Debug)]
180pub enum AssetInstallationUpdate {
181 Downloading(DownloadProgress),
183 Symlink,
185}