~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
385 lines · 13.6 KB Raw History
//! 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());
    }
}