crates_registry/
serve_frontend.rs1use anyhow::{anyhow, Result};
2use bytes::Bytes;
3use glob::glob;
4use include_dir::{include_dir, Dir};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use tempfile::NamedTempFile;
9use toml::Table;
10use tracing::error;
11use warp::hyper::Body;
12use warp::path::Tail;
13use warp::reply::Response;
14use warp::Filter;
15
16use crate::serve::ServerError;
17use crate::unpack;
18
19static FRONTEND: Dir<'_> = include_dir!("$OUT_DIR/frontend_dist_folder/");
20
21fn available_platforms(root: &Path) -> Result<Vec<String>> {
22 Ok(std::fs::read_dir(root.join("rustup").join("dist"))?
23 .map(|entry| {
24 let platform_folder = entry?;
25 Ok(platform_folder.file_name().to_str().unwrap().to_owned())
26 })
27 .collect::<Result<Vec<_>>>()?)
28}
29
30#[derive(Serialize, Deserialize)]
31#[serde(rename_all(serialize = "camelCase", deserialize = "snake_case"))]
32struct Versions {
33 versions: HashMap<String, Vec<String>>,
35}
36
37fn load_config(path: &Path) -> Result<Table> {
38 let content = std::fs::read_to_string(path)?;
39 Ok(content.parse::<Table>()?)
40}
41
42fn extract_available_platforms_for_channel(
44 config: &Table,
45 version_name: &str,
46) -> Option<Vec<String>> {
47 Some(
48 config
49 .get("versions")?
50 .as_table()?
51 .values()
52 .flat_map(|v| v.as_array().map(Clone::clone).unwrap_or_default())
53 .filter_map(|p| {
54 let p = p.as_str()?;
55 let prefix = format!("cargo-{}-", version_name);
56 let start_index = p.find(&prefix)? + prefix.len();
57 Some(p[start_index..].strip_suffix("tar.xz")?.to_owned())
58 })
59 .collect::<Vec<String>>(),
60 )
61}
62
63fn available_versions(root: &Path) -> Result<Versions> {
64 let versions = glob(root.join("*.toml").to_str().unwrap())?
65 .map(|conf_path| -> Result<_> {
66 let conf_path: PathBuf = conf_path?;
67 let conf_file = load_config(&conf_path)?;
68 let file_name = conf_path.file_name().unwrap().to_str().unwrap();
69 let is_nightly = file_name.contains("nightly");
70 let version_name = if is_nightly {
71 "nightly"
72 } else {
73 file_name
74 .strip_prefix("mirror-")
75 .ok_or(anyhow!("strip_prefix NoneError"))?
76 .strip_suffix("-history.toml")
77 .ok_or(anyhow!("strip_suffix NoneError"))?
78 };
79 let platforms: Vec<String> =
80 extract_available_platforms_for_channel(&conf_file, &version_name)
81 .ok_or(anyhow!("None Error channel config"))?;
82 let version_name = if is_nightly {
83 let date = file_name
84 .strip_prefix("mirror-nightly-")
85 .ok_or(anyhow!("strip_prefix NoneError"))?
86 .strip_suffix("-history.toml")
87 .ok_or(anyhow!("strip_suffix NoneError"))?;
88 format!("{}-{}", version_name, date)
89 } else {
90 version_name.to_owned()
91 };
92
93 Ok((version_name, platforms))
94 })
95 .collect::<Result<HashMap<String, Vec<String>>>>()?;
96 Ok(Versions { versions })
97}
98
99fn frontend_api(
100 root: &Path,
101) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
102 let path_for_platforms = root.to_path_buf();
103 let available_platforms = warp::get()
104 .and(warp::path("api"))
105 .and(warp::path("available-platforms"))
106 .and_then(move || {
107 let path_for_api = path_for_platforms.clone();
108 async move {
109 let res = available_platforms(&path_for_api)
110 .map_err(|e| warp::reject::custom(ServerError(e)))
111 .map(|platforms| warp::reply::json(&platforms));
112 res
113 }
114 });
115
116 let path_for_versions = root.to_path_buf();
117 let versions_for_channel = warp::get()
118 .and(warp::path("api"))
119 .and(warp::path("versions"))
120 .and_then(move || {
121 let path_for_version = path_for_versions.clone();
122 async move {
123 available_versions(&path_for_version)
124 .map_err(|e| warp::reject::custom(ServerError(e)))
125 .map(|versions| warp::reply::json(&versions))
126 }
127 });
128 let path_for_loading = root.to_path_buf();
129 let load_pack_file = warp::put()
130 .and(warp::path("api"))
131 .and(warp::path("load-pack-file"))
132 .and(warp::body::bytes())
133 .and(warp::header::optional::<String>("Content-Type"))
134 .and_then(move |data: Bytes, content_type: Option<String>| {
135 let path_for_loading = path_for_loading.clone();
137 async move {
138 if !matches!(content_type, Some(file_type) if file_type == "application/x-tar") {
139 error!("Invalid content type. support only tar files (application/x-tar)");
140 return Err(warp::reject::custom(ServerError(anyhow!(
141 "Invalid content type. support only tar files (application/x-tar)"
142 ))));
143 }
144
145 let tmp = NamedTempFile::new()
146 .map_err(|e| warp::reject::custom(ServerError(anyhow!(e))))?;
147 tokio::fs::write(tmp.path(), data).await.map_err(|e| {
148 error!("error writing file: {}", e);
149 warp::reject::reject()
150 })?;
151 unpack(tmp.path(), &path_for_loading)
152 .await
153 .map_err(|e| warp::reject::custom(ServerError(anyhow!(e))))?;
154 Ok(warp::reply())
155 }
156 });
157
158 available_platforms
159 .or(versions_for_channel)
160 .or(load_pack_file)
161}
162
163pub fn serve_frontend(
164 root: &Path,
165) -> impl warp::Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone {
166 let home_page = warp::get().and(warp::path::end()).and_then(|| async {
167 FRONTEND
168 .get_file("index.html")
169 .ok_or_else(warp::reject::not_found)
170 .map(|f| warp::reply::html(f.contents()))
171 });
172
173 let static_files = warp::get()
174 .and(warp::path::tail())
175 .and_then(|path: Tail| async move {
176 FRONTEND
177 .get_file(path.as_str())
178 .ok_or_else(warp::reject::not_found)
179 .map(|f| Response::new(Body::from(f.contents())))
180 });
181
182 let api = frontend_api(&root);
183 home_page.or(api).or(static_files)
184}