johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
//! A repository's releases, as its owner published them
//! (docs/design/packages.md): each version of each package, newest first,
//! marked as its package's latest where it is, an image's marked as one;
//! and, for its pages' header, the release to show and its packages.
use super::repo::{found, invalid};
use super::*;
use crate::releases::Records;
use axum::http::HeaderName;
use knotless::records::{ReleaseFile, ReleaseRecord};
use knotless::release;
/// Releases a page shows unless asked for more or fewer.
const PAGE: usize = 30;
/// `dev.knotless.defs#releaseViewBasic`.
#[derive(Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ReleaseViewBasic {
uri: String,
package: String,
version: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
latest: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
image: bool,
created_at: String,
}
/// `dev.knotless.defs#releaseView`.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReleaseView {
uri: String,
package: String,
version: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
latest: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
image: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
yanked: bool,
#[serde(rename = "crate", skip_serializing_if = "std::ops::Not::not")]
is_crate: bool,
#[serde(skip_serializing_if = "Option::is_none")]
commit: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
check: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
notes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
html: Option<String>,
files: Vec<ReleaseFileView>,
created_at: String,
}
/// `dev.knotless.defs#releaseFileView`.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReleaseFileView {
name: String,
size: u64,
sha256: String,
}
/// `dev.knotless.defs#packageView`.
#[derive(Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PackageView {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
latest: Option<String>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
image: bool,
release_count: usize,
}
/// `dev.knotless.defs#releasesView`.
#[derive(Serialize, Debug, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ReleasesView {
count: usize,
release: ReleaseViewBasic,
packages: Vec<PackageView>,
}
/// `dev.knotless.listReleases`'s answer.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Releases {
releases: Vec<ReleaseView>,
#[serde(skip_serializing_if = "Vec::is_empty")]
packages: Vec<PackageView>,
/// Absent on the last page.
#[serde(skip_serializing_if = "Option::is_none")]
cursor: Option<String>,
}
#[derive(Deserialize)]
pub struct ReleasesParams {
repo: String,
package: Option<String>,
limit: Option<usize>,
cursor: Option<String>,
}
/// Whether `r` is a container image: an OCI layout, whose `index.json`
/// names its manifest.
fn image(r: &ReleaseRecord) -> bool {
r.files.iter().any(|f| f.name == "index.json")
}
/// A repository's releases, newest first, each with whether it is its
/// package's latest, as [`release::latest`] finds it.
pub(super) struct Published {
pub(super) all: Vec<(String, ReleaseRecord, bool)>,
}
impl Published {
/// `repo`'s, as the index has them: none for a private repository,
/// which cannot publish, or without an index.
pub(super) fn of(app: &App, repo: &Repo) -> Published {
let index = app.index.as_ref().filter(|_| repo.in_space.is_none());
let found = index.and_then(|i| i.releases(&repo.did, &repo.key).ok());
Published::new(found.unwrap_or_default())
}
fn new(found: Vec<(String, ReleaseRecord)>) -> Published {
let records: Vec<ReleaseRecord> = found.iter().map(|(_, r)| r.clone()).collect();
let all = found.into_iter().map(|(uri, r)| {
let latest = release::latest(&records, &r.package).map(|l| &l.version);
let latest = latest == Some(&r.version);
(uri, r, latest)
});
Published { all: all.collect() }
}
/// Each package, by name, with its latest version, how many versions
/// it has, and whether its latest, or its newest when it has none, is
/// an image.
fn packages(&self) -> Vec<PackageView> {
let mut packages: BTreeMap<&str, PackageView> = BTreeMap::new();
for (_, r, latest) in &self.all {
let p = packages.entry(&r.package).or_insert_with(|| PackageView {
name: r.package.clone(),
latest: None,
image: image(r),
release_count: 0,
});
p.release_count += 1;
if *latest {
p.latest = Some(r.version.clone());
p.image = image(r);
}
}
packages.into_values().collect()
}
/// What the pages of the repository `name` say of its releases under
/// its name: the latest of the package named as it is, its own, else
/// the newest that is its package's latest, or the newest of all when
/// each is a pre-release; none with no releases, or when each is yanked.
fn summary(&self, name: &str) -> Option<ReleasesView> {
let latest = || self.all.iter().filter(|(_, _, latest)| *latest);
let own = latest().find(|(_, r, _)| r.package == name);
let live = self.all.iter().find(|(_, r, _)| !r.yanked);
let (uri, r, latest) = own.or(latest().next()).or(live)?;
Some(ReleasesView {
count: self.all.len(),
release: ReleaseViewBasic {
uri: uri.clone(),
package: r.package.clone(),
version: r.version.clone(),
latest: *latest,
image: image(r),
created_at: r.created_at.clone(),
},
packages: self.packages(),
})
}
}
/// What `repo`'s pages say of its releases under its name.
pub(super) fn summary(app: &App, repo: &Repo) -> Option<ReleasesView> {
Published::of(app, repo).summary(&repo.name)
}
/// `dev.knotless.listReleases`.
pub(super) async fn list_releases(
State(app): State<Arc<App>>,
Query(p): Query<ReleasesParams>,
) -> Result<Response> {
let limit = p.limit.unwrap_or(PAGE).clamp(1, 100);
let skip = match p.cursor.as_deref().map(str::parse::<usize>) {
None => 0,
Some(Ok(skip)) => skip,
Some(Err(_)) => return Err(invalid("cursor is no count of releases".into())),
};
let repo = found(&app, &p.repo).await?;
let published = Published::of(&app, &repo);
let packages = published.packages();
let base = repo.href("");
let wanted = |r: &ReleaseRecord| p.package.as_ref().is_none_or(|name| &r.package == name);
let mut all = published.all.into_iter().filter(|(_, r, _)| wanted(r));
let releases: Vec<ReleaseView> = all
.by_ref()
.skip(skip)
.take(limit)
.map(|(uri, r, latest)| ReleaseView {
html: r
.notes
.as_deref()
.map(|n| markdown(n, &base, &base, "", None)),
image: image(&r),
is_crate: r.cargo.is_some(),
uri,
package: r.package,
version: r.version,
latest,
yanked: r.yanked,
commit: r.commit,
check: r.check,
notes: r.notes,
files: r.files.into_iter().map(file_view).collect(),
created_at: r.created_at,
})
.collect();
let more = all.next().is_some();
let cursor = more.then(|| (skip + releases.len()).to_string());
Ok(answer(Releases {
releases,
packages,
cursor,
}))
}
/// `/<authority>/<repo>/releases/download/<package>/<version>/<file>`: a
/// file of a public repository's release, read from its owner's PDS the
/// first time it is asked for, checked against its record and kept, as
/// `--releases` hands out its own. Read as no one, so a private
/// repository's, or one taken down, is not found.
pub(super) async fn download(
State(app): State<Arc<App>>,
headers: HeaderMap,
Path((authority, name, rest)): RestPath,
) -> Result<Response> {
let repo = Repo::open(&app, &authority, &name).await?;
let Some((package, f)) = released(&app, &repo, &rest) else {
let why = anyhow::anyhow!("{} has released no {rest}", repo.href(""));
return Err(not_found(why));
};
let etag = format!("\"{}\"", f.sha256);
let seen = headers.get(header::IF_NONE_MATCH);
if seen.is_some_and(|v| v.as_bytes() == etag.as_bytes()) {
return Ok((StatusCode::NOT_MODIFIED, [(header::ETAG, etag)]).into_response());
}
let url = format!("knotless://{}/{}", repo.did, repo.name);
let url = RepoUrl::parse(&url).map_err(AppError::internal)?;
let records = Records::new(app.ctx.clone(), url, package);
let sent = records.send(&f, sending(&f, etag)).await;
let sent = sent.map_err(AppError::upstream)?;
Ok(sent)
}
/// The package and file `rest`, `<package>/<version>/<file>`, names among
/// `repo`'s releases, as its releases page lists them.
fn released(app: &App, repo: &Repo, rest: &str) -> Option<(String, ReleaseFile)> {
let (rest, file) = rest.rsplit_once('/')?;
let (package, version) = rest.rsplit_once('/')?;
let published = Published::of(app, repo).all.into_iter();
let mut asked = published.filter(|(_, r, _)| r.package == package && r.version == version);
let (_, r, _) = asked.next()?;
let f = r.files.into_iter().find(|f| f.name == file);
// Its name goes in a header, so only one a publish takes.
let f = f.filter(|f| release::valid_file(&f.name))?;
Some((r.package, f))
}
/// What a release's file `f` is sent with: as bytes to save, never a page
/// of the site; checked again each time, so a takedown holds at once; and
/// `etag`, its hash, for a cache to ask by.
pub(super) fn sending(f: &ReleaseFile, etag: String) -> [(HeaderName, String); 6] {
let says = |s: &str| s.to_string();
let saved = format!("attachment; filename=\"{}\"", f.name);
[
(header::CONTENT_TYPE, says("application/octet-stream")),
(header::CONTENT_DISPOSITION, saved),
(header::CACHE_CONTROL, says("no-cache")),
(header::ETAG, etag),
(header::X_CONTENT_TYPE_OPTIONS, says("nosniff")),
(
header::CONTENT_SECURITY_POLICY,
says("sandbox; default-src 'none'"),
),
]
}
fn file_view(f: ReleaseFile) -> ReleaseFileView {
ReleaseFileView {
name: f.name,
size: f.size,
sha256: f.sha256,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn record(package: &str, version: &str, files: &[&str]) -> (String, ReleaseRecord) {
let files = files.iter().map(|name| ReleaseFile {
name: name.to_string(),
size: 1,
sha256: "0".repeat(64),
parts: Vec::new(),
});
let r = ReleaseRecord {
repo: "k".into(),
package: package.into(),
version: version.into(),
commit: None,
check: None,
notes: None,
files: files.collect(),
created_at: "2026-10-01T00:00:00Z".into(),
cargo: None,
yanked: false,
};
(
format!("at://did:a/{}/{package}{version}", nsid::RELEASE),
r,
)
}
#[test]
fn the_release_shown_is_its_own_or_the_newest_latest_or_else_the_newest() {
let only_pre = Published::new(vec![
record("a", "1.0.0-rc.2", &["x"]),
record("a", "1.0.0-rc.1", &["x"]),
]);
let shown = only_pre.summary("a").unwrap();
assert_eq!(
(shown.release.version.as_str(), shown.release.latest),
("1.0.0-rc.2", false)
);
assert_eq!(shown.packages[0].latest, None);
let mixed = Published::new(vec![
record("img", "2.0.0-rc.1", &["x"]),
record("img", "1.0.0", &["index.json", "sha256-0"]),
record("cli", "0.1.0", &["x"]),
]);
let shown = mixed.summary("tool").unwrap();
assert_eq!(shown.release.package, "img");
assert!(shown.release.latest && shown.release.image);
// The package named as the repository is the one shown, newest
// though another's latest is.
let own = mixed.summary("cli").unwrap();
assert_eq!(
(own.release.package.as_str(), own.release.version.as_str()),
("cli", "0.1.0")
);
let names: Vec<_> = shown
.packages
.iter()
.map(|p| (p.name.as_str(), p.image, p.release_count))
.collect();
assert_eq!(names, [("cli", false, 1), ("img", true, 2)]);
assert!(Published::new(Vec::new()).summary("a").is_none());
// A yanked version is never the one shown, nor its package's latest.
let yanked = |version| {
let (uri, r) = record("a", version, &["x"]);
(uri, ReleaseRecord { yanked: true, ..r })
};
let some = Published::new(vec![yanked("2.0.0"), record("a", "1.0.0", &["x"])]);
let shown = some.summary("a").unwrap();
assert_eq!(shown.release.version, "1.0.0");
assert_eq!(shown.packages[0].latest.as_deref(), Some("1.0.0"));
let pre = Published::new(vec![
yanked("2.0.0-rc.2"),
record("a", "2.0.0-rc.1", &["x"]),
]);
assert_eq!(pre.summary("a").unwrap().release.version, "2.0.0-rc.1");
assert!(Published::new(vec![yanked("1.0.0")]).summary("a").is_none());
}
}