crates_registry/
serve_frontend.rs

1use 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    // Channel name -> Platforms
34    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
42/// Extract available platforms by mirror history file.
43fn 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            // FIXME() - Stream the body to file without load the whole file in the memory.
136            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}