feat: give more info from registry api

This commit is contained in:
daimond113 2024-08-13 20:14:41 +02:00
parent 83286ff758
commit 4f03155af0
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
6 changed files with 115 additions and 97 deletions

View file

@ -1,5 +1,3 @@
use std::str::FromStr;
use actix_web::{http::header::ACCEPT, web, HttpRequest, HttpResponse, Responder}; use actix_web::{http::header::ACCEPT, web, HttpRequest, HttpResponse, Responder};
use rusty_s3::{actions::GetObject, S3Action}; use rusty_s3::{actions::GetObject, S3Action};
use semver::Version; use semver::Version;
@ -38,38 +36,16 @@ impl<'de> Deserialize<'de> for VersionRequest {
} }
} }
#[derive(Debug)]
pub enum TargetRequest {
All,
Specific(TargetKind),
}
impl<'de> Deserialize<'de> for TargetRequest {
fn deserialize<D>(deserializer: D) -> Result<TargetRequest, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if s.eq_ignore_ascii_case("all") {
return Ok(TargetRequest::All);
}
TargetKind::from_str(&s)
.map(TargetRequest::Specific)
.map_err(serde::de::Error::custom)
}
}
pub async fn get_package_version( pub async fn get_package_version(
request: HttpRequest, request: HttpRequest,
app_state: web::Data<AppState>, app_state: web::Data<AppState>,
path: web::Path<(PackageName, VersionRequest, TargetRequest)>, path: web::Path<(PackageName, VersionRequest, TargetKind)>,
) -> Result<impl Responder, Error> { ) -> Result<impl Responder, Error> {
let (name, version, target) = path.into_inner(); let (name, version, target) = path.into_inner();
let (scope, name_part) = name.as_str(); let (scope, name_part) = name.as_str();
let versions: IndexFile = { let entries: IndexFile = {
let source = app_state.source.lock().unwrap(); let source = app_state.source.lock().unwrap();
match source.read_file([scope, name_part], &app_state.project, None)? { match source.read_file([scope, name_part], &app_state.project, None)? {
@ -78,33 +54,17 @@ pub async fn get_package_version(
} }
}; };
let mut versions = entries.iter().filter(|(v_id, _)| *v_id.target() == target);
let version = match version { let version = match version {
VersionRequest::Latest => versions VersionRequest::Latest => versions.max_by_key(|(v, _)| v.version().clone()),
.iter() VersionRequest::Specific(version) => versions.find(|(v, _)| *v.version() == version),
.filter(|(v_id, _)| match target {
TargetRequest::All => true,
TargetRequest::Specific(target) => *v_id.target() == target,
})
.max_by_key(|(v, _)| v.version().clone()),
VersionRequest::Specific(version) => versions.iter().find(|(v, _)| {
*v.version() == version
&& match target {
TargetRequest::All => true,
TargetRequest::Specific(target) => *v.target() == target,
}
}),
}; };
let Some((v_id, entry)) = version else { let Some((v_id, entry)) = version else {
return Ok(HttpResponse::NotFound().finish()); return Ok(HttpResponse::NotFound().finish());
}; };
let other_targets = versions
.iter()
.filter(|(v, _)| v.version() == v_id.version() && v.target() != v_id.target())
.map(|(v_id, _)| v_id.target().to_string())
.collect::<Vec<_>>();
if request if request
.headers() .headers()
.get(ACCEPT) .get(ACCEPT)
@ -130,20 +90,19 @@ pub async fn get_package_version(
)); ));
} }
let entry = entry.clone(); Ok(HttpResponse::Ok().json(PackageResponse {
let mut response = serde_json::to_value(PackageResponse {
name: name.to_string(), name: name.to_string(),
version: v_id.version().to_string(), version: v_id.version().to_string(),
target: entry.target.into(), targets: entries
description: entry.description.unwrap_or_default(), .values()
published_at: entry.published_at, .map(|entry| (&entry.target).into())
license: entry.license.unwrap_or_default(), .collect(),
})?; description: entry.description.clone().unwrap_or_default(),
published_at: entries
if !other_targets.is_empty() { .values()
response["other_targets"] = serde_json::to_value(other_targets)?; .max_by_key(|entry| entry.published_at)
} .unwrap()
.published_at,
Ok(HttpResponse::Ok().json(response)) license: entry.license.clone().unwrap_or_default(),
}))
} }

