1use freighter_api_types::index::request::Publish;
2use freighter_api_types::index::response::{
3 CompletedPublication, CrateVersion, ListAll, RegistryConfig,
4};
5use reqwest::header::{HeaderValue, AUTHORIZATION};
6use reqwest::{Body, Request, StatusCode};
7use semver::Version;
8use thiserror::Error;
9
10const API_PATH: &str = "api/v1/crates";
11
12pub struct Client {
13 http: reqwest::Client,
14 endpoint: String,
15 token: Option<String>,
16 config: RegistryConfig,
17 auth_required: bool,
18}
19
20#[derive(Error, Debug)]
21pub enum Error {
22 #[error("Received error from freighter server: {0}")]
23 ServerError(#[source] anyhow::Error),
24 #[error("Conflict due to resource already being present")]
25 Conflict,
26 #[error("Permission denied to perform operation")]
27 Unauthorized,
28 #[error("Requested object was not found")]
29 NotFound,
30 #[error("Failed to deserialize stuff")]
31 Deserialization(#[from] serde_json::Error),
32 #[error("Received unknown error")]
33 Other(#[from] anyhow::Error),
34}
35
36impl From<reqwest::Error> for Error {
37 fn from(value: reqwest::Error) -> Self {
38 if let Some(status) = value.status() {
39 match status {
40 StatusCode::INTERNAL_SERVER_ERROR => Self::ServerError(anyhow::anyhow!(value)),
41 StatusCode::CONFLICT => Self::Conflict,
42 StatusCode::UNAUTHORIZED => Self::Unauthorized,
43 StatusCode::NOT_FOUND => Self::NotFound,
44 _ => Self::Other(anyhow::anyhow!(value)),
45 }
46 } else {
47 Self::Other(anyhow::anyhow!(value))
48 }
49 }
50}
51
52pub type Result<T> = std::result::Result<T, Error>;
53
54impl Client {
55 pub async fn new(endpoint: &str, token: Option<String>) -> Self {
56 let http = reqwest::Client::new();
57
58 Self::from_reqwest(endpoint, token, http).await
59 }
60
61 pub async fn from_reqwest(endpoint: &str, token: Option<String>, client: reqwest::Client) -> Self {
62 let endpoint = endpoint.to_string();
63 let config_url = format!("{endpoint}/config.json");
64
65 let mut auth_required = false;
66 let mut resp = client.get(&config_url).send().await.unwrap();
67
68 if resp.status() == StatusCode::UNAUTHORIZED {
69 let token = token.as_ref().expect("registry requires auth, no token given");
70 auth_required = true;
71 resp = client.get(&config_url)
72 .header(AUTHORIZATION, HeaderValue::from_str(token).unwrap())
73 .send().await.unwrap();
74 }
75 assert_eq!(resp.status(), StatusCode::OK);
76
77 let mut config: RegistryConfig = resp.json().await.unwrap();
78
79 if config.api.ends_with('/') {
80 config.api.pop();
81 }
82
83 if config.dl.ends_with('/') {
84 config.dl.pop();
85 }
86
87 Self {
88 http: client,
89 endpoint,
90 token,
91 config,
92 auth_required,
93 }
94 }
95
96 pub async fn fetch_index(&self, name: &str) -> Result<Vec<CrateVersion>> {
97 let prefix = match name.len() {
98 0 => panic!("Should not be asked for crate name of len 0"),
99 1 => "1".to_string(),
100 2 => "2".to_string(),
101 3 => format!("3/{}", name.split_at(1).0),
102 _ => {
103 let (prefix_1_tmp, rest) = name.split_at(2);
104 let (prefix_2_tmp, _) = rest.split_at(2);
105 format!("{prefix_1_tmp}/{prefix_2_tmp}")
106 }
107 };
108
109 let url = format!("{}/{prefix}/{name}", &self.endpoint);
110
111 let mut req = self.http.get(url).build().unwrap();
112
113 if self.auth_required {
114 self.attach_auth(&mut req);
115 }
116
117 let resp = self.http.execute(req).await?;
118
119 resp.error_for_status_ref()?;
120
121 let text = resp.text().await?;
122
123 let mut crates = Vec::new();
124
125 for l in text.lines() {
126 crates.push(serde_json::from_str(l)?);
127 }
128
129 Ok(crates)
130 }
131
132 pub async fn download_crate(&self, name: &str, version: &Version) -> Result<Vec<u8>> {
133 let url = format!("{}/{name}/{version}", self.config.dl);
134
135 let mut req = self.http.get(url).build().unwrap();
136
137 if self.auth_required {
138 self.attach_auth(&mut req);
139 }
140
141 let resp = self.http.execute(req).await?;
142
143 resp.error_for_status_ref()?;
144
145 let bytes = resp.bytes().await?;
146
147 Ok(bytes.to_vec())
148 }
149
150 pub async fn publish(&self, version: &Publish, tarball: &[u8]) -> Result<CompletedPublication> {
151 let serialized = serde_json::to_vec(version)?;
152
153 let tarball_len_off = 4 + serialized.len();
154 let tarball_off = 4 + tarball_len_off;
155
156 let mut buf = vec![0; tarball_off + tarball.len()];
157
158 buf[0..4].copy_from_slice(&u32::try_from(serialized.len()).unwrap().to_le_bytes());
160
161 buf[4..tarball_len_off].copy_from_slice(&serialized);
163
164 buf[tarball_len_off..tarball_off].copy_from_slice(&u32::try_from(tarball.len()).unwrap().to_le_bytes());
166
167 buf[tarball_off..].copy_from_slice(tarball);
169
170 let url = format!("{}/{API_PATH}/new", &self.config.api);
171
172 let mut req = self.http.put(url).build().unwrap();
173
174 *req.body_mut() = Some(Body::from(buf));
175
176 self.attach_auth(&mut req);
177
178 let resp = self.http.execute(req).await?;
179
180 resp.error_for_status_ref()?;
181
182 let json = resp.json().await?;
183
184 Ok(json)
185 }
186
187 pub async fn list(&self, per_page: Option<usize>, page: Option<usize>) -> Result<ListAll> {
188 let url = format!("{}/all", self.config.api);
189
190 let mut req = self.http.get(url).build().unwrap();
191
192 if self.auth_required {
193 self.attach_auth(&mut req);
194 }
195
196 {
197 let mut query_pairs = req.url_mut().query_pairs_mut();
198
199 if let Some(inner) = per_page {
200 query_pairs.append_pair("per_page", &inner.to_string());
201 }
202
203 if let Some(inner) = page {
204 query_pairs.append_pair("page", &inner.to_string());
205 }
206 }
207
208 let resp = self.http.execute(req).await?;
209
210 resp.error_for_status_ref()?;
211
212 let json = resp.json().await?;
213
214 Ok(json)
215 }
216
217 pub async fn register(&mut self, username: &str) -> Result<()> {
242 let url = format!("{}/{API_PATH}/account", self.config.api);
243
244 let mut req = self
245 .http
246 .post(url)
247 .form(&[("username", username)])
248 .build()
249 .unwrap();
250
251 self.attach_auth(&mut req);
252
253 let resp = self.http.execute(req).await?;
254
255 resp.error_for_status_ref()?;
256
257 let text = resp.text().await?;
258
259 self.token = Some(text);
260
261 Ok(())
262 }
263
264 #[allow(clippy::unused_async)] pub async fn set_token(&mut self, token: String) {
266 self.token = Some(token);
267 }
268
269 #[must_use]
270 pub fn token(&self) -> Option<&str> {
271 self.token.as_deref()
272 }
273
274 fn attach_auth(&self, req: &mut Request) {
275 if let Some(token) = self.token.as_ref() {
276 req.headers_mut()
277 .append(AUTHORIZATION, HeaderValue::from_str(token).unwrap());
278 }
279 }
280}