Rust入门实战 编写Minecraft启动器#4下载资源

100 阅读3分钟

首发于Enaium的个人博客


首先我们需要添加几个依赖。


model = { path = "../model" }

parse = { path = "../parse" }

reqwest = { version = "0.12", features = ["blocking", "json"] }

file-hashing = { version = "0.1" }

sha1 = { version = "0.10" }

reqwest用于发送请求,file-hashing用于计算文件的hashsha1用于计算sha1

之后我们需要添加下载的trait


pub trait Download {

    fn download(&self, game_dir: &Path) -> Result<(), Box<dyn std::error::Error>>;

}

接着我们需要使用Client::builder()来创建一个Client,因为默认的get方法会用有个超时时间,而我们需要设置超时时间为无限。


pub fn get<T: reqwest::IntoUrl>(url: T) -> reqwest::Result<reqwest::blocking::Response> {

    reqwest::blocking::Client::builder()

        .timeout(None)

        .build()?

        .get(url)

        .send()

}

最后我们需要创建一个计算文件hash的函数。


pub fn sha1<P: AsRef<Path>>(path: P) -> Result<String, std::io::Error> {

    let mut hasher = Sha1::new();

    file_hashing::get_hash_file(path, &mut hasher)

}

之后需要出创建asset.rslibrary.rsversion.rs文件,分别对应下载资源、下载库、下载游戏版本。

asset.rs


use std::{fs, path::Path};

  


use model::asset::*;

use parse::Parse;

  


use crate::{get, Download};

  


impl Download for AssetIndex {

    fn download(&self, game_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {

        println!("Downloading asset index:{}", self.id);

  


        let indexes_dir = &game_dir.join("assets").join("indexes");

  


        if !indexes_dir.exists() {

            std::fs::create_dir_all(indexes_dir)?;

        }

  


        let path = &indexes_dir.join(&format!("{}.json", self.id));

  


        std::fs::File::create(path)?;

  


        let url = &self.url;

        let text = &get(url)?.text()?;

  


        std::fs::write(path, text)?;

  


        let index = Index::parse(text)?;

  


        let objects_dir = &game_dir.join("assets").join("objects");

  


        if !objects_dir.exists() {

            std::fs::create_dir_all(objects_dir)?;

        }

  


        for (_, value) in index.objects {

            let hash = &value.hash;

            let hash_first_two = &hash.chars().take(2).collect::<String>();

  


            let first_two_dir = &objects_dir.join(hash_first_two);

  


            if !first_two_dir.exists() {

                std::fs::create_dir_all(first_two_dir)?;

            }

  


            let path = &first_two_dir.join(hash);

  


            if path.exists() {

                if crate::sha1(path)?.eq(hash) {

                    continue;

                } else {

                    std::fs::remove_file(path)?;

                }

            }

  


            std::fs::File::create(path)?;

  


            let url = format!(

                "https://resources.download.minecraft.net/{}/{}",

                hash_first_two, hash

            );

  


            println!("Downloading:{}", url);

  


            let bytes = get(&url)?.bytes()?;

            fs::write(path, bytes)?;

        }

        Ok(())

    }

}

  


#[cfg(test)]

mod tests {

  


    use super::*;

  


    #[test]

    fn test_asset_index() {

        let asset_index = model::asset::AssetIndex {

            id: "17".to_string(),

            sha1: "fab15439bdef669e389e25e815eee8f1b2aa915e".to_string(),

            size: 447033,

            total_size: 799252591,

            url: "https://piston-meta.mojang.com/v1/packages/fab15439bdef669e389e25e815eee8f1b2aa915e/17.json".to_string(),

        };

  


        let download_path = &std::env::temp_dir().join("rust-minecraft-client-launch");

        std::fs::create_dir_all(download_path).unwrap_or_else(|err| panic!("{:?}", err));

  


        if let Err(err) = asset_index.download(download_path) {

            panic!("{:?}", err);

        }

    }

}

library.rs


use std::path::Path;

  


use model::{library, version::Libraries};

  


use crate::{Download, LibraryAllowed};

  


impl LibraryAllowed for library::Library {

    fn allowed(&self) -> bool {

        let mut allowed = true;

  


        if self.rules.is_some() {

            for rule in self.rules.as_ref().unwrap() {

                if rule.os.name == "osx" && !cfg!(target_os = "macos") {

                    allowed = false;

                    break;

                } else if rule.os.name == "linux" && !cfg!(target_os = "linux") {

                    allowed = false;

                    break;

                } else if rule.os.name == "windows" && !cfg!(target_os = "windows") {

                    allowed = false;

                    break;

                }

            }

        }

  


        if self.name.contains("natives") {

            if self.name.contains("x86") && !cfg!(target_arch = "x86") {

                allowed = false;

            } else if self.name.contains("arm64") && !cfg!(target_arch = "aarch64") {

                allowed = false;

            } else if !cfg!(target_arch = "x86_64") {

                allowed = false;

            }

        }

  


        allowed

    }

}

  


impl Download for Libraries {

