feat: store more package info in index

This commit is contained in:
daimond113 2024-08-14 19:55:58 +02:00
parent cc85135a8e
commit 7aaea85a2d
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
11 changed files with 158 additions and 105 deletions

View file

@ -37,15 +37,15 @@ pub async fn authentication(
}; };
let token = if token.to_lowercase().starts_with("bearer ") { let token = if token.to_lowercase().starts_with("bearer ") {
token[7..].to_string()
} else {
token.to_string() token.to_string()
} else {
format!("Bearer {token}")
}; };
let response = match app_state let response = match app_state
.reqwest_client .reqwest_client
.get("https://api.github.com/user") .get("https://api.github.com/user")
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}")) .header(reqwest::header::AUTHORIZATION, token)
.send() .send()
.await .await
.and_then(|res| res.error_for_status()) .and_then(|res| res.error_for_status())

View file

@ -1,19 +1,21 @@
use actix_web::{http::header::ACCEPT, web, HttpRequest, HttpResponse, Responder};
use rusty_s3::{actions::GetObject, S3Action};
use semver::Version;
use serde::{Deserialize, Deserializer};
use std::collections::BTreeSet;
use crate::{ use crate::{
error::Error, error::Error,
package::{s3_name, PackageResponse, S3_SIGN_DURATION}, package::{s3_name, PackageResponse, S3_SIGN_DURATION},
AppState, AppState,
}; };
use actix_web::{
http::header::{ACCEPT, LOCATION},
web, HttpRequest, HttpResponse, Responder,
};
use pesde::{ use pesde::{
manifest::target::TargetKind, manifest::target::TargetKind,
names::PackageName, names::PackageName,
source::{git_index::GitBasedSource, pesde::IndexFile}, source::{git_index::GitBasedSource, pesde::IndexFile},
}; };
use rusty_s3::{actions::GetObject, S3Action};
use semver::Version;
use serde::{Deserialize, Deserializer};
use std::collections::BTreeSet;
#[derive(Debug)] #[derive(Debug)]
pub enum VersionRequest { pub enum VersionRequest {
@ -68,29 +70,27 @@ pub async fn get_package_version(
return Ok(HttpResponse::NotFound().finish()); return Ok(HttpResponse::NotFound().finish());
}; };
if request let accept = request
.headers() .headers()
.get(ACCEPT) .get(ACCEPT)
.and_then(|accept| accept.to_str().ok()) .and_then(|accept| accept.to_str().ok())
.is_some_and(|accept| accept.eq_ignore_ascii_case("application/octet-stream")) .and_then(|accept| match accept.to_lowercase().as_str() {
{ "text/plain" => Some(true),
"application/octet-stream" => Some(false),
_ => None,
});
if let Some(readme) = accept {
let object_url = GetObject::new( let object_url = GetObject::new(
&app_state.s3_bucket, &app_state.s3_bucket,
Some(&app_state.s3_credentials), Some(&app_state.s3_credentials),
&s3_name(&name, &v_id), &s3_name(&name, &v_id, readme),
) )
.sign(S3_SIGN_DURATION); .sign(S3_SIGN_DURATION);
return Ok(HttpResponse::Ok().body( return Ok(HttpResponse::TemporaryRedirect()
app_state .append_header((LOCATION, object_url.as_str()))
.reqwest_client .finish());
.get(object_url)
.send()
.await?
.error_for_status()?
.bytes()
.await?,
));
} }
Ok(HttpResponse::Ok().json(PackageResponse { Ok(HttpResponse::Ok().json(PackageResponse {
@ -100,5 +100,7 @@ pub async fn get_package_version(
description: entry.description.clone().unwrap_or_default(), description: entry.description.clone().unwrap_or_default(),
published_at: entry.published_at, published_at: entry.published_at,
license: entry.license.clone().unwrap_or_default(), license: entry.license.clone().unwrap_or_default(),
authors: entry.authors.clone(),
repository: entry.repository.clone().map(|url| url.to_string()),
})) }))
} }

View file

@ -36,6 +36,8 @@ pub async fn get_package_versions(
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(),
authors: entry.authors.clone(),
repository: entry.repository.clone().map(|url| url.to_string()),
}); });
info.targets.insert(entry.target.into()); info.targets.insert(entry.target.into());

View file

