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
12pub const API_BASE: &'static str = "https://api.papermc.io/v2";
14
15#[derive(Debug, Deserialize)]
23pub struct Root {
24 pub projects: Vec<String>,
26}
27
28impl Root {
29 pub const fn link() -> &'static str {
31 constcat::concat!(API_BASE, "/projects")
32 }
33
34 pub async fn new() -> Result<Self> {
36 Ok(reqwest::get(Root::link()).await?.json::<Self>().await?)
37 }
38
39 pub async fn get_project(&self, project: &str) -> Result<Project> {
41 Project::new(project).await
42 }
43}
44
45#[derive(Debug, Deserialize)]
47pub struct Project {
48 pub project_id: String,
50 pub project_name: String,
52 pub version_groups: Vec<String>,
54 pub versions: Vec<String>,
56}
57
58impl Project {
59 pub fn link(project: &str) -> String {
61 format!("{0}/{1}", Root::link(), project)
62 }
63
64 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 pub async fn get_version(&self, version: &str) -> Result<Version> {
74 Ok(Version::new(&self.project_id, version).await?)
75 }
76
77 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#[derive(Debug, Deserialize)]
88pub struct Version {
89 pub project_id: String,
91 pub project_name: String,
93 pub version: String,
95 pub builds: Vec<u16>,
97}
98impl Version {
99 pub fn link(project: &str, version: &str) -> String {
101 format!("{0}/versions/{1}", Project::link(project), version)
102 }
103
104 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 pub async fn get_build(&self, build: u16) -> Result<Build> {
112 Ok(Build::new(&self.project_id, &self.version, build).await?)
113 }
114
115 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#[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, pub channel: String, 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 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 pub fn download_digest_sha256(&self) -> &str {
156 &self.downloads.application.sha256
157 }
158
159 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 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
192pub mod wrapper {
194 use super::*;
195 #[allow(dead_code)]
197 #[derive(Debug, Deserialize)]
198 pub struct BuildChange {
199 commit: String, summary: String, message: String, }
203
204 #[derive(Debug, Deserialize)]
206 pub struct Application {
207 pub application: FileInfo,
208 }
209
210 #[derive(Debug, Deserialize)]
212 pub struct FileInfo {
213 pub name: String, pub sha256: String, }
216}
217
218pub 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}