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 ") {
token[7..].to_string()
} else {
token.to_string()
} else {
format!("Bearer {token}")
};
let response = match app_state
.reqwest_client
.get("https://api.github.com/user")
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
.header(reqwest::header::AUTHORIZATION, token)
.send()
.await
.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::{
error::Error,
package::{s3_name, PackageResponse, S3_SIGN_DURATION},
AppState,
};
use actix_web::{
http::header::{ACCEPT, LOCATION},
web, HttpRequest, HttpResponse, Responder,
};
use pesde::{
manifest::target::TargetKind,
names::PackageName,
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)]
pub enum VersionRequest {
@ -68,29 +70,27 @@ pub async fn get_package_version(
return Ok(HttpResponse::NotFound().finish());
};
if request
let accept = request
.headers()
.get(ACCEPT)
.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(
&app_state.s3_bucket,
Some(&app_state.s3_credentials),
&s3_name(&name, &v_id),
&s3_name(&name, &v_id, readme),
)
.sign(S3_SIGN_DURATION);
return Ok(HttpResponse::Ok().body(
app_state
.reqwest_client
.get(object_url)
.send()
.await?
.error_for_status()?
.bytes()
.await?,
));
return Ok(HttpResponse::TemporaryRedirect()
.append_header((LOCATION, object_url.as_str()))
.finish());
}
Ok(HttpResponse::Ok().json(PackageResponse {
@ -100,5 +100,7 @@ pub async fn get_package_version(
description: entry.description.clone().unwrap_or_default(),
published_at: entry.published_at,
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(),
published_at: entry.published_at,
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());

View file

@ -8,19 +8,12 @@ use actix_web::{web, HttpResponse, Responder};
use flate2::read::GzDecoder;
use futures::StreamExt;
use git2::{Remote, Repository, Signature};
use reqwest::header::{CONTENT_ENCODING, CONTENT_TYPE};
use rusty_s3::{actions::PutObject, S3Action};
use tar::Archive;
use crate::{
auth::UserId,
benv,
error::{Error, ErrorResponse},
package::{s3_name, S3_SIGN_DURATION},
search::update_version,
AppState,
};
use pesde::{
manifest::{DependencyType, Manifest},
manifest::Manifest,
source::{
git_index::GitBasedSource,
pesde::{IndexFile, IndexFileEntry, ScopeInfo, SCOPE_INFO_FILE},
@ -31,6 +24,15 @@ use pesde::{
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> {
Signature::now(
&benv!(required "COMMITTER_GIT_NAME"),
@ -80,6 +82,7 @@ pub async fn publish_package(
let entries = archive.entries()?;
let mut manifest = None::<Manifest>;
let mut readme = None::<Vec<u8>>;
for entry in entries {
let mut entry = entry?;
@ -107,6 +110,21 @@ pub async fn publish_package(
let mut content = String::new();
entry.read_to_string(&mut content)?;
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,10 +139,10 @@ pub async fn publish_package(
let dependencies = manifest
.all_dependencies()
.map_err(|_| Error::InvalidArchive)?
.into_iter()
.filter_map(|(alias, (specifier, ty))| {
match &specifier {
.map_err(|_| Error::InvalidArchive)?;
for (specifier, _) in dependencies.values() {
match specifier {
DependencySpecifiers::Pesde(specifier) => {
if specifier
.index
@ -136,19 +154,19 @@ pub async fn publish_package(
})
.is_none()
{
return Some(Err(Error::InvalidArchive));
return Err(Error::InvalidArchive);
}
let (dep_scope, dep_name) = specifier.name.as_str();
match source.read_file([dep_scope, dep_name], &app_state.project, None) {
Ok(Some(_)) => {}
Ok(None) => return Some(Err(Error::InvalidArchive)),
Err(e) => return Some(Err(e.into())),
Ok(None) => return Err(Error::InvalidArchive),
Err(e) => return Err(e.into()),
}
}
DependencySpecifiers::Wally(specifier) => {
if !config.wally_allowed {
return Some(Err(Error::InvalidArchive));
return Err(Error::InvalidArchive);
}
if specifier
@ -157,23 +175,16 @@ pub async fn publish_package(
.filter(|index| index.parse::<url::Url>().is_ok())
.is_none()
{
return Some(Err(Error::InvalidArchive));
return Err(Error::InvalidArchive);
}
}
DependencySpecifiers::Git(_) => {
if !config.git_allowed {
return Some(Err(Error::InvalidArchive));
return Err(Error::InvalidArchive);
}
}
}
};
if ty == DependencyType::Dev {
return None;
}
Some(Ok((alias, (specifier, ty))))
})
.collect::<Result<_, Error>>()?;
let repo = source.repo_git2(&app_state.project)?;
@ -209,6 +220,8 @@ pub async fn publish_package(
published_at: chrono::Utc::now(),
description: manifest.description.clone(),
license: manifest.license.clone(),
authors: manifest.authors.clone(),
repository: manifest.repository.clone(),
dependencies,
};
@ -219,10 +232,12 @@ pub async fn publish_package(
if let Some(this_version) = this_version {
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?
// the others cannot be different because what to return from the versions endpoint?
if other_entry.description != new_entry.description
|| other_entry.license != new_entry.license
|| other_entry.authors != new_entry.authors
|| other_entry.repository != new_entry.repository
{
return Ok(HttpResponse::BadRequest().json(ErrorResponse {
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);
}
let version_id = VersionId::new(manifest.version.clone(), manifest.target.kind());
let object_url = PutObject::new(
&app_state.s3_bucket,
Some(&app_state.s3_credentials),
&s3_name(
&manifest.name,
&VersionId::new(manifest.version.clone(), manifest.target.kind()),
),
&s3_name(&manifest.name, &version_id, false),
)
.sign(S3_SIGN_DURATION);
app_state
.reqwest_client
.put(object_url)
.header(CONTENT_TYPE, "application/gzip")
.header(CONTENT_ENCODING, "gzip")
.body(bytes)
.send()
.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!(
"published {}@{} {}",
manifest.name, manifest.version, manifest.target

View file

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

View file

@ -7,10 +7,15 @@ use pesde::{
use serde::Serialize;
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 {
format!("{}+{}.tar.gz", name.escaped(), version_id.escaped())
pub fn s3_name(name: &PackageName, version_id: &VersionId, is_readme: bool) -> String {
format!(
"{}+{}{}",
name.escaped(),
version_id.escaped(),
if is_readme { "+readme.gz" } else { ".tar.gz" }
)
}
#[derive(Debug, Serialize, Eq, PartialEq)]
@ -22,11 +27,7 @@ pub struct 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(),
}
(&target).into()
}
}
@ -62,4 +63,8 @@ pub struct PackageResponse {
pub published_at: DateTime<Utc>,
#[serde(skip_serializing_if = "String::is_empty")]
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!(
"authors: {}",
manifest
.authors
.as_ref()
.map_or("(none)".to_string(), |a| a.join(", "))
if manifest.authors.is_empty() {
"(none)".to_string()
} else {
manifest.authors.join(", ")
}
);
println!(
"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);

View file

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

View file

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

View file

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

View file

@ -402,6 +402,12 @@ pub struct IndexFileEntry {
/// The license of this package
#[serde(default, skip_serializing_if = "Option::is_none")]
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
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]