freighter_client/
lib.rs

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        // copy json len to buffer
159        buf[0..4].copy_from_slice(&u32::try_from(serialized.len()).unwrap().to_le_bytes());
160
161        // copy json to buffer
162        buf[4..tarball_len_off].copy_from_slice(&serialized);
163
164        // copy tarball len to buffer
165        buf[tarball_len_off..tarball_off].copy_from_slice(&u32::try_from(tarball.len()).unwrap().to_le_bytes());
166
167        // copy tarball to buffer
168        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 search(&self, query: &str, per_page: Option<usize>) -> Result<SearchResults> {
218    //     todo!()
219    // }
220
221    // pub async fn yank(&self, name: &str, version: &Version) {
222    //     todo!()
223    // }
224    //
225    // pub async fn unyank(&self, name: &str, version: &Version) {
226    //     todo!()
227    // }
228    //
229    // pub async fn list_owners(&self, name: &str) {
230    //     todo!()
231    // }
232    //
233    // pub async fn add_owners(&self, name: &str, owners: &[&str]) {
234    //     todo!()
235    // }
236    //
237    // pub async fn remove_owners(&self, name: &str, owners: &[&str]) {
238    //     todo!()
239    // }
240
241    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)] // api
265    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}