Skip to content

Commit 677bce6

Browse files
committed
directly return rustdoc rustdoc json instead of redirect
1 parent 7e58385 commit 677bce6

File tree

8 files changed

+306
-220
lines changed

8 files changed

+306
-220
lines changed

src/docbuilder/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,6 @@ pub use self::rustwide_builder::{
88
};
99

1010
#[cfg(test)]
11-
pub use self::rustwide_builder::RUSTDOC_JSON_COMPRESSION_ALGORITHMS;
11+
pub use self::rustwide_builder::{
12+
RUSTDOC_JSON_COMPRESSION_ALGORITHMS, read_format_version_from_rustdoc_json,
13+
};

src/docbuilder/rustwide_builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub const RUSTDOC_JSON_COMPRESSION_ALGORITHMS: &[CompressionAlgorithm] =
5454
&[CompressionAlgorithm::Zstd, CompressionAlgorithm::Gzip];
5555

5656
/// read the format version from a rustdoc JSON file.
57-
fn read_format_version_from_rustdoc_json(
57+
pub fn read_format_version_from_rustdoc_json(
5858
reader: impl std::io::Read,
5959
) -> Result<RustdocJsonFormatVersion> {
6060
let reader = BufReader::new(reader);

src/storage/mod.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -419,14 +419,22 @@ impl AsyncStorage {
419419
self.get_stream(path).await?.materialize(max_size).await
420420
}
421421

422-
/// get a decompressing stream to an object in storage
422+
/// get a raw stream to an object in storage
423+
///
424+
/// We don't decompress ourselves, S3 only decompresses with a correct
425+
/// `Content-Encoding` header set, which we don't.
423426
#[instrument]
424-
pub(crate) async fn get_stream(&self, path: &str) -> Result<StreamingBlob> {
425-
let blob = match &self.backend {
427+
pub(crate) async fn get_raw_stream(&self, path: &str) -> Result<StreamingBlob> {
428+
match &self.backend {
426429
StorageBackend::Database(db) => db.get_stream(path, None).await,
427430
StorageBackend::S3(s3) => s3.get_stream(path, None).await,
428-
}?;
429-
Ok(blob.decompress().await?)
431+
}
432+
}
433+
434+
/// get a decompressing stream to an object in storage.
435+
#[instrument]
436+
pub(crate) async fn get_stream(&self, path: &str) -> Result<StreamingBlob> {
437+
Ok(self.get_raw_stream(path).await?.decompress().await?)
430438
}
431439

432440
/// get, decompress and materialize part of an object from store

src/test/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ pub(crate) trait AxumRouterTestExt {
154154
path: &str,
155155
cache_policy: cache::CachePolicy,
156156
config: &Config,
157-
) -> Result<()>;
157+
) -> Result<AxumResponse>;
158158
async fn assert_success(&self, path: &str) -> Result<AxumResponse>;
159159
async fn get(&self, path: &str) -> Result<AxumResponse>;
160160
async fn post(&self, path: &str) -> Result<AxumResponse>;
@@ -269,7 +269,7 @@ impl AxumRouterTestExt for axum::Router {
269269
path: &str,
270270
cache_policy: cache::CachePolicy,
271271
config: &Config,
272-
) -> Result<()> {
272+
) -> Result<AxumResponse> {
273273
let response = self.get(path).await?;
274274
let status = response.status();
275275
assert!(
@@ -278,7 +278,7 @@ impl AxumRouterTestExt for axum::Router {
278278
response.redirect_target().unwrap_or_default()
279279
);
280280
response.assert_cache_control(cache_policy, config);
281-
Ok(())
281+
Ok(response)
282282
}
283283

284284
async fn get(&self, path: &str) -> Result<AxumResponse> {

src/web/extractors/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ mod path;
33
pub(crate) mod rustdoc;
44

55
pub(crate) use context::DbConnection;
6-
pub(crate) use path::{Path, PathFileExtension};
6+
pub(crate) use path::{Path, WantedCompression};

src/web/extractors/path.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
//! custom axum extractors for path parameters
2-
use crate::web::error::AxumNope;
2+
use crate::{
3+
storage::{CompressionAlgorithm, compression::compression_from_file_extension},
4+
web::error::AxumNope,
5+
};
36
use anyhow::anyhow;
47
use axum::{
58
RequestPartsExt,
69
extract::{FromRequestParts, OptionalFromRequestParts},
710
http::request::Parts,
811
};
12+
use derive_more::Deref;
913

1014
/// custom axum `Path` extractor that uses our own AxumNope::BadRequest
1115
/// as error response instead of a plain text "bad request"
@@ -97,6 +101,58 @@ where
97101
}
98102
}
99103

104+
/// get wanted compression from file extension in path.
105+
///
106+
/// TODO: we could also additionally read the accept-encoding header here. But especially
107+
/// in combination with priorities it's complex to parse correctly. So for now only
108+
/// file extensions in the URL.
109+
/// When using Accept-Encoding, we also have to return "Vary: Accept-Encoding" to ensure
110+
/// the cache behaves correctly.
111+
#[derive(Debug, Clone, Deref, Default, PartialEq)]
112+
pub(crate) struct WantedCompression(pub(crate) CompressionAlgorithm);
113+
114+
impl<S> FromRequestParts<S> for WantedCompression
115+
where
116+
S: Send + Sync,
117+
{
118+
type Rejection = AxumNope;
119+
120+
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
121+
parts
122+
.extract::<Option<WantedCompression>>()
123+
.await
124+
.expect("can never fail")
125+
.ok_or_else(|| AxumNope::BadRequest(anyhow!("compression extension not found in path")))
126+
}
127+
}
128+
129+
impl<S> OptionalFromRequestParts<S> for WantedCompression
130+
where
131+
S: Send + Sync,
132+
{
133+
type Rejection = AxumNope;
134+
135+
async fn from_request_parts(
136+
parts: &mut Parts,
137+
_state: &S,
138+
) -> Result<Option<Self>, Self::Rejection> {
139+
if let Some(ext) = parts
140+
.extract::<Option<PathFileExtension>>()
141+
.await
142+
.expect("can't fail")
143+
.map(|ext| ext.0)
144+
{
145+
Ok(Some(WantedCompression(
146+
compression_from_file_extension(&ext).ok_or_else(|| {
147+
AxumNope::BadRequest(anyhow!("unknown compression file extension: {}", ext))
148+
})?,
149+
)))
150+
} else {
151+
Ok(None)
152+
}
153+
}
154+
}
155+
100156
#[cfg(test)]
101157
mod tests {
102158
use super::*;

src/web/extractors/rustdoc.rs

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use crate::{
44
db::BuildId,
5+
storage::CompressionAlgorithm,
56
web::{
67
MatchedRelease, MetaData, ReqVersion, error::AxumNope, escaped_uri::EscapedURI,
78
extractors::Path, url_decode,
@@ -573,6 +574,30 @@ impl RustdocParams {
573574
EscapedURI::from_path(path)
574575
}
575576

577+
pub(crate) fn json_download_url(
578+
&self,
579+
wanted_compression: Option<CompressionAlgorithm>,
580+
format_version: Option<&str>,
581+
) -> EscapedURI {
582+
let mut path = format!("/crate/{}/{}", self.name, self.req_version);
583+
584+
if let Some(doc_target) = self.doc_target() {
585+
path.push_str(&format!("/{doc_target}"));
586+
}
587+
588+
if let Some(format_version) = format_version {
589+
path.push_str(&format!("/json/{format_version}"));
590+
} else {
591+
path.push_str("/json");
592+
}
593+
594+
if let Some(wanted_compression) = wanted_compression {
595+
path.push_str(&format!(".{}", wanted_compression.file_extension()));
596+
}
597+
598+
EscapedURI::from_path(path)
599+
}
600+
576601
pub(crate) fn features_url(&self) -> EscapedURI {
577602
EscapedURI::from_path(format!(
578603
"/crate/{}/{}/features",
@@ -863,7 +888,7 @@ mod tests {
863888
use super::*;
864889
use crate::{
865890
db::types::version::Version,
866-
test::{AxumResponseTestExt, AxumRouterTestExt},
891+
test::{AxumResponseTestExt, AxumRouterTestExt, V1},
867892
};
868893
use axum::{Router, routing::get};
869894
use test_case::test_case;
@@ -1719,4 +1744,66 @@ mod tests {
17191744
format!("/{KRATE}/0.14.0/{KRATE}/trait.Itertools.html")
17201745
)
17211746
}
1747+
1748+
#[test_case(None)]
1749+
#[test_case(Some(CompressionAlgorithm::Gzip))]
1750+
#[test_case(Some(CompressionAlgorithm::Zstd))]
1751+
fn test_plain_json_url(wanted_compression: Option<CompressionAlgorithm>) {
1752+
let mut params = RustdocParams::new(KRATE)
1753+
.with_page_kind(PageKind::Rustdoc)
1754+
.with_req_version(ReqVersion::Exact(V1));
1755+
1756+
assert_eq!(
1757+
params.json_download_url(wanted_compression, None),
1758+
format!(
1759+
"/crate/{KRATE}/{V1}/json{}",
1760+
wanted_compression
1761+
.map(|c| format!(".{}", c.file_extension()))
1762+
.unwrap_or_default()
1763+
)
1764+
);
1765+
1766+
params = params.with_doc_target("some-target");
1767+
1768+
assert_eq!(
1769+
params.json_download_url(wanted_compression, None),
1770+
format!(
1771+
"/crate/{KRATE}/{V1}/some-target/json{}",
1772+
wanted_compression
1773+
.map(|c| format!(".{}", c.file_extension()))
1774+
.unwrap_or_default()
1775+
)
1776+
);
1777+
}
1778+
1779+
#[test_case(None)]
1780+
#[test_case(Some(CompressionAlgorithm::Gzip))]
1781+
#[test_case(Some(CompressionAlgorithm::Zstd))]
1782+
fn test_plain_json_url_with_format(wanted_compression: Option<CompressionAlgorithm>) {
1783+
let mut params = RustdocParams::new(KRATE)
1784+
.with_page_kind(PageKind::Rustdoc)
1785+
.with_req_version(ReqVersion::Exact(V1));
1786+
1787+
assert_eq!(
1788+
params.json_download_url(wanted_compression, Some("42")),
1789+
format!(
1790+
"/crate/{KRATE}/{V1}/json/42{}",
1791+
wanted_compression
1792+
.map(|c| format!(".{}", c.file_extension()))
1793+
.unwrap_or_default()
1794+
)
1795+
);
1796+
1797+
params = params.with_doc_target("some-target");
1798+
1799+
assert_eq!(
1800+
params.json_download_url(wanted_compression, Some("42")),
1801+
format!(
1802+
"/crate/{KRATE}/{V1}/some-target/json/42{}",
1803+
wanted_compression
1804+
.map(|c| format!(".{}", c.file_extension()))
1805+
.unwrap_or_default()
1806+
)
1807+
);
1808+
}
17221809
}

0 commit comments

Comments
 (0)