Skip to content

Commit 8ede7dc

Browse files
SuficioACsyphar
authored andcommitted
Use RegistryApi in releases
1 parent 83e30f0 commit 8ede7dc

File tree

4 files changed

+118
-106
lines changed

4 files changed

+118
-106
lines changed

src/registry_api.rs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{error::Result, utils::retry_async};
2-
use anyhow::{anyhow, Context};
2+
use anyhow::{anyhow, bail, Context};
33
use chrono::{DateTime, Utc};
44
use reqwest::header::{HeaderValue, ACCEPT, USER_AGENT};
55
use semver::Version;
@@ -69,6 +69,26 @@ impl fmt::Display for OwnerKind {
6969
}
7070
}
7171

72+
#[derive(Deserialize, Debug)]
73+
74+
pub(crate) struct SearchCrate {
75+
pub(crate) name: String,
76+
}
77+
78+
#[derive(Deserialize, Debug)]
79+
80+
pub(crate) struct SearchMeta {
81+
pub(crate) next_page: Option<String>,
82+
pub(crate) prev_page: Option<String>,
83+
}
84+
85+
#[derive(Deserialize, Debug)]
86+
pub(crate) struct Search {
87+
pub(crate) crates: Vec<SearchCrate>,
88+
pub(crate) meta: SearchMeta,
89+
pub(crate) executed_query: Option<String>,
90+
}
91+
7292
impl RegistryApi {
7393
pub fn new(api_base: Url, max_retries: u32) -> Result<Self> {
7494
let headers = vec![
@@ -227,4 +247,71 @@ impl RegistryApi {
227247

228248
Ok(result)
229249
}
250+
251+
/// Fetch crates from the registry's API
252+
pub(crate) async fn get_crates(&self, query: Option<&str>) -> Result<Search> {
253+
#[derive(Deserialize, Debug)]
254+
struct SearchError {
255+
detail: String,
256+
}
257+
258+
#[derive(Deserialize, Debug)]
259+
struct SearchResponse {
260+
crates: Option<Vec<SearchCrate>>,
261+
meta: Option<SearchMeta>,
262+
errors: Option<Vec<SearchError>>,
263+
}
264+
265+
let url = {
266+
let mut url = self.api_base.clone();
267+
url.path_segments_mut()
268+
.map_err(|()| anyhow!("Invalid API url"))?
269+
.extend(&["api", "v1", "crates"]);
270+
url.set_query(query);
271+
url
272+
};
273+
274+
// Extract the query from the query args
275+
let executed_query = url.query_pairs().find_map(|(key, value)| {
276+
if key == "q" {
277+
Some(value.to_string())
278+
} else {
279+
None
280+
}
281+
});
282+
283+
let response: SearchResponse = retry_async(
284+
|| async {
285+
Ok(self
286+
.client
287+
.get(url.clone())
288+
.send()
289+
.await?
290+
.error_for_status()?)
291+
},
292+
self.max_retries,
293+
)
294+
.await?
295+
.json()
296+
.await?;
297+
298+
if let Some(errors) = response.errors {
299+
let messages: Vec<_> = errors.into_iter().map(|e| e.detail).collect();
300+
bail!("got error from crates.io: {}", messages.join("\n"));
301+
}
302+
303+
let Some(crates) = response.crates else {
304+
bail!("missing releases in crates.io response");
305+
};
306+
307+
let Some(meta) = response.meta else {
308+
bail!("missing metadata in crates.io response");
309+
};
310+
311+
Ok(Search {
312+
crates,
313+
meta,
314+
executed_query,
315+
})
316+
}
230317
}

src/utils/mod.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,6 @@ pub(crate) mod sized_buffer;
3131

3232
use std::{future::Future, thread, time::Duration};
3333

34-
pub(crate) const APP_USER_AGENT: &str = concat!(
35-
env!("CARGO_PKG_NAME"),
36-
" ",
37-
include_str!(concat!(env!("OUT_DIR"), "/git_version"))
38-
);
39-
4034
pub(crate) fn report_error(err: &anyhow::Error) {
4135
// Debug-format for anyhow errors includes context & backtrace
4236
if std::env::var("SENTRY_DSN").is_ok() {

src/web/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ async fn apply_middleware(
424424
.layer(Extension(context.service_metrics()?))
425425
.layer(Extension(context.instance_metrics()?))
426426
.layer(Extension(context.config()?))
427+
.layer(Extension(context.registry_api()?))
427428
.layer(Extension(async_storage))
428429
.layer(option_layer(template_data.map(Extension)))
429430
.layer(middleware::from_fn(csp::csp_middleware))

src/web/releases.rs

Lines changed: 29 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use crate::{
44
build_queue::{QueuedCrate, REBUILD_PRIORITY},
55
cdn, impl_axum_webpage,
6-
utils::{report_error, retry_async},
6+
utils::report_error,
77
web::{
88
axum_parse_uri_with_params, axum_redirect, encode_url_path,
99
error::{AxumNope, AxumResult},
@@ -12,9 +12,9 @@ use crate::{
1212
page::templates::{filters, RenderRegular, RenderSolid},
1313
ReqVersion,
1414
},
15-
AsyncBuildQueue, Config, InstanceMetrics,
15+
AsyncBuildQueue, Config, InstanceMetrics, RegistryApi,
1616
};
17-
use anyhow::{anyhow, bail, Context as _, Result};
17+
use anyhow::{anyhow, Context as _, Result};
1818
use axum::{
1919
extract::{Extension, Query},
2020
response::{IntoResponse, Response as AxumResponse},
@@ -23,14 +23,13 @@ use base64::{engine::general_purpose::STANDARD as b64, Engine};
2323
use chrono::{DateTime, Utc};
2424
use futures_util::stream::TryStreamExt;
2525
use itertools::Itertools;
26-
use once_cell::sync::Lazy;
2726
use rinja::Template;
2827
use serde::{Deserialize, Serialize};
2928
use sqlx::Row;
3029
use std::collections::{BTreeMap, HashMap, HashSet};
3130
use std::str;
3231
use std::sync::Arc;
33-
use tracing::{debug, warn};
32+
use tracing::warn;
3433
use url::form_urlencoded;
3534

3635
use super::cache::CachePolicy;
@@ -143,85 +142,14 @@ struct SearchResult {
143142
/// This delegates to the crates.io search API.
144143
async fn get_search_results(
145144
conn: &mut sqlx::PgConnection,
146-
config: &Config,
147-
query_params: &str,
145+
registry: &RegistryApi,
146+
query_params: Option<&str>,
148147
) -> Result<SearchResult, anyhow::Error> {
149-
#[derive(Deserialize)]
150-
struct CratesIoError {
151-
detail: String,
152-
}
153-
#[derive(Deserialize)]
154-
struct CratesIoSearchResult {
155-
crates: Option<Vec<CratesIoCrate>>,
156-
meta: Option<CratesIoMeta>,
157-
errors: Option<Vec<CratesIoError>>,
158-
}
159-
#[derive(Deserialize, Debug)]
160-
struct CratesIoCrate {
161-
name: String,
162-
}
163-
#[derive(Deserialize, Debug)]
164-
struct CratesIoMeta {
165-
next_page: Option<String>,
166-
prev_page: Option<String>,
167-
}
168-
169-
use crate::utils::APP_USER_AGENT;
170-
use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, USER_AGENT};
171-
use reqwest::Client as HttpClient;
172-
173-
static HTTP_CLIENT: Lazy<HttpClient> = Lazy::new(|| {
174-
let mut headers = HeaderMap::new();
175-
headers.insert(USER_AGENT, HeaderValue::from_static(APP_USER_AGENT));
176-
headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
177-
HttpClient::builder()
178-
.default_headers(headers)
179-
.build()
180-
.unwrap()
181-
});
182-
183-
let url = config
184-
.registry_api_host
185-
.join(&format!("api/v1/crates{query_params}"))?;
186-
debug!("fetching search results from {}", url);
187-
188-
// extract the query from the query args.
189-
// This is easier because the query might have been encoded in the bash64-encoded
190-
// paginate parameter.
191-
let executed_query = url.query_pairs().find_map(|(key, value)| {
192-
if key == "q" {
193-
Some(value.to_string())
194-
} else {
195-
None
196-
}
197-
});
198-
199-
let response: CratesIoSearchResult = retry_async(
200-
|| async {
201-
Ok(HTTP_CLIENT
202-
.get(url.clone())
203-
.send()
204-
.await?
205-
.error_for_status()?)
206-
},
207-
config.crates_io_api_call_retries,
208-
)
209-
.await?
210-
.json()
211-
.await?;
212-
213-
if let Some(errors) = response.errors {
214-
let messages: Vec<_> = errors.into_iter().map(|e| e.detail).collect();
215-
bail!("got error from crates.io: {}", messages.join("\n"));
216-
}
217-
218-
let Some(crates) = response.crates else {
219-
bail!("missing releases in crates.io response");
220-
};
221-
222-
let Some(meta) = response.meta else {
223-
bail!("missing metadata in crates.io response");
224-
};
148+
let crate::registry_api::Search {
149+
crates,
150+
meta,
151+
executed_query,
152+
} = registry.get_crates(query_params).await?;
225153

226154
let names = Arc::new(
227155
crates
@@ -574,6 +502,7 @@ impl_axum_webpage! {
574502
pub(crate) async fn search_handler(
575503
mut conn: DbConnection,
576504
Extension(config): Extension<Arc<Config>>,
505+
Extension(registry): Extension<Arc<RegistryApi>>,
577506
Extension(metrics): Extension<Arc<InstanceMetrics>>,
578507
Query(mut params): Query<HashMap<String, String>>,
579508
) -> AxumResult<AxumResponse> {
@@ -646,20 +575,21 @@ pub(crate) async fn search_handler(
646575
AxumNope::NoResults
647576
})?;
648577
let query_params = String::from_utf8_lossy(&decoded);
649-
650-
if !query_params.starts_with('?') {
651-
// sometimes we see plain bytes being passed to `paginate`.
652-
// In these cases we just return `NoResults` and don't call
653-
// the crates.io API.
654-
// The whole point of the `paginate` design is that we don't
655-
// know anything about the pagination args and crates.io can
656-
// change them as they wish, so we cannot do any more checks here.
657-
warn!(
658-
"didn't get query args in `paginate` arguments for search: \"{}\"",
659-
query_params
660-
);
661-
return Err(AxumNope::NoResults);
662-
}
578+
let query_params = match query_params.strip_prefix('?') {
579+
Some(query_params) => query_params,
580+
None => {
581+
// sometimes we see plain bytes being passed to `paginate`.
582+
// In these cases we just return `NoResults` and don't call
583+
// the crates.io API.
584+
// The whole point of the `paginate` design is that we don't
585+
// know anything about the pagination args and crates.io can
586+
// change them as they wish, so we cannot do any more checks here.
587+
warn!(
588+
"didn't get query args in `paginate` arguments for search: \"{query_params}\""
589+
);
590+
return Err(AxumNope::NoResults);
591+
}
592+
};
663593

664594
let mut p = form_urlencoded::parse(query_params.as_bytes());
665595
if let Some(v) = p.find_map(|(k, v)| {
@@ -672,15 +602,15 @@ pub(crate) async fn search_handler(
672602
sort_by = v;
673603
};
674604

675-
get_search_results(&mut conn, &config, &query_params).await?
605+
get_search_results(&mut conn, &registry, Some(query_params)).await?
676606
} else if !query.is_empty() {
677607
let query_params: String = form_urlencoded::Serializer::new(String::new())
678608
.append_pair("q", &query)
679609
.append_pair("sort", &sort_by)
680610
.append_pair("per_page", &RELEASES_IN_RELEASES.to_string())
681611
.finish();
682612

683-
get_search_results(&mut conn, &config, &format!("?{}", &query_params)).await?
613+
get_search_results(&mut conn, &registry, Some(&query_params)).await?
684614
} else {
685615
return Err(AxumNope::NoResults);
686616
};

0 commit comments

Comments
 (0)