    fn download(&self, game_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {

        println!("Downloading libraries");

  


        let libraries_dir = &game_dir.join("libraries");

  


        if !libraries_dir.exists() {

            std::fs::create_dir_all(libraries_dir)?;

        }

  


        for library in self {

            if !library.allowed() {

                continue;

            }

  


            let library_file = &library.downloads.artifact.path;

  


            let library_path = &libraries_dir.join(library_file);

  


            if !library_path.parent().unwrap().exists() {

                std::fs::create_dir_all(library_path.parent().unwrap())?;

            }

  


            if library_path.exists() {

                if crate::sha1(library_path)? == library.downloads.artifact.sha1 {

                    continue;

                } else {

                    std::fs::remove_file(library_path)?;

                }

            }

  


            std::fs::File::create(&library_path)?;

  


            let url = &library.downloads.artifact.url;

  


            println!("Downloading: {}", url);

  


            let bytes = crate::get(url)?.bytes()?;

  


            std::fs::write(library_path, bytes)?;

        }

  


        Ok(())

    }

}

  


#[cfg(test)]

mod tests {

    use super::*;

    use model::version::Version;

  


    #[test]

    fn test_download() {

        let game = reqwest::blocking::get("https://piston-meta.mojang.com/v1/packages/177e49d3233cb6eac42f0495c0a48e719870c2ae/1.21.json")

            .unwrap()

            .json::<Version>()

            .unwrap();

  


        let download_path = &std::env::temp_dir().join("rust-minecraft-client-launch");

        std::fs::create_dir_all(download_path).unwrap_or_else(|err| panic!("{:?}", err));

  


        if let Err(err) = game.libraries.download(download_path) {

            panic!("{:?}", err);

        }

    }

}

这里我们需要添加一个trait,用于判断库是否允许下载。


pub trait LibraryAllowed {

    fn allowed(&self) -> bool;

}

version.rs


use std::path::Path;

  


use model::version_manifest::Version;

  


use crate::{get, sha1, Download};

  


impl Download for Version {

    fn download(&self, game_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {

        let game = get(&self.url)?.json::<model::version::Version>()?;

  


        let versions_dir = &game_dir.join(game_dir).join("versions").join(&self.id);

  


        if !versions_dir.exists() {

            std::fs::create_dir_all(versions_dir)?;

        }

  


        game.libraries.download(game_dir)?;

        game.asset_index.download(game_dir)?;

  


        let version_config = &game_dir

            .join("versions")

            .join(&self.id)

            .join(&format!("{}.json", &self.id));

  


        if version_config.exists() {

            std::fs::remove_file(version_config).unwrap();

        }

  


        std::fs::File::create(version_config).unwrap();

        std::fs::write(version_config, get(&self.url).unwrap().bytes().unwrap()).unwrap();

  


        let path = &versions_dir

            .join(versions_dir)

            .join(&format!("{}.jar", &self.id));

  


        if path.exists() {

            if sha1(path)? == game.downloads.client.sha1 {

                return Ok(());

            } else {

                std::fs::remove_file(path)?;

            }

        }

  


        std::fs::File::create(path)?;

  


        let bytes = crate::get(&game.downloads.client.url)?.bytes()?;

  


        std::fs::write(path, bytes)?;

  


        Ok(())

    }

}

  


#[cfg(test)]

mod tests {

    use super::*;

  


    #[test]

    fn test_download() {

        let version = Version {

            id: "1.21".to_string(),

            type_: "release".to_string(),

            url: "https://piston-meta.mojang.com/v1/packages/177e49d3233cb6eac42f0495c0a48e719870c2ae/1.21.json".to_string(),

            time : "2024-06-13T08:32:38+00:00".to_string(),

            release_time : "2024-06-13T08:24:03+00:00".to_string(),

        };

  


        let download_path = &std::env::temp_dir().join("rust-minecraft-client-launch");

        std::fs::create_dir_all(download_path).unwrap_or_else(|err| panic!("{:?}", err));

  


        if let Err(err) = version.download(download_path) {

            panic!("{:?}", err);

        }

    }

}

好了,现在我们可以测试下载资源了。

项目地址