mirror of
https://github.com/pesde-pkg/pesde.git
synced 2024-12-12 11:00:36 +00:00
feat: give more info from registry api
This commit is contained in:
parent
83286ff758
commit
4f03155af0
6 changed files with 115 additions and 97 deletions
|
@ -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(),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
|
@ -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)
|
&(
|
||||||
|
Count,
|
||||||
|
tantivy::collector::TopDocs::with_limit(50)
|
||||||
.and_offset(request.offset.unwrap_or_default())
|
.and_offset(request.offset.unwrap_or_default())
|
||||||
.order_by_fast_field::<DateTime>("published_at", Order::Desc),
|
.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,
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue