Skip to content

Commit b775025

Browse files
committed
Use RegistryApi in releases
1 parent 4e3e136 commit b775025

File tree

4 files changed

+119
-107
lines changed

4 files changed

+119
-107
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+
pub(crate) detail: String,
256+
}
257+
258+
#[derive(Deserialize, Debug)]
259+
struct SearchResponse {
260+
pub(crate) crates: Option<Vec<SearchCrate>>,
261+
pub(crate) meta: Option<SearchMeta>,
262+
pub(crate) 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: 30 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use crate::{
44
build_queue::QueuedCrate,
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,24 +12,23 @@ 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},
2121
};
2222
use base64::{engine::general_purpose::STANDARD as b64, Engine};
2323
use chrono::{DateTime, Utc};
2424
use futures_util::stream::TryStreamExt;
25-
use once_cell::sync::Lazy;
2625
use rinja::Template;
27-
use serde::{Deserialize, Serialize};
26+
use serde::Serialize;
2827
use sqlx::Row;
2928
use std::collections::{BTreeMap, HashMap, HashSet};
3029
use std::str;
3130
use std::sync::Arc;
32-
use tracing::{debug, warn};
31+
use tracing::warn;
3332
use url::form_urlencoded;
3433

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

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

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

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

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

0 commit comments

Comments
 (0)