cobble_core/instance/
backup.rs

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    /// Exports the instance using a gzip compressed tar archive.
17    /// The `compression` is a level is an integer from 0-9 where 0 means "no
18    /// compression" and 9 means "take as long as you'd like".
19    ///
20    /// The `offline` parameter determines whether all needed assets are packaged as well.
21    #[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    /// Imports an instance from an archive file.
59    // TODO: Tracing fields
60    #[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    // TODO: Tracing fields
122    #[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        // Set paths and installed state of instance
133        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                // Instance
150                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                // Library
160                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                // Asset
170                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                // Log Config
180                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            // File on disk
245            let file_path = library.jar_path(&libraries_path);
246            let mut file = File::open(file_path)?;
247
248            // Path in archive
249            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            // File on disk
275            let file_path = asset.asset_path(&assets_path);
276            let mut file = File::open(file_path)?;
277
278            // Path in archive
279            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    // File on disk
290    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    // Path in archive
295    let mut relative_path = PathBuf::from(ASSETS_FOLDER);
296    relative_path.push(format!("indexes/{}.json", &version_data.assets));
297
298    // Append
299    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        // File name
311        let file_name = logging_info
312            .client
313            .file
314            .id
315            .as_ref()
316            .expect("Logging Info has no ID");
317
318        // File on disk
319        let mut file_path = instance.log_configs_path();
320        file_path.push(file_name);
321        let mut file = File::open(file_path)?;
322
323        // Path in archive
324        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        // Parsing information
352        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}