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 rusty_s3::{actions::GetObject, S3Action};
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(
request: HttpRequest,
app_state: web::Data<AppState>,
path: web::Path<(PackageName, VersionRequest, TargetRequest)>,
path: web::Path<(PackageName, VersionRequest, TargetKind)>,
) -> Result<impl Responder, Error> {
let (name, version, target) = path.into_inner();
let (scope, name_part) = name.as_str();
let versions: IndexFile = {
let entries: IndexFile = {
let source = app_state.source.lock().unwrap();
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 {
VersionRequest::Latest => versions
.iter()
.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,
}
}),
VersionRequest::Latest => versions.max_by_key(|(v, _)| v.version().clone()),
VersionRequest::Specific(version) => versions.find(|(v, _)| *v.version() == version),
};
let Some((v_id, entry)) = version else {
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
.headers()
.get(ACCEPT)
@ -130,20 +90,19 @@ pub async fn get_package_version(
));
}
let entry = entry.clone();
let mut response = serde_json::to_value(PackageResponse {
Ok(HttpResponse::Ok().json(PackageResponse {
name: name.to_string(),
version: v_id.version().to_string(),
target: entry.target.into(),
description: entry.description.unwrap_or_default(),
published_at: entry.published_at,
license: entry.license.unwrap_or_default(),
})?;
if !other_targets.is_empty() {
response["other_targets"] = serde_json::to_value(other_targets)?;
}
Ok(HttpResponse::Ok().json(response))
targets: entries
.values()
.map(|entry| (&entry.target).into())
.collect(),
description: entry.description.clone().unwrap_or_default(),
published_at: entries
.values()
.max_by_key(|entry| entry.published_at)
.unwrap()
.published_at,
license: entry.license.clone().unwrap_or_default(),
}))
}

View file

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

View file

@ -26,7 +26,7 @@ use pesde::{
use crate::{
auth::UserId,
benv,
error::Error,
error::{Error, ErrorResponse},
package::{s3_name, S3_SIGN_DURATION},
search::update_version,
AppState,
@ -208,6 +208,22 @@ pub async fn publish_package(
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
.insert(
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 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::{
names::PackageName,
source::{git_index::GitBasedSource, pesde::IndexFile},
};
use crate::{error::Error, package::PackageResponse, AppState};
#[derive(Deserialize)]
pub struct Request {
#[serde(default)]
@ -46,12 +47,15 @@ pub async fn search_packages(
query_parser.parse_query(query)?
};
let top_docs = searcher
let (count, top_docs) = searcher
.search(
&query,
&tantivy::collector::TopDocs::with_limit(50)
&(
Count,
tantivy::collector::TopDocs::with_limit(50)
.and_offset(request.offset.unwrap_or_default())
.order_by_fast_field::<DateTime>("published_at", Order::Desc),
),
)
.unwrap();
@ -71,7 +75,7 @@ pub async fn search_packages(
.unwrap();
let (scope, name) = id.as_str();
let mut versions: IndexFile = toml::de::from_str(
let versions: IndexFile = toml::de::from_str(
&source
.read_file([scope, name], &app_state.project, None)
.unwrap()
@ -79,18 +83,32 @@ pub async fn search_packages(
)
.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 {
name: id.to_string(),
version: version_id.version().to_string(),
target: entry.target.into(),
description: entry.description.unwrap_or_default(),
published_at: entry.published_at,
license: entry.license.unwrap_or_default(),
version: latest_version.version().to_string(),
targets: versions
.iter()
.filter(|(v_id, _)| v_id.version() == latest_version.version())
.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<_>>();
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,
};
use serde::Serialize;
use std::time::Duration;
use std::{collections::BTreeSet, time::Duration};
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())
}
#[derive(Debug, Serialize)]
#[derive(Debug, Serialize, Eq, PartialEq)]
pub struct TargetInfo {
kind: TargetKind,
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)]
pub struct PackageResponse {
pub name: String,
pub version: String,
pub target: TargetInfo,
pub targets: BTreeSet<TargetInfo>,
#[serde(skip_serializing_if = "String::is_empty")]
pub description: String,
pub published_at: DateTime<Utc>,

View file

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