1use crate::error::{CobbleError, CobbleResult};
2use crate::minecraft::models::{AssetIndex, VersionData};
3use crate::Instance;
4use flate2::read::GzDecoder;
5use flate2::write::GzEncoder;
6use std::fs::{create_dir_all, remove_file, File};
7use std::path::PathBuf;
8
9pub const INSTANCE_MARKER_FILE: &str = ".cobble_instance.json";
10pub const INSTANCE_FOLDER: &str = "instance";
11pub const LIBRARIES_FOLDER: &str = "libraries";
12pub const ASSETS_FOLDER: &str = "assets";
13pub const LOG_CONFIGS_FOLDER: &str = "assets/log_configs";
14
15impl Instance {
16 #[instrument(
22 name = "export_instance",
23 level = "trace",
24 skip_all,
25 fields(
26 instance_path = %self.instance_path.to_string_lossy(),
27 offline,
28 dest,
29 compression,
30 )
31 )]
32 #[cfg_attr(doc_cfg, doc(cfg(all(feature = "backup", not(feature = "fabric")))))]
33 #[cfg(not(feature = "fabric"))]
34 pub async fn export(
35 &self,
36 dest: impl AsRef<std::path::Path>,
37 offline: bool,
38 compression: u32,
39 ) -> CobbleResult<()> {
40 if !self.installed {
41 return Err(CobbleError::NotInstalled);
42 }
43
44 let this = self.clone();
45 let dest = PathBuf::from(dest.as_ref());
46
47 trace!("Getting version data");
48 let version_data = self.read_version_data().await?;
49 trace!("Getting asset index");
50 let asset_index = version_data.asset_index.fetch_index().await?;
51
52 tokio::task::spawn_blocking(move || {
53 this.export_blocking(version_data, asset_index, dest, offline, compression)
54 })
55 .await?
56 }
57
58 #[instrument(name = "import_instance", level = "trace", skip_all, fields())]
61 #[cfg_attr(doc_cfg, doc(cfg(feature = "backup")))]
62 pub async fn import(
63 src: impl AsRef<std::path::Path>,
64 instance_path: impl AsRef<std::path::Path>,
65 libraries_path: impl AsRef<std::path::Path>,
66 assets_path: impl AsRef<std::path::Path>,
67 offline: bool,
68 ) -> CobbleResult<Self> {
69 let src = PathBuf::from(src.as_ref());
70 let instance_path = PathBuf::from(instance_path.as_ref());
71 let libraries_path = PathBuf::from(libraries_path.as_ref());
72 let assets_path = PathBuf::from(assets_path.as_ref());
73
74 tokio::task::spawn_blocking(move || {
75 Self::import_blocking(src, instance_path, libraries_path, assets_path, offline)
76 })
77 .await?
78 }
79
80 #[instrument(
81 name = "export_instance_blocking",
82 level = "trace",
83 skip_all,
84 fields(
85 instance_path = %self.instance_path.to_string_lossy(),
86 offline,
87 dest,
88 compression,
89 )
90 )]
91 #[cfg(not(feature = "fabric"))]
92 fn export_blocking(
93 mut self,
94 version_data: VersionData,
95 asset_index: AssetIndex,
96 dest: PathBuf,
97 offline: bool,
98 compression: u32,
99 ) -> CobbleResult<()> {
100 trace!("Creating archive file");
101 let archive_file = File::create(dest)?;
102 let encoder = GzEncoder::new(archive_file, flate2::Compression::new(compression));
103 let mut tar = tar::Builder::new(encoder);
104
105 append_instance_information(&mut self, offline, &mut tar)?;
106 append_instance_folder(&self, &mut tar)?;
107
108 if offline {
109 append_libraries(&self, &version_data, &mut tar)?;
110 append_assets(&self, &asset_index, &version_data, &mut tar)?;
111 append_log_config(&self, &version_data, &mut tar)?;
112 }
113
114 trace!("Flushing archive");
115 let mut encoder = tar.into_inner()?;
116 encoder.try_finish()?;
117
118 Ok(())
119 }
120
121 #[instrument(name = "import_instance_blocking", level = "trace", skip_all, fields())]
123 fn import_blocking(
124 src: PathBuf,
125 instance_path: PathBuf,
126 libraries_path: PathBuf,
127 assets_path: PathBuf,
128 offline: bool,
129 ) -> CobbleResult<Self> {
130 let mut instance = read_instance_information(&src)?;
131
132 instance.instance_path = instance_path;
134 instance.libraries_path = libraries_path;
135 instance.assets_path = assets_path;
136 instance.installed = offline;
137
138 trace!("Opening source archive file");
139 let archive_file = File::open(src)?;
140 let decoder = GzDecoder::new(archive_file);
141 let mut tar = tar::Archive::new(decoder);
142
143 trace!("Extracting files");
144 for entry_result in tar.entries()? {
145 let mut entry = entry_result?;
146 let archive_path = entry.path()?;
147
148 if archive_path.starts_with(INSTANCE_FOLDER) {
149 let mut path = instance.instance_path();
151 path.push(archive_path.components().skip(1).collect::<PathBuf>());
152
153 if let Some(parent) = path.parent() {
154 create_dir_all(parent)?;
155 }
156
157 entry.unpack(path)?;
158 } else if archive_path.starts_with(LIBRARIES_FOLDER) && offline {
159 let mut path = instance.libraries_path();
161 path.push(archive_path.components().skip(1).collect::<PathBuf>());
162
163 if let Some(parent) = path.parent() {
164 create_dir_all(parent)?;
165 }
166
167 entry.unpack(path)?;
168 } else if archive_path.starts_with(ASSETS_FOLDER) && offline {
169 let mut path = instance.assets_path();
171 path.push(archive_path.components().skip(1).collect::<PathBuf>());
172
173 if let Some(parent) = path.parent() {
174 create_dir_all(parent)?;
175 }
176
177 entry.unpack(path)?;
178 } else if archive_path.starts_with(LOG_CONFIGS_FOLDER) && offline {
179 let mut path = instance.log_configs_path();
181 path.push(archive_path.components().skip(1).collect::<PathBuf>());
182
183 if let Some(parent) = path.parent() {
184 create_dir_all(parent)?;
185 }
186
187 entry.unpack(path)?;
188 }
189 }
190
191 Ok(instance)
192 }
193}
194
195pub(crate) fn append_instance_information(
196 instance: &mut Instance,
197 offline: bool,
198 tar: &mut tar::Builder<GzEncoder<File>>,
199) -> CobbleResult<()> {
200 trace!("Checking if instance is installed for offline export");
201 if offline && !instance.installed {
202 return Err(CobbleError::NotInstalled);
203 }
204 instance.installed = offline;
205
206 trace!("Writing instance JSON to disk");
207 let mut json_path = instance.instance_path();
208 json_path.push(INSTANCE_MARKER_FILE);
209 let json_file = File::create(&json_path)?;
210 serde_json::to_writer_pretty(json_file, &instance)?;
211
212 trace!("Adding instance JSON to archive");
213 let mut json_file = File::open(&json_path)?;
214 tar.append_file(INSTANCE_MARKER_FILE, &mut json_file)?;
215
216 trace!("Removing instance JSON from disk");
217 remove_file(json_path)?;
218
219 Ok(())
220}
221
222pub(crate) fn append_instance_folder(
223 instance: &Instance,
224 tar: &mut tar::Builder<GzEncoder<File>>,
225) -> CobbleResult<()> {
226 trace!("Adding instance folder to archive");
227 tar.append_dir_all(INSTANCE_FOLDER, instance.instance_path())?;
228
229 Ok(())
230}
231
232pub(crate) fn append_libraries(
233 instance: &Instance,
234 version_data: &VersionData,
235 tar: &mut tar::Builder<GzEncoder<File>>,
236) -> CobbleResult<()> {
237 trace!("Adding libraries to archive");
238 let libraries_path = instance.libraries_path();
239
240 version_data
241 .needed_libraries()
242 .into_iter()
243 .try_for_each(|library| -> CobbleResult<()> {
244 let file_path = library.jar_path(&libraries_path);
246 let mut file = File::open(file_path)?;
247
248 let mut relative_path = PathBuf::from(LIBRARIES_FOLDER);
250 relative_path.push(library.relative_jar_path());
251
252 trace!("Adding library {} to archive", &library.name);
253 tar.append_file(relative_path, &mut file)?;
254
255 Ok(())
256 })?;
257
258 Ok(())
259}
260
261pub(crate) fn append_assets(
262 instance: &Instance,
263 asset_index: &AssetIndex,
264 version_data: &VersionData,
265 tar: &mut tar::Builder<GzEncoder<File>>,
266) -> CobbleResult<()> {
267 trace!("Adding assets to archive");
268 let assets_path = instance.assets_path();
269
270 asset_index
271 .objects
272 .values()
273 .try_for_each(|asset| -> CobbleResult<()> {
274 let file_path = asset.asset_path(&assets_path);
276 let mut file = File::open(file_path)?;
277
278 let mut relative_path = PathBuf::from(ASSETS_FOLDER);
280 relative_path.push(asset.relative_asset_path());
281
282 trace!("Adding asset {} to archive", &asset.hash);
283 tar.append_file(relative_path, &mut file)?;
284
285 Ok(())
286 })?;
287
288 trace!("Adding asset index to archive");
289 let mut asset_index_path = instance.asset_indexes_path();
291 asset_index_path.push(format!("{}.json", &version_data.assets));
292 let mut file = File::open(asset_index_path)?;
293
294 let mut relative_path = PathBuf::from(ASSETS_FOLDER);
296 relative_path.push(format!("indexes/{}.json", &version_data.assets));
297
298 tar.append_file(relative_path, &mut file)?;
300
301 Ok(())
302}
303
304pub(crate) fn append_log_config(
305 instance: &Instance,
306 version_data: &VersionData,
307 tar: &mut tar::Builder<GzEncoder<File>>,
308) -> CobbleResult<()> {
309 if let Some(logging_info) = &version_data.logging {
310 let file_name = logging_info
312 .client
313 .file
314 .id
315 .as_ref()
316 .expect("Logging Info has no ID");
317
318 let mut file_path = instance.log_configs_path();
320 file_path.push(file_name);
321 let mut file = File::open(file_path)?;
322
323 let mut relative_path = PathBuf::from(LOG_CONFIGS_FOLDER);
325 relative_path.push(file_name);
326
327 trace!("Adding log_config to archive");
328 tar.append_file(relative_path, &mut file)?;
329 }
330
331 Ok(())
332}
333
334fn read_instance_information(src: impl AsRef<std::path::Path>) -> CobbleResult<Instance> {
335 trace!("Opening source archive file for reading instance JSON");
336 let archive_file = File::open(src)?;
337 let decoder = GzDecoder::new(archive_file);
338 let mut tar = tar::Archive::new(decoder);
339
340 trace!("Searching for instance JSON file");
341 let marker_path = PathBuf::from(INSTANCE_MARKER_FILE);
342
343 for entry_result in tar.entries()? {
344 let entry = entry_result?;
345 let path = PathBuf::from(entry.path()?);
346
347 if path != marker_path {
348 continue;
349 }
350
351 trace!("Parsing instance JSON");
353 let instance = serde_json::from_reader::<_, Instance>(entry)?;
354
355 return Ok(instance);
356 }
357
358 Err(CobbleError::MissingMarkerFile)
359}