fetch_paper_lib/
lib.rs

1use std::path::Path;
2
3use anyhow::{anyhow, Ok, Result};
4use constcat;
5use reqwest;
6use serde::Deserialize;
7
8use sha2::{Digest, Sha256};
9use tokio::{fs::File, io::AsyncWriteExt};
10use tokio_stream::StreamExt;
11
12/// Official api url base.
13pub const API_BASE: &'static str = "https://api.papermc.io/v2";
14
15/// Everything start from here.
16///
17/// A root is a json response from papermc's official ci.
18/// It contains different projects, sach as paper, velocity, etc.
19///
20/// You can easily read these project name(or project_id),
21/// then use [`Self::get_project`] to get further.
22#[derive(Debug, Deserialize)]
23pub struct Root {
24    /// a list contains all supported projects' name(or project_id)
25    pub projects: Vec<String>,
26}
27
28impl Root {
29    /// Get raw url link of ci root page
30    pub const fn link() -> &'static str {
31        constcat::concat!(API_BASE, "/projects")
32    }
33
34    /// Request the url then parse the response into [`Root`]
35    pub async fn new() -> Result<Self> {
36        Ok(reqwest::get(Root::link()).await?.json::<Self>().await?)
37    }
38
39    /// Return the given project's info.
40    pub async fn get_project(&self, project: &str) -> Result<Project> {
41        Project::new(project).await
42    }
43}
44
45/// A specific project info
46#[derive(Debug, Deserialize)]
47pub struct Project {
48    /// Project id that given by [`Root`], eg. "paper"
49    pub project_id: String,
50    /// Full name / Display name of this project, eg. "Paper"
51    pub project_name: String,
52    /// (No desc)
53    pub version_groups: Vec<String>,
54    /// All downloadable versions, eg. "1.16.5"
55    pub versions: Vec<String>,
56}
57
58impl Project {
59    /// Get raw url link of a project, giving a project_id
60    pub fn link(project: &str) -> String {
61        format!("{0}/{1}", Root::link(), project)
62    }
63
64    /// See [`Self::link`]
65    pub async fn new(project: &str) -> Result<Self> {
66        Ok(reqwest::get(Project::link(project))
67            .await?
68            .json::<Self>()
69            .await?)
70    }
71
72    /// Return the given version's info.
73    pub async fn get_version(&self, version: &str) -> Result<Version> {
74        Ok(Version::new(&self.project_id, version).await?)
75    }
76
77    /// Return the latest version's info.
78    ///
79    /// It is assumed that latest version is the last item in the list.
80    pub async fn get_latest_version(&self) -> Result<Version> {
81        self.get_version(self.versions.last().ok_or(anyhow!("no version found"))?)
82            .await
83    }
84}
85
86/// A specific verison info
87#[derive(Debug, Deserialize)]
88pub struct Version {
89    /// Project id that given by [`Root`], eg. "paper"
90    pub project_id: String,
91    /// Full name / Display name of this project, eg. "Paper"
92    pub project_name: String,
93    /// Version name, eg. "1.16.5"
94    pub version: String,
95    /// All downloadable builds, eg. 250
96    pub builds: Vec<u16>,
97}
98impl Version {
99    /// Get raw url link of a version, giving a project_id and a version number.
100    pub fn link(project: &str, version: &str) -> String {
101        format!("{0}/versions/{1}", Project::link(project), version)
102    }
103
104    /// See [`Self::link`]
105    pub async fn new(project: &str, version: &str) -> Result<Self> {
106        let link = Version::link(project, version);
107        Ok(reqwest::get(link).await?.json::<Self>().await?)
108    }
109
110    /// Return the given build's info.
111    pub async fn get_build(&self, build: u16) -> Result<Build> {
112        Ok(Build::new(&self.project_id, &self.version, build).await?)
113    }
114
115    /// Return the latest build's info.
116    pub async fn get_latest_build(&self) -> Result<Build> {
117        self.get_build(*self.builds.last().ok_or(anyhow!("no builds found"))?)
118            .await
119    }
120}
121
122/// Same as [`Version`]
123#[derive(Debug, Deserialize)]
124pub struct Build {
125    pub project_id: String,
126    pub project_name: String,
127    pub version: String,
128    pub build: u16,
129    pub time: String,    //"2016-02-29T01:43:34.279Z"
130    pub channel: String, //"default"
131    pub promoted: bool,
132    pub changes: Vec<wrapper::BuildChange>,
133    pub downloads: wrapper::Application,
134}
135impl Build {
136    pub fn link(project: &str, version: &str, build: u16) -> String {
137        format!("{0}/builds/{1}", Version::link(project, version), build)
138    }
139
140    pub async fn new(project: &str, version: &str, build: u16) -> Result<Self> {
141        let link = Build::link(project, version, build);
142        Ok(reqwest::get(link).await?.json::<Self>().await?)
143    }
144
145    /// Get the direct download link of this build.
146    pub fn download_link(&self) -> String {
147        format!(
148            "{0}/downloads/{1}",
149            Self::link(&self.project_id, &self.version, self.build),
150            self.downloads.application.name
151        )
152    }
153
154    /// Get the remote file sha256 digest.
155    pub fn download_digest_sha256(&self) -> &str {
156        &self.downloads.application.sha256
157    }
158
159    /// Download the file.
160    pub async fn download(&self, path: impl AsRef<Path>) -> Result<()> {
161        let mut file = File::create(path).await?;
162
163        let mut stream = reqwest::get(self.download_link()).await?.bytes_stream();
164
165        while let Some(chunk_result) = stream.next().await {
166            let chunk = chunk_result?;
167            file.write_all(&chunk).await?;
168        }
169
170        file.flush().await?;
171
172        Ok(())
173    }
174
175    /// Check file sum.
176    pub async fn checksum(&self, path: impl AsRef<Path>) -> Result<bool> {
177        fn sha256(path: impl AsRef<Path>) -> Result<String> {
178            let mut file = std::fs::File::open(path)?;
179            let mut hasher = Sha256::new();
180            let _n = std::io::copy(&mut file, &mut hasher)?;
181            let hash = hasher.finalize();
182
183            Ok(format!("{:x}", hash))
184        }
185        let owned_path = path.as_ref().to_path_buf();
186        let rtn = self.download_digest_sha256()
187            == tokio::task::spawn_blocking(|| sha256(owned_path)).await??;
188        Ok(rtn)
189    }
190}
191
192/// Structs that help json parse.
193pub mod wrapper {
194    use super::*;
195    /// (Json response wrapper component)
196    #[allow(dead_code)]
197    #[derive(Debug, Deserialize)]
198    pub struct BuildChange {
199        commit: String,  //"a7b53030d943c8205513e03c2bc888ba2568cf06",
200        summary: String, //"Add exception reporting events",
201        message: String, //"Add exception reporting events"
202    }
203
204    /// (Json response wrapper component)
205    #[derive(Debug, Deserialize)]
206    pub struct Application {
207        pub application: FileInfo,
208    }
209
210    /// (Json response wrapper component)
211    #[derive(Debug, Deserialize)]
212    pub struct FileInfo {
213        pub name: String,   //"paper-1.8.8-443.jar",
214        pub sha256: String, //"621649a139ea51a703528eac1eccac40a1c8635bc4d376c05e248043b23cb3c3"
215    }
216}
217
218/// Download the file.
219///
220/// `download("/tmp/target.jar", "paper", Some("1.16.5"), None, true).await?;`
221/// this will download papermc, version 1.16.5, with latest build (None means latest), and check download file's hash.
222pub async fn download(
223    path: impl AsRef<Path>,
224    project: &str,
225    version: Option<&str>,
226    build: Option<u16>,
227    checksum: bool,
228) -> Result<()> {
229    let root = Root::new().await?;
230    let project = root.get_project(project).await?;
231    let version = if let Some(version) = version {
232        project.get_version(version).await?
233    } else {
234        project.get_latest_version().await?
235    };
236    let build = if let Some(build) = build {
237        version.get_build(build).await?
238    } else {
239        version.get_latest_build().await?
240    };
241    build.download(&path).await?;
242    if checksum {
243        build.checksum(&path).await?;
244    }
245    Ok(())
246}
247
248#[tokio::test(flavor = "multi_thread", worker_threads = 10)]
249async fn test() -> Result<()> {
250    let root = Root::new().await?;
251    let projects = &root.projects;
252    for p in projects {
253        println!("{p}");
254    }
255
256    let paper = root.get_project("paper").await?;
257    for v in &paper.versions {
258        println!("{}\t{}", paper.project_id, v);
259    }
260
261    let latest_version = paper.get_latest_version().await?;
262    for b in &latest_version.builds {
263        println!(
264            "{}\t{}\t{}",
265            latest_version.project_id, latest_version.version, b
266        );
267    }
268
269    let latest_build = latest_version.get_latest_build().await?;
270    println!(
271        "{}\t{}\t{}\t{}",
272        latest_build.project_id,
273        latest_build.version,
274        latest_build.build,
275        latest_build.download_link()
276    );
277
278    let path: &'static str = "./target.jar";
279    latest_build.download(path).await?;
280    assert!(latest_build.checksum(path).await?);
281
282    Ok(())
283}