Skip to content

Commit a5e7032

Browse files
committed
Fetch readme from source archive if available for crate details page
1 parent f1a7e46 commit a5e7032

File tree

3 files changed

+121
-8
lines changed

3 files changed

+121
-8
lines changed

src/storage/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::web::metrics::RenderingTimesRecorder;
1313
use crate::{db::Pool, Config, InstanceMetrics};
1414
use anyhow::{anyhow, ensure};
1515
use chrono::{DateTime, Utc};
16+
use fn_error_context::context;
1617
use path_slash::PathExt;
1718
use std::io::BufReader;
1819
use std::num::NonZeroU64;
@@ -199,6 +200,7 @@ impl Storage {
199200
})
200201
}
201202

203+
#[context("fetching {path} from {name} {version} (archive: {archive_storage})")]
202204
pub(crate) fn fetch_source_file(
203205
&self,
204206
name: &str,

src/test/fakes.rs

+23
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ impl<'a> FakeRelease<'a> {
214214
self.source_file("README.md", content.as_bytes())
215215
}
216216

217+
/// NOTE: this should be markdown. It will be rendered as HTML when served.
218+
pub(crate) fn readme_only_database(mut self, content: &'a str) -> Self {
219+
self.readme = Some(content);
220+
self
221+
}
222+
217223
pub(crate) fn add_owner(mut self, owner: CrateOwner) -> Self {
218224
self.registry_crate_data.owners.push(owner);
219225
self
@@ -347,6 +353,23 @@ impl<'a> FakeRelease<'a> {
347353
debug!("before upload source");
348354
let source_tmp = create_temp_dir();
349355
store_files_into(&self.source_files, source_tmp.path())?;
356+
357+
if !self
358+
.source_files
359+
.iter()
360+
.any(|&(path, _)| path == "Cargo.toml")
361+
{
362+
let MetadataPackage { name, version, .. } = &package;
363+
let content = format!(
364+
r#"
365+
[package]
366+
name = "{name}"
367+
version = "{version}"
368+
"#
369+
);
370+
store_files_into(&[("Cargo.toml", content.as_bytes())], source_tmp.path())?;
371+
}
372+
350373
let (source_meta, algs) = upload_files(FileKind::Sources, source_tmp.path())?;
351374
debug!("added source files {}", source_meta);
352375

src/web/crate_details.rs

+96-8
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ use crate::{
99
encode_url_path,
1010
error::{AxumNope, AxumResult},
1111
},
12+
Storage,
1213
};
13-
use anyhow::anyhow;
14+
use anyhow::{bail, Context, Result};
1415
use axum::{
1516
extract::{Extension, Path},
1617
response::{IntoResponse, Response as AxumResponse},
@@ -91,6 +92,34 @@ pub struct Release {
9192
pub target_name: String,
9293
}
9394

95+
#[fn_error_context::context("fetching readme for {name} {version}")]
96+
fn fetch_readme_from_source(
97+
storage: &Storage,
98+
name: &str,
99+
version: &str,
100+
archive_storage: bool,
101+
) -> anyhow::Result<String> {
102+
let manifest = storage.fetch_source_file(name, version, "Cargo.toml", archive_storage)?;
103+
let manifest = String::from_utf8(manifest.content)
104+
.context("parsing Cargo.toml")?
105+
.parse::<toml::Value>()
106+
.context("parsing Cargo.toml")?;
107+
let paths = match manifest.get("package").and_then(|p| p.get("readme")) {
108+
Some(toml::Value::Boolean(true)) => vec!["README.md"],
109+
Some(toml::Value::Boolean(false)) => vec![],
110+
Some(toml::Value::String(path)) => vec![path.as_ref()],
111+
_ => vec!["README.md", "README.txt", "README"],
112+
};
113+
for path in &paths {
114+
if let Ok(readme) = storage.fetch_source_file(name, version, path, archive_storage) {
115+
let readme = String::from_utf8(readme.content)
116+
.with_context(|| format!("parsing {path} content"))?;
117+
return Ok(readme);
118+
}
119+
}
120+
bail!("couldn't find readme in stored source, checked {paths:?}")
121+
}
122+
94123
impl CrateDetails {
95124
pub fn new(
96125
conn: &mut impl GenericClient,
@@ -237,6 +266,13 @@ impl CrateDetails {
237266
Ok(Some(crate_details))
238267
}
239268

269+
fn enrich_readme(&mut self, storage: &Storage) -> Result<()> {
270+
let readme =
271+
fetch_readme_from_source(storage, &self.name, &self.version, self.archive_storage)?;
272+
self.readme = Some(readme);
273+
Ok(())
274+
}
275+
240276
/// Returns the latest non-yanked, non-prerelease release of this crate (or latest
241277
/// yanked/prereleased if that is all that exist).
242278
pub fn latest_release(&self) -> &Release {
@@ -270,7 +306,9 @@ pub(crate) fn releases_for_crate(
270306
.into_iter()
271307
.filter_map(|row| {
272308
let version: String = row.get("version");
273-
match semver::Version::parse(&version) {
309+
match semver::Version::parse(&version).with_context(|| {
310+
format!("invalid semver in database for crate {crate_id}: {version}")
311+
}) {
274312
Ok(semversion) => Some(Release {
275313
id: row.get("id"),
276314
version: semversion,
@@ -281,9 +319,7 @@ pub(crate) fn releases_for_crate(
281319
target_name: row.get("target_name"),
282320
}),
283321
Err(err) => {
284-
report_error(&anyhow!(err).context(format!(
285-
"invalid semver in database for crate {crate_id}: {version}"
286-
)));
322+
report_error(&err);
287323
None
288324
}
289325
}
@@ -310,9 +346,10 @@ pub(crate) struct CrateDetailHandlerParams {
310346
version: Option<String>,
311347
}
312348

313-
#[tracing::instrument]
349+
#[tracing::instrument(skip(pool, storage))]
314350
pub(crate) async fn crate_details_handler(
315351
Path(params): Path<CrateDetailHandlerParams>,
352+
Extension(storage): Extension<Arc<Storage>>,
316353
Extension(pool): Extension<Pool>,
317354
Extension(repository_stats_updater): Extension<Arc<RepositoryStatsUpdater>>,
318355
) -> AxumResult<AxumResponse> {
@@ -352,13 +389,19 @@ pub(crate) async fn crate_details_handler(
352389

353390
let details = spawn_blocking(move || {
354391
let mut conn = pool.get()?;
355-
CrateDetails::new(
392+
let mut details = CrateDetails::new(
356393
&mut *conn,
357394
&params.name,
358395
&version,
359396
&version_or_latest,
360397
Some(&repository_stats_updater),
361-
)
398+
)?;
399+
if let Some(ref mut details) = details {
400+
if let Err(e) = details.enrich_readme(&storage) {
401+
tracing::debug!("{e:?}")
402+
}
403+
}
404+
Ok(details)
362405
})
363406
.await?
364407
.ok_or(AxumNope::VersionNotFound)?;
@@ -1111,4 +1154,49 @@ mod tests {
11111154
Ok(())
11121155
});
11131156
}
1157+
1158+
#[test]
1159+
fn readme() {
1160+
wrapper(|env| {
1161+
env.fake_release()
1162+
.name("dummy")
1163+
.version("0.1.0")
1164+
.readme_only_database("database readme")
1165+
.create()?;
1166+
1167+
env.fake_release()
1168+
.name("dummy")
1169+
.version("0.2.0")
1170+
.readme_only_database("database readme")
1171+
.source_file("README.md", b"storage readme")
1172+
.create()?;
1173+
1174+
env.fake_release()
1175+
.name("dummy")
1176+
.version("0.3.0")
1177+
.source_file("README.md", b"storage readme")
1178+
.create()?;
1179+
1180+
env.fake_release()
1181+
.name("dummy")
1182+
.version("0.4.0")
1183+
.readme_only_database("database readme")
1184+
.source_file("MEREAD", b"storage meread")
1185+
.source_file("Cargo.toml", br#"package.readme = "MEREAD""#)
1186+
.create()?;
1187+
1188+
let check_readme = |path, content| {
1189+
let resp = env.frontend().get(path).send().unwrap();
1190+
let body = String::from_utf8(resp.bytes().unwrap().to_vec()).unwrap();
1191+
assert!(body.contains(content));
1192+
};
1193+
1194+
check_readme("/crate/dummy/0.1.0", "database readme");
1195+
check_readme("/crate/dummy/0.2.0", "storage readme");
1196+
check_readme("/crate/dummy/0.3.0", "storage readme");
1197+
check_readme("/crate/dummy/0.4.0", "storage meread");
1198+
1199+
Ok(())
1200+
});
1201+
}
11141202
}

0 commit comments

Comments
 (0)