@ -8,19 +8,12 @@ use actix_web::{web, HttpResponse, Responder};
use flate2::read::GzDecoder; use flate2::read::GzDecoder;
use futures::StreamExt; use futures::StreamExt;
use git2::{Remote, Repository, Signature}; use git2::{Remote, Repository, Signature};
use reqwest::header::{CONTENT_ENCODING, CONTENT_TYPE};
use rusty_s3::{actions::PutObject, S3Action}; use rusty_s3::{actions::PutObject, S3Action};
use tar::Archive; use tar::Archive;
use crate::{
auth::UserId,
benv,
error::{Error, ErrorResponse},
package::{s3_name, S3_SIGN_DURATION},
search::update_version,
AppState,
};
use pesde::{ use pesde::{
manifest::{DependencyType, Manifest}, manifest::Manifest,
source::{ source::{
git_index::GitBasedSource, git_index::GitBasedSource,
pesde::{IndexFile, IndexFileEntry, ScopeInfo, SCOPE_INFO_FILE}, pesde::{IndexFile, IndexFileEntry, ScopeInfo, SCOPE_INFO_FILE},
@ -31,6 +24,15 @@ use pesde::{
MANIFEST_FILE_NAME, MANIFEST_FILE_NAME,
}; };
use crate::{
auth::UserId,
benv,
error::{Error, ErrorResponse},
package::{s3_name, S3_SIGN_DURATION},
search::update_version,
AppState,
};
fn signature<'a>() -> Signature<'a> { fn signature<'a>() -> Signature<'a> {
Signature::now( Signature::now(
&benv!(required "COMMITTER_GIT_NAME"), &benv!(required "COMMITTER_GIT_NAME"),
@ -80,6 +82,7 @@ pub async fn publish_package(
let entries = archive.entries()?; let entries = archive.entries()?;
let mut manifest = None::<Manifest>; let mut manifest = None::<Manifest>;
let mut readme = None::<Vec<u8>>;
for entry in entries { for entry in entries {
let mut entry = entry?; let mut entry = entry?;
@ -107,6 +110,21 @@ pub async fn publish_package(
let mut content = String::new(); let mut content = String::new();
entry.read_to_string(&mut content)?; entry.read_to_string(&mut content)?;
manifest = Some(toml::de::from_str(&content).map_err(|_| Error::InvalidArchive)?); manifest = Some(toml::de::from_str(&content).map_err(|_| Error::InvalidArchive)?);
} else if path.to_lowercase() == "readme"
|| path
.to_lowercase()
.split_once('.')
.filter(|(file, ext)| *file == "readme" && (*ext == "md" || *ext == "txt"))
.is_some()
{
if readme.is_some() {
return Err(Error::InvalidArchive);
}
let mut gz = flate2::read::GzEncoder::new(entry, flate2::Compression::best());
let mut bytes = vec![];
gz.read_to_end(&mut bytes)?;
readme = Some(bytes);
} }
} }
@ -121,59 +139,52 @@ pub async fn publish_package(
let dependencies = manifest let dependencies = manifest
.all_dependencies() .all_dependencies()
.map_err(|_| Error::InvalidArchive)? .map_err(|_| Error::InvalidArchive)?;
.into_iter()
.filter_map(|(alias, (specifier, ty))| {
match &specifier {
DependencySpecifiers::Pesde(specifier) => {
if specifier
.index
.as_ref()
.filter(|index| match index.parse::<url::Url>() {
Ok(_) if config.other_registries_allowed => true,
Ok(url) => url == env!("CARGO_PKG_REPOSITORY").parse().unwrap(),
Err(_) => false,
})
.is_none()
{
return Some(Err(Error::InvalidArchive));
}
let (dep_scope, dep_name) = specifier.name.as_str(); for (specifier, _) in dependencies.values() {
match source.read_file([dep_scope, dep_name], &app_state.project, None) { match specifier {
Ok(Some(_)) => {} DependencySpecifiers::Pesde(specifier) => {
Ok(None) => return Some(Err(Error::InvalidArchive)), if specifier
Err(e) => return Some(Err(e.into())), .index
} .as_ref()
.filter(|index| match index.parse::<url::Url>() {
Ok(_) if config.other_registries_allowed => true,
Ok(url) => url == env!("CARGO_PKG_REPOSITORY").parse().unwrap(),
Err(_) => false,
})
.is_none()
{
return Err(Error::InvalidArchive);
} }
DependencySpecifiers::Wally(specifier) => {
if !config.wally_allowed {
return Some(Err(Error::InvalidArchive));
}
if specifier let (dep_scope, dep_name) = specifier.name.as_str();
.index match source.read_file([dep_scope, dep_name], &app_state.project, None) {
.as_ref() Ok(Some(_)) => {}
.filter(|index| index.parse::<url::Url>().is_ok()) Ok(None) => return Err(Error::InvalidArchive),
.is_none() Err(e) => return Err(e.into()),
{
return Some(Err(Error::InvalidArchive));
}
} }
DependencySpecifiers::Git(_) => {
if !config.git_allowed {
return Some(Err(Error::InvalidArchive));
}
}
};
if ty == DependencyType::Dev {
return None;
} }
DependencySpecifiers::Wally(specifier) => {
if !config.wally_allowed {
return Err(Error::InvalidArchive);
}
Some(Ok((alias, (specifier, ty)))) if specifier
}) .index
.collect::<Result<_, Error>>()?; .as_ref()
.filter(|index| index.parse::<url::Url>().is_ok())
.is_none()
{
return Err(Error::InvalidArchive);
}
}
DependencySpecifiers::Git(_) => {
if !config.git_allowed {
return Err(Error::InvalidArchive);
}
}
}
}
let repo = source.repo_git2(&app_state.project)?; let repo = source.repo_git2(&app_state.project)?;
@ -209,6 +220,8 @@ pub async fn publish_package(
published_at: chrono::Utc::now(), published_at: chrono::Utc::now(),
description: manifest.description.clone(), description: manifest.description.clone(),
license: manifest.license.clone(), license: manifest.license.clone(),
authors: manifest.authors.clone(),
repository: manifest.repository.clone(),
dependencies, dependencies,
}; };
@ -219,10 +232,12 @@ pub async fn publish_package(
if let Some(this_version) = this_version { if let Some(this_version) = this_version {
let other_entry = entries.get(this_version).unwrap(); let other_entry = entries.get(this_version).unwrap();
// TODO: should different licenses be allowed?
// description cannot be different - which one to render in the "Recently published" list? // description cannot be different - which one to render in the "Recently published" list?
// the others cannot be different because what to return from the versions endpoint?
if other_entry.description != new_entry.description if other_entry.description != new_entry.description
|| other_entry.license != new_entry.license || other_entry.license != new_entry.license
|| other_entry.authors != new_entry.authors
|| other_entry.repository != new_entry.repository
{ {
return Ok(HttpResponse::BadRequest().json(ErrorResponse { return Ok(HttpResponse::BadRequest().json(ErrorResponse {
error: "same version with different description or license already exists" error: "same version with different description or license already exists"
@ -297,23 +312,42 @@ pub async fn publish_package(
update_version(&app_state, &manifest.name, new_entry); update_version(&app_state, &manifest.name, new_entry);
} }
let version_id = VersionId::new(manifest.version.clone(), manifest.target.kind());
let object_url = PutObject::new( let object_url = PutObject::new(
&app_state.s3_bucket, &app_state.s3_bucket,
Some(&app_state.s3_credentials), Some(&app_state.s3_credentials),
&s3_name( &s3_name(&manifest.name, &version_id, false),
&manifest.name,
&VersionId::new(manifest.version.clone(), manifest.target.kind()),
),
) )
.sign(S3_SIGN_DURATION); .sign(S3_SIGN_DURATION);
app_state app_state
.reqwest_client .reqwest_client
.put(object_url) .put(object_url)
.header(CONTENT_TYPE, "application/gzip")
.header(CONTENT_ENCODING, "gzip")
.body(bytes) .body(bytes)
.send() .send()
.await?; .await?;
if let Some(readme) = readme {
let object_url = PutObject::new(
&app_state.s3_bucket,
Some(&app_state.s3_credentials),
&s3_name(&manifest.name, &version_id, true),
)
.sign(S3_SIGN_DURATION);
app_state
.reqwest_client
.put(object_url)
.header(CONTENT_TYPE, "text/plain")
.header(CONTENT_ENCODING, "gzip")
.body(readme)
.send()
.await?;
}
Ok(HttpResponse::Ok().body(format!( Ok(HttpResponse::Ok().body(format!(
"published {}@{} {}", "published {}@{} {}",
manifest.name, manifest.version, manifest.target manifest.name, manifest.version, manifest.target

View file

@ -103,6 +103,8 @@ pub async fn search_packages(
.unwrap() .unwrap()
.published_at, .published_at,
license: entry.license.clone().unwrap_or_default(), license: entry.license.clone().unwrap_or_default(),
authors: entry.authors.clone(),
repository: entry.repository.clone().map(|url| url.to_string()),
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View file

@ -7,10 +7,15 @@ use pesde::{
use serde::Serialize; use serde::Serialize;
use std::{collections::BTreeSet, 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 * 3);
pub fn s3_name(name: &PackageName, version_id: &VersionId) -> String { pub fn s3_name(name: &PackageName, version_id: &VersionId, is_readme: bool) -> String {
format!("{}+{}.tar.gz", name.escaped(), version_id.escaped()) format!(
"{}+{}{}",
name.escaped(),
version_id.escaped(),
if is_readme { "+readme.gz" } else { ".tar.gz" }
)
} }
#[derive(Debug, Serialize, Eq, PartialEq)] #[derive(Debug, Serialize, Eq, PartialEq)]
@ -22,11 +27,7 @@ pub struct TargetInfo {
impl From<Target> for TargetInfo { impl From<Target> for TargetInfo {
fn from(target: Target) -> Self { fn from(target: Target) -> Self {
TargetInfo { (&target).into()
kind: target.kind(),
lib: target.lib_path().is_some(),
bin: target.bin_path().is_some(),
}
} }
} }
@ -62,4 +63,8 @@ pub struct PackageResponse {
pub published_at: DateTime<Utc>, pub published_at: DateTime<Utc>,
#[serde(skip_serializing_if = "String::is_empty")] #[serde(skip_serializing_if = "String::is_empty")]
pub license: String, pub license: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repository: Option<String>,
} }

View file

@ -291,14 +291,19 @@ impl PublishCommand {
); );
println!( println!(
"authors: {}", "authors: {}",
manifest if manifest.authors.is_empty() {
.authors "(none)".to_string()
.as_ref() } else {
.map_or("(none)".to_string(), |a| a.join(", ")) manifest.authors.join(", ")
}
); );
println!( println!(
"repository: {}", "repository: {}",
manifest.repository.as_deref().unwrap_or("(none)") manifest
.repository
.as_ref()
.map(|r| r.as_str())
.unwrap_or("(none)")
); );
let roblox_target = roblox_target.is_some_and(|_| true); let roblox_target = roblox_target.is_some_and(|_| true);

View file

@ -151,9 +151,7 @@ fn run() -> anyhow::Result<()> {
if let Some(token) = token { if let Some(token) = token {
headers.insert( headers.insert(
reqwest::header::AUTHORIZATION, reqwest::header::AUTHORIZATION,
format!("Bearer {token}") token.parse().context("failed to create auth header")?,
.parse()
.context("failed to create auth header")?,
); );
} }

View file

@ -29,11 +29,11 @@ pub struct Manifest {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>, pub license: Option<String>,
/// The authors of the package /// The authors of the package
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authors: Option<Vec<String>>, pub authors: Vec<String>,
/// The repository of the package /// The repository of the package
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<String>, pub repository: Option<url::Url>,
/// The target of the package /// The target of the package
pub target: Target, pub target: Target,
/// Whether the package is private /// Whether the package is private

View file

@ -280,8 +280,7 @@ impl Project {
pkg_ref.dependencies().clone() pkg_ref.dependencies().clone()
{ {
if dependency_ty == DependencyType::Dev { if dependency_ty == DependencyType::Dev {
// dev dependencies of dependencies are not included in the graph // dev dependencies of dependencies are to be ignored
// they should not even be stored in the index, so this is just a check to avoid potential issues
continue; continue;
} }

View file

@ -402,6 +402,12 @@ pub struct IndexFileEntry {
/// The license of this package /// The license of this package
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>, pub license: Option<String>,
/// The authors of this package
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authors: Vec<String>,
/// The repository of this package
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repository: Option<url::Url>,
/// The dependencies of this package /// The dependencies of this package
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")] #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]