View file

@ -1,4 +1,5 @@
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use std::collections::BTreeSet;
use crate::{error::Error, package::PackageResponse, AppState}; use crate::{error::Error, package::PackageResponse, AppState};
use pesde::{ use pesde::{
@ -27,7 +28,7 @@ pub async fn get_package_versions(
.map(|(v_id, entry)| PackageResponse { .map(|(v_id, entry)| PackageResponse {
name: name.to_string(), name: name.to_string(),
version: v_id.version().to_string(), version: v_id.version().to_string(),
target: entry.target.into(), targets: BTreeSet::from([entry.target.into()]),
description: entry.description.unwrap_or_default(), description: entry.description.unwrap_or_default(),
published_at: entry.published_at, published_at: entry.published_at,
license: entry.license.unwrap_or_default(), license: entry.license.unwrap_or_default(),

View file

@ -26,7 +26,7 @@ use pesde::{
use crate::{ use crate::{
auth::UserId, auth::UserId,
benv, benv,
error::Error, error::{Error, ErrorResponse},
package::{s3_name, S3_SIGN_DURATION}, package::{s3_name, S3_SIGN_DURATION},
search::update_version, search::update_version,
AppState, AppState,
@ -208,6 +208,22 @@ pub async fn publish_package(
dependencies, dependencies,
}; };
let this_version = entries
.keys()
.find(|v_id| *v_id.version() == manifest.version);
if let Some(this_version) = this_version {
let other_entry = entries.get(this_version).unwrap();
if other_entry.description != new_entry.description
|| other_entry.license != new_entry.license
{
return Ok(HttpResponse::BadRequest().json(ErrorResponse {
error: "same version with different description or license already exists"
.to_string(),
}));
}
}
if entries if entries
.insert( .insert(
VersionId::new(manifest.version.clone(), manifest.target.kind()), VersionId::new(manifest.version.clone(), manifest.target.kind()),

View file

@ -2,14 +2,15 @@ use std::collections::HashMap;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use serde::Deserialize; use serde::Deserialize;
use tantivy::{query::AllQuery, schema::Value, DateTime, Order}; use tantivy::{collector::Count, query::AllQuery, schema::Value, DateTime, Order};
use crate::{error::Error, package::PackageResponse, AppState};
use pesde::{ use pesde::{
names::PackageName, names::PackageName,
source::{git_index::GitBasedSource, pesde::IndexFile}, source::{git_index::GitBasedSource, pesde::IndexFile},
}; };
use crate::{error::Error, package::PackageResponse, AppState};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct Request { pub struct Request {
#[serde(default)] #[serde(default)]
@ -46,12 +47,15 @@ pub async fn search_packages(
query_parser.parse_query(query)? query_parser.parse_query(query)?
}; };
let top_docs = searcher let (count, top_docs) = searcher
.search( .search(
&query, &query,
&tantivy::collector::TopDocs::with_limit(50) &(
.and_offset(request.offset.unwrap_or_default()) Count,
.order_by_fast_field::<DateTime>("published_at", Order::Desc), tantivy::collector::TopDocs::with_limit(50)
.and_offset(request.offset.unwrap_or_default())
.order_by_fast_field::<DateTime>("published_at", Order::Desc),
),
) )
.unwrap(); .unwrap();
@ -71,7 +75,7 @@ pub async fn search_packages(
.unwrap(); .unwrap();
let (scope, name) = id.as_str(); let (scope, name) = id.as_str();
let mut versions: IndexFile = toml::de::from_str( let versions: IndexFile = toml::de::from_str(
&source &source
.read_file([scope, name], &app_state.project, None) .read_file([scope, name], &app_state.project, None)
.unwrap() .unwrap()
@ -79,18 +83,32 @@ pub async fn search_packages(
) )
.unwrap(); .unwrap();
let (version_id, entry) = versions.pop_last().unwrap(); let (latest_version, entry) = versions
.iter()
.max_by_key(|(v_id, _)| v_id.version())
.unwrap();
PackageResponse { PackageResponse {
name: id.to_string(), name: id.to_string(),
version: version_id.version().to_string(), version: latest_version.version().to_string(),
target: entry.target.into(), targets: versions
description: entry.description.unwrap_or_default(), .iter()
published_at: entry.published_at, .filter(|(v_id, _)| v_id.version() == latest_version.version())
license: entry.license.unwrap_or_default(), .map(|(_, entry)| (&entry.target).into())
.collect(),
description: entry.description.clone().unwrap_or_default(),
published_at: versions
.values()
.max_by_key(|entry| entry.published_at)
.unwrap()
.published_at,
license: entry.license.clone().unwrap_or_default(),
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(top_docs)) Ok(HttpResponse::Ok().json(serde_json::json!({
"data": top_docs,
"count": count,
})))
} }

View file

@ -5,7 +5,7 @@ use pesde::{
source::version_id::VersionId, source::version_id::VersionId,
}; };
use serde::Serialize; use serde::Serialize;
use std::time::Duration; use std::{collections::BTreeSet, time::Duration};
pub const S3_SIGN_DURATION: Duration = Duration::from_secs(60 * 60); pub const S3_SIGN_DURATION: Duration = Duration::from_secs(60 * 60);
@ -13,7 +13,7 @@ pub fn s3_name(name: &PackageName, version_id: &VersionId) -> String {
format!("{}+{}.tar.gz", name.escaped(), version_id.escaped()) format!("{}+{}.tar.gz", name.escaped(), version_id.escaped())
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize, Eq, PartialEq)]
pub struct TargetInfo { pub struct TargetInfo {
kind: TargetKind, kind: TargetKind,
lib: bool, lib: bool,
@ -30,11 +30,33 @@ impl From<Target> for TargetInfo {
} }
} }
impl From<&Target> for TargetInfo {
fn from(target: &Target) -> Self {
TargetInfo {
kind: target.kind(),
lib: target.lib_path().is_some(),
bin: target.bin_path().is_some(),
}
}
}
impl Ord for TargetInfo {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.kind.cmp(&other.kind)
}
}
impl PartialOrd for TargetInfo {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct PackageResponse { pub struct PackageResponse {
pub name: String, pub name: String,
pub version: String, pub version: String,
pub target: TargetInfo, pub targets: BTreeSet<TargetInfo>,
#[serde(skip_serializing_if = "String::is_empty")] #[serde(skip_serializing_if = "String::is_empty")]
pub description: String, pub description: String,
pub published_at: DateTime<Utc>, pub published_at: DateTime<Utc>,

View file

@ -406,34 +406,24 @@ impl PublishCommand {
return Ok(()); return Ok(());
} }
match reqwest let response = reqwest
.post(format!("{}/v0/packages", config.api())) .post(format!("{}/v0/packages", config.api()))
.multipart(reqwest::blocking::multipart::Form::new().part( .multipart(reqwest::blocking::multipart::Form::new().part(
"tarball", "tarball",
reqwest::blocking::multipart::Part::bytes(archive).file_name("package.tar.gz"), reqwest::blocking::multipart::Part::bytes(archive).file_name("package.tar.gz"),
)) ))
.send() .send()
.context("failed to send request")? .context("failed to send request")?;
.error_for_status()
.and_then(|response| response.text())
{
Ok(response) => {
println!("{response}");
Ok(()) let status = response.status();
} let text = response.text().context("failed to get response text")?;
Err(e) match status {
if e.status() StatusCode::CONFLICT => {
.is_some_and(|status| status == StatusCode::CONFLICT) =>
{
println!("{}", "package version already exists".red().bold()); println!("{}", "package version already exists".red().bold());
Ok(()) Ok(())
} }
Err(e) StatusCode::FORBIDDEN => {
if e.status()
.is_some_and(|status| status == StatusCode::FORBIDDEN) =>
{
println!( println!(
"{}", "{}",
"unauthorized to publish under this scope".red().bold() "unauthorized to publish under this scope".red().bold()
@ -441,7 +431,19 @@ impl PublishCommand {
Ok(()) Ok(())
} }
Err(e) => Err(e).context("failed to get response"), StatusCode::BAD_REQUEST => {
println!("{}: {text}", "invalid package".red().bold());
Ok(())
}
code if !code.is_success() => {
anyhow::bail!("failed to publish package: {code} ({text})");
}
_ => {
println!("{text}");
Ok(())
}
} }
} }
} }