feat: implement registry

This commit is contained in:
daimond113 2024-07-30 12:37:54 +02:00
parent ea887e56ef
commit c481826d77
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
24 changed files with 2678 additions and 105 deletions

1430
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -67,7 +67,7 @@ sha2 = "0.10.8"
git2 = { version = "0.19.0", optional = true } git2 = { version = "0.19.0", optional = true }
zip = { version = "2.1.5", optional = true } zip = { version = "2.1.5", optional = true }
serde_json = { version = "1.0.120", optional = true } serde_json = { version = "1.0.121", optional = true }
anyhow = { version = "1.0.86", optional = true } anyhow = { version = "1.0.86", optional = true }
open = { version = "5.3.0", optional = true } open = { version = "5.3.0", optional = true }
@ -86,7 +86,7 @@ winreg = { version = "0.52.0", optional = true }
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [] members = ["registry"]
[profile.dev.package.full_moon] [profile.dev.package.full_moon]
opt-level = 3 opt-level = 3

View file

@ -13,14 +13,13 @@ kill_timeout = '5s'
[env] [env]
ADDRESS = '0.0.0.0' ADDRESS = '0.0.0.0'
PORT = '8080' PORT = '8080'
INDEX_REPO_URL = 'https://github.com/daimond113/pesde-index' COMMITTER_GIT_NAME = 'pesde index updater'
COMMITTER_GIT_NAME = 'Pesde Index Updater'
COMMITTER_GIT_EMAIL = 'pesde@daimond113.com' COMMITTER_GIT_EMAIL = 'pesde@daimond113.com'
[http_service] [http_service]
internal_port = 8080 internal_port = 8080
force_https = true force_https = true
auto_stop_machines = true auto_stop_machines = "suspend"
auto_start_machines = true auto_start_machines = true
min_machines_running = 0 min_machines_running = 0
processes = ['app'] processes = ['app']

0
registry/.env.example Normal file
View file

40
registry/Cargo.toml Normal file
View file

@ -0,0 +1,40 @@
[package]
name = "pesde-registry"
version = "0.7.0"
edition = "2021"
repository = "https://github.com/daimond113/pesde-index"
publish = false
[dependencies]
actix-web = "4.8.0"
actix-web-lab = "0.20.2"
actix-multipart = { version = "0.7.2", features = ["derive"] }
actix-cors = "0.7.0"
actix-governor = "0.5.0"
dotenvy = "0.15.7"
thiserror = "1.0.63"
tantivy = "0.22.0"
semver = "1.0.23"
chrono = { version = "0.4.38", features = ["serde"] }
git2 = "0.19.0"
gix = { version = "0.64.0", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls", "credentials"] }
serde = "1.0.204"
serde_json = "1.0.121"
toml = "0.8.16"
rusty-s3 = "0.5.0"
reqwest = { version = "0.12.5", features = ["json", "rustls-tls"] }
tar = "0.4.41"
flate2 = "1.0.30"
log = "0.4.22"
pretty_env_logger = "0.5.0"
sentry = "0.34.0"
sentry-log = "0.34.0"
sentry-actix = "0.34.0"
pesde = { path = "..", features = ["roblox", "lune", "luau", "wally-compat", "git2"] }

95
registry/src/auth.rs Normal file
View file

@ -0,0 +1,95 @@
use crate::AppState;
use actix_governor::{KeyExtractor, SimpleKeyExtractionError};
use actix_web::{
body::MessageBody,
dev::{ServiceRequest, ServiceResponse},
error::Error as ActixError,
http::header::AUTHORIZATION,
web, HttpMessage, HttpResponse,
};
use actix_web_lab::middleware::Next;
use serde::Deserialize;
#[derive(Debug, Copy, Clone, Hash, PartialOrd, PartialEq, Eq, Ord)]
pub struct UserId(pub u64);
#[derive(Debug, Deserialize)]
struct UserResponse {
id: u64,
}
pub async fn authentication(
app_state: web::Data<AppState>,
req: ServiceRequest,
next: Next<impl MessageBody + 'static>,
) -> Result<ServiceResponse<impl MessageBody>, ActixError> {
let token = match req
.headers()
.get(AUTHORIZATION)
.map(|token| token.to_str().unwrap())
{
Some(token) => token,
None => {
return Ok(req
.into_response(HttpResponse::Unauthorized().finish())
.map_into_right_body())
}
};
let token = if token.to_lowercase().starts_with("bearer ") {
token[7..].to_string()
} else {
token.to_string()
};
let response = match app_state
.reqwest_client
.get("https://api.github.com/user")
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
.send()
.await
.and_then(|res| res.error_for_status())
{
Ok(response) => response,
Err(e) if e.status() == Some(reqwest::StatusCode::UNAUTHORIZED) => {
return Ok(req
.into_response(HttpResponse::Unauthorized().finish())
.map_into_right_body())
}
Err(e) => {
log::error!("failed to get user: {e}");
return Ok(req
.into_response(HttpResponse::InternalServerError().finish())
.map_into_right_body());
}
};
let user_id = match response.json::<UserResponse>().await {
Ok(user) => user.id,
Err(_) => {
return Ok(req
.into_response(HttpResponse::Unauthorized().finish())
.map_into_right_body())
}
};
req.extensions_mut().insert(UserId(user_id));
let res = next.call(req).await?;
Ok(res.map_into_left_body())
}
#[derive(Debug, Clone)]
pub struct UserIdExtractor;
impl KeyExtractor for UserIdExtractor {
type Key = UserId;
type KeyExtractionError = SimpleKeyExtractionError<&'static str>;
fn extract(&self, req: &ServiceRequest) -> Result<Self::Key, Self::KeyExtractionError> {
match req.extensions().get::<UserId>() {
Some(user_id) => Ok(*user_id),
None => Err(SimpleKeyExtractionError::new("UserId not found")),
}
}
}

View file

@ -0,0 +1,4 @@
pub mod package_version;
pub mod package_versions;
pub mod publish_version;
pub mod search;

View file

@ -0,0 +1,146 @@
use std::str::FromStr;
use actix_web::{http::header::ACCEPT, web, HttpRequest, HttpResponse, Responder};
use rusty_s3::{actions::GetObject, S3Action};
use semver::Version;
use serde::{Deserialize, Deserializer};
use pesde::{manifest::target::TargetKind, names::PackageName, source::pesde::IndexFile};
use crate::{
error::Error,
package::{s3_name, PackageResponse, S3_SIGN_DURATION},
AppState,
};
#[derive(Debug)]
pub enum VersionRequest {
Latest,
Specific(Version),
}
impl<'de> Deserialize<'de> for VersionRequest {
fn deserialize<D>(deserializer: D) -> Result<VersionRequest, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if s.eq_ignore_ascii_case("latest") {
return Ok(VersionRequest::Latest);
}
Version::parse(&s)
.map(VersionRequest::Specific)
.map_err(serde::de::Error::custom)
}
}
#[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)>,
) -> Result<impl Responder, Error> {
let (name, version, target) = path.into_inner();
let (scope, name_part) = name.as_str();
let versions: IndexFile = {
let source = app_state.source.lock().unwrap();
match source.read_file([scope, name_part], &app_state.project)? {
Some(versions) => toml::de::from_str(&versions)?,
None => return Ok(HttpResponse::NotFound().finish()),
}
};
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,
}
}),
};
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)
.and_then(|accept| accept.to_str().ok())
.is_some_and(|accept| accept.eq_ignore_ascii_case("application/octet-stream"))
{
let object_url = GetObject::new(
&app_state.s3_bucket,
Some(&app_state.s3_credentials),
&s3_name(&name, v_id),
)
.sign(S3_SIGN_DURATION);
return Ok(HttpResponse::Ok().body(
app_state
.reqwest_client
.get(object_url)
.send()
.await?
.error_for_status()?
.bytes()
.await?,
));
}
let entry = entry.clone();
let mut response = serde_json::to_value(PackageResponse {
name: name.to_string(),
version: v_id.version().to_string(),
target: Some(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))
}

View file

@ -0,0 +1,34 @@
use actix_web::{web, HttpResponse, Responder};
use pesde::{names::PackageName, source::pesde::IndexFile};
use crate::{error::Error, package::PackageResponse, AppState};
pub async fn get_package_versions(
app_state: web::Data<AppState>,
path: web::Path<PackageName>,
) -> Result<impl Responder, Error> {
let name = path.into_inner();
let (scope, name_part) = name.as_str();
let source = app_state.source.lock().unwrap();
let versions: IndexFile = match source.read_file([scope, name_part], &app_state.project)? {
Some(versions) => toml::de::from_str(&versions)?,
None => return Ok(HttpResponse::NotFound().finish()),
};
Ok(HttpResponse::Ok().json(
versions
.into_iter()
.map(|(v_id, entry)| PackageResponse {
name: name.to_string(),
version: v_id.version().to_string(),
target: Some(entry.target.into()),
description: entry.description.unwrap_or_default(),
published_at: entry.published_at,
license: entry.license.unwrap_or_default(),
})
.collect::<Vec<_>>(),
))
}

View file

@ -0,0 +1,270 @@
use std::{
collections::BTreeSet,
io::{Cursor, Read, Write},
};
use actix_multipart::form::{bytes::Bytes, MultipartForm};
use actix_web::{web, HttpResponse, Responder};
use flate2::read::GzDecoder;
use git2::{Remote, Repository, Signature};
use rusty_s3::{actions::PutObject, S3Action};
use tar::Archive;
use pesde::{
manifest::Manifest,
source::{
pesde::{IndexFile, IndexFileEntry, ScopeInfo, SCOPE_INFO_FILE},
specifiers::DependencySpecifiers,
traits::PackageSource,
version_id::VersionId,
},
DEFAULT_INDEX_NAME, MANIFEST_FILE_NAME,
};
use crate::{
auth::UserId,
benv,
error::Error,
package::{s3_name, S3_SIGN_DURATION},
search::update_version,
AppState,
};
#[derive(MultipartForm)]
pub struct PublishBody {
#[multipart(limit = "4 MiB")]
tarball: Bytes,
}
fn signature<'a>() -> Signature<'a> {
Signature::now(
&benv!(required "COMMITTER_GIT_NAME"),
&benv!(required "COMMITTER_GIT_EMAIL"),
)
.unwrap()
}
fn get_refspec(repo: &Repository, remote: &mut Remote) -> Result<String, git2::Error> {
let upstream_branch_buf = repo.branch_upstream_name(repo.head()?.name().unwrap())?;
let upstream_branch = upstream_branch_buf.as_str().unwrap();
let refspec_buf = remote
.refspecs()
.find(|r| r.direction() == git2::Direction::Fetch && r.dst_matches(upstream_branch))
.unwrap()
.rtransform(upstream_branch)?;
let refspec = refspec_buf.as_str().unwrap();
Ok(refspec.to_string())
}
const FORBIDDEN_FILES: &[&str] = &[".DS_Store", "default.project.json"];
const FORBIDDEN_DIRECTORIES: &[&str] = &[".git"];
pub async fn publish_package(
app_state: web::Data<AppState>,
body: MultipartForm<PublishBody>,
user_id: web::ReqData<UserId>,
) -> Result<impl Responder, Error> {
let bytes = body.tarball.data.to_vec();
let mut decoder = GzDecoder::new(Cursor::new(&bytes));
let mut archive = Archive::new(&mut decoder);
let entries = archive.entries()?;
let mut manifest = None::<Manifest>;
for entry in entries {
let mut entry = entry?;
let path = entry.path()?;
let path = path.to_str().ok_or(Error::InvalidArchive)?;
if entry.header().entry_type().is_dir() {
if FORBIDDEN_DIRECTORIES.contains(&path) {
return Err(Error::InvalidArchive);
}
continue;
}
if FORBIDDEN_FILES.contains(&path) {
return Err(Error::InvalidArchive);
}
if path == MANIFEST_FILE_NAME {
let mut content = String::new();
entry.read_to_string(&mut content)?;
manifest = Some(toml::de::from_str(&content).map_err(|_| Error::InvalidArchive)?);
}
}
let Some(manifest) = manifest else {
return Err(Error::InvalidArchive);
};
{
let source = app_state.source.lock().unwrap();
source.refresh(&app_state.project).map_err(Box::new)?;
let config = source.config(&app_state.project)?;
if manifest
.indices
.get(DEFAULT_INDEX_NAME)
.filter(|index_url| *index_url == source.repo_url())
.is_none()
{
return Err(Error::InvalidArchive);
}
let dependencies = manifest
.all_dependencies()
.map_err(|_| Error::InvalidArchive)?;
for (specifier, _) in dependencies.values() {
match specifier {
DependencySpecifiers::Pesde(specifier) => {
if specifier
.index
.as_ref()
.is_some_and(|index| index != DEFAULT_INDEX_NAME)
&& !config.other_registries_allowed
{
return Err(Error::InvalidArchive);
}
let (dep_scope, dep_name) = specifier.name.as_str();
if source
.read_file([dep_scope, dep_name], &app_state.project)?
.is_none()
{
return Err(Error::InvalidArchive);
}
}
};
}
let repo = source.repo_git2(&app_state.project)?;
let (scope, name) = manifest.name.as_str();
let mut oids = vec![];
match source.read_file([scope, SCOPE_INFO_FILE], &app_state.project)? {
Some(info) => {
let info: ScopeInfo = toml::de::from_str(&info)?;
if !info.owners.contains(&user_id.0) {
return Ok(HttpResponse::Forbidden().finish());
}
}
None => {
let scope_info = toml::to_string(&ScopeInfo {
owners: BTreeSet::from([user_id.0]),
})?;
let mut blob_writer = repo.blob_writer(None)?;
blob_writer.write_all(scope_info.as_bytes())?;
oids.push((SCOPE_INFO_FILE, blob_writer.commit()?));
}
};
let mut entries: IndexFile = toml::de::from_str(
&source
.read_file([scope, name], &app_state.project)?
.unwrap_or_default(),
)?;
let new_entry = IndexFileEntry {
target: manifest.target.clone(),
published_at: chrono::Utc::now(),
description: manifest.description.clone(),
license: manifest.license.clone(),
dependencies,
};
if entries
.insert(
VersionId::new(manifest.version.clone(), manifest.target.kind()),
new_entry.clone(),
)
.is_some()
{
return Ok(HttpResponse::Conflict().finish());
}
let mut remote = repo.find_remote("origin")?;
let refspec = get_refspec(&repo, &mut remote)?;
let reference = repo.find_reference(&refspec)?;
{
let index_content = toml::to_string(&entries)?;
let mut blob_writer = repo.blob_writer(None)?;
blob_writer.write_all(index_content.as_bytes())?;
oids.push((name, blob_writer.commit()?));
}
let old_root_tree = reference.peel_to_tree()?;
let old_scope_tree = match old_root_tree.get_name(scope) {
Some(entry) => Some(repo.find_tree(entry.id())?),
None => None,
};
let mut scope_tree = repo.treebuilder(old_scope_tree.as_ref())?;
for (file, oid) in oids {
scope_tree.insert(file, oid, 0o100644)?;
}
let scope_tree_id = scope_tree.write()?;
let mut root_tree = repo.treebuilder(Some(&repo.find_tree(old_root_tree.id())?))?;
root_tree.insert(scope, scope_tree_id, 0o040000)?;
let tree_oid = root_tree.write()?;
repo.commit(
Some("HEAD"),
&signature(),
&signature(),
&format!(
"add {}@{} {}",
manifest.name, manifest.version, manifest.target
),
&repo.find_tree(tree_oid)?,
&[&reference.peel_to_commit()?],
)?;
let mut push_options = git2::PushOptions::new();
let mut remote_callbacks = git2::RemoteCallbacks::new();
let git_creds = app_state.project.auth_config().git_credentials().unwrap();
remote_callbacks.credentials(|_, _, _| {
git2::Cred::userpass_plaintext(&git_creds.username, &git_creds.password)
});
push_options.remote_callbacks(remote_callbacks);
remote.push(&[refspec], Some(&mut push_options))?;
update_version(&app_state, &manifest.name, new_entry);
}
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()),
),
)
.sign(S3_SIGN_DURATION);
app_state
.reqwest_client
.put(object_url)
.body(bytes)
.send()
.await?;
Ok(HttpResponse::Ok().body(format!(
"published {}@{} {}",
manifest.name, manifest.version, manifest.target
)))
}

View file

@ -0,0 +1,94 @@
use std::collections::HashMap;
use actix_web::{web, HttpResponse, Responder};
use serde::Deserialize;
use tantivy::{query::AllQuery, schema::Value, DateTime, Order};
use pesde::{names::PackageName, source::pesde::IndexFile};
use crate::{error::Error, package::PackageResponse, AppState};
#[derive(Deserialize)]
pub struct Request {
#[serde(default)]
query: Option<String>,
#[serde(default)]
offset: Option<usize>,
}
pub async fn search_packages(
app_state: web::Data<AppState>,
request: web::Query<Request>,
) -> Result<impl Responder, Error> {
let searcher = app_state.search_reader.searcher();
let schema = searcher.schema();
let id = schema.get_field("id").unwrap();
let scope = schema.get_field("scope").unwrap();
let name = schema.get_field("name").unwrap();
let description = schema.get_field("description").unwrap();
let query = request.query.as_deref().unwrap_or_default().trim();
let query = if query.is_empty() {
Box::new(AllQuery)
} else {
let mut query_parser = tantivy::query::QueryParser::for_index(
searcher.index(),
vec![scope, name, description],
);
query_parser.set_field_boost(scope, 2.0);
query_parser.set_field_boost(name, 3.5);
query_parser.parse_query(query)?
};
let top_docs = searcher
.search(
&query,
&tantivy::collector::TopDocs::with_limit(50)
.and_offset(request.offset.unwrap_or_default())
.order_by_fast_field::<DateTime>("published_at", Order::Desc),
)
.unwrap();
let source = app_state.source.lock().unwrap();
let top_docs = top_docs
.into_iter()
.map(|(_, doc_address)| {
let doc = searcher.doc::<HashMap<_, _>>(doc_address).unwrap();
let id = doc
.get(&id)
.unwrap()
.as_str()
.unwrap()
.parse::<PackageName>()
.unwrap();
let (scope, name) = id.as_str();
let mut versions: IndexFile = toml::de::from_str(
&source
.read_file([scope, name], &app_state.project)
.unwrap()
.unwrap(),
)
.unwrap();
let (version_id, entry) = versions.pop_last().unwrap();
PackageResponse {
name: id.to_string(),
version: version_id.version().to_string(),
target: None,
description: entry.description.unwrap_or_default(),
published_at: entry.published_at,
license: entry.license.unwrap_or_default(),
}
})
.collect::<Vec<_>>();
Ok(HttpResponse::Ok().json(top_docs))
}

63
registry/src/error.rs Normal file
View file

@ -0,0 +1,63 @@
use actix_web::{body::BoxBody, HttpResponse, ResponseError};
use log::error;
use pesde::source::pesde::errors::ReadFile;
use serde::Serialize;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("failed to parse query")]
Query(#[from] tantivy::query::QueryParserError),
#[error("error reading repo file")]
ReadFile(#[from] ReadFile),
#[error("error deserializing file")]
Deserialize(#[from] toml::de::Error),
#[error("error sending request")]
Reqwest(#[from] reqwest::Error),
#[error("failed to parse archive entries")]
Tar(#[from] std::io::Error),
#[error("invalid archive")]
InvalidArchive,
#[error("failed to read index config")]
Config(#[from] pesde::source::pesde::errors::ConfigError),
#[error("git error")]
Git(#[from] git2::Error),
#[error("failed to refresh source")]
Refresh(#[from] Box<pesde::source::pesde::errors::RefreshError>),
#[error("failed to serialize struct")]
Serialize(#[from] toml::ser::Error),
#[error("failed to serialize struct")]
SerializeJson(#[from] serde_json::Error),
}
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub error: String,
}
impl ResponseError for Error {
fn error_response(&self) -> HttpResponse<BoxBody> {
match self {
Error::Query(e) => HttpResponse::BadRequest().json(ErrorResponse {
error: format!("failed to parse query: {e}"),
}),
Error::Tar(_) | Error::InvalidArchive => HttpResponse::BadRequest().json(ErrorResponse {
error: "invalid archive. ensure it has all the required files, and all the dependencies exist in the registry.".to_string(),
}),
e => {
log::error!("unhandled error: {e:?}");
HttpResponse::InternalServerError().finish()
}
}
}
}

222
registry/src/main.rs Normal file
View file

@ -0,0 +1,222 @@
use std::{env::current_dir, fs::create_dir_all, sync::Mutex};
use actix_cors::Cors;
use actix_governor::{Governor, GovernorConfigBuilder};
use actix_web::{
middleware::{Compress, Condition, Logger, NormalizePath, TrailingSlash},
rt::System,
web, App, HttpServer,
};
use actix_web_lab::middleware::from_fn;
use log::info;
use rusty_s3::{Bucket, Credentials, UrlStyle};
use pesde::{
source::{pesde::PesdePackageSource, traits::PackageSource},
AuthConfig, Project,
};
use crate::{auth::UserIdExtractor, search::make_search};
mod auth;
mod endpoints;
mod error;
mod package;
mod search;
pub struct AppState {
pub s3_bucket: Bucket,
pub s3_credentials: Credentials,
pub source: Mutex<PesdePackageSource>,
pub project: Project,
pub reqwest_client: reqwest::Client,
pub search_reader: tantivy::IndexReader,
pub search_writer: Mutex<tantivy::IndexWriter>,
}
#[macro_export]
macro_rules! benv {
($name:expr) => {
std::env::var($name)
};
($name:expr => $default:expr) => {
benv!($name).unwrap_or($default.to_string())
};
(required $name:expr) => {
benv!($name).expect(concat!("Environment variable `", $name, "` must be set"))
};
(parse $name:expr) => {
benv!($name)
.map(|v| v.parse().expect(concat!(
"Environment variable `",
$name,
"` must be a valid value"
)))
};
(parse required $name:expr) => {
benv!(parse $name).expect(concat!("Environment variable `", $name, "` must be set"))
};
(parse $name:expr => $default:expr) => {
benv!($name => $default)
.parse()
.expect(concat!(
"Environment variable `",
$name,
"` must a valid value"
))
};
}
async fn run(with_sentry: bool) -> std::io::Result<()> {
let address = benv!("ADDRESS" => "127.0.0.1");
let port: u16 = benv!(parse "PORT" => "8080");
let cwd = current_dir().unwrap();
let data_dir = cwd.join("data");
create_dir_all(&data_dir).unwrap();
let project = Project::new(
&cwd,
data_dir.join("project"),
&cwd,
AuthConfig::new().with_git_credentials(Some(gix::sec::identity::Account {
username: benv!(required "GITHUB_USERNAME"),
password: benv!(required "GITHUB_PAT"),
})),
);
let source = PesdePackageSource::new(env!("CARGO_PKG_REPOSITORY").try_into().unwrap());
source.refresh(&project).expect("failed to refresh source");
let (search_reader, search_writer) = make_search(&project, &source);
let app_data = web::Data::new(AppState {
s3_bucket: Bucket::new(
benv!(parse required "S3_ENDPOINT"),
UrlStyle::Path,
benv!(required "S3_BUCKET_NAME"),
benv!(required "S3_REGION"),
)
.unwrap(),
s3_credentials: Credentials::new(
benv!(required "S3_ACCESS_KEY"),
benv!(required "S3_SECRET_KEY"),
),
source: Mutex::new(source),
project,
reqwest_client: reqwest::ClientBuilder::new()
.user_agent(concat!(
env!("CARGO_PKG_NAME"),
"/",
env!("CARGO_PKG_VERSION")
))
.build()
.unwrap(),
search_reader,
search_writer: Mutex::new(search_writer),
});
let generic_governor_config = GovernorConfigBuilder::default()
.burst_size(500)
.per_millisecond(500)
.use_headers()
.finish()
.unwrap();
let publish_governor_config = GovernorConfigBuilder::default()
.key_extractor(UserIdExtractor)
.burst_size(12)
.per_second(60)
.use_headers()
.finish()
.unwrap();
info!("listening on {address}:{port}");
HttpServer::new(move || {
App::new()
.wrap(Condition::new(with_sentry, sentry_actix::Sentry::new()))
.wrap(NormalizePath::new(TrailingSlash::Trim))
.wrap(Cors::permissive())
.wrap(Logger::default())
.wrap(Compress::default())
.app_data(app_data.clone())
.route(
"/",
web::get().to(|| async {
concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"))
}),
)
.service(
web::scope("/v0")
.route(
"/search",
web::get()
.to(endpoints::search::search_packages)
.wrap(Governor::new(&generic_governor_config)),
)
.route(
"/packages/{name}",
web::get()
.to(endpoints::package_versions::get_package_versions)
.wrap(Governor::new(&generic_governor_config)),
)
.route(
"/packages/{name}/{version}/{target}",
web::get()
.to(endpoints::package_version::get_package_version)
.wrap(Governor::new(&generic_governor_config)),
)
.route(
"/packages",
web::post()
.to(endpoints::publish_version::publish_package)
.wrap(Governor::new(&publish_governor_config))
.wrap(from_fn(auth::authentication)),
),
)
})
.bind((address, port))?
.run()
.await
}
// can't use #[actix_web::main] because of Sentry:
// "Note: Macros like #[tokio::main] and #[actix_web::main] are not supported. The Sentry client must be initialized before the async runtime is started so that all threads are correctly connected to the Hub."
// https://docs.sentry.io/platforms/rust/guides/actix-web/
fn main() -> std::io::Result<()> {
let _ = dotenvy::dotenv();
let sentry_url = benv!("SENTRY_URL").ok();
let with_sentry = sentry_url.is_some();
let mut log_builder = pretty_env_logger::formatted_builder();
log_builder.parse_env(pretty_env_logger::env_logger::Env::default().default_filter_or("info"));
if with_sentry {
let logger = sentry_log::SentryLogger::with_dest(log_builder.build());
log::set_boxed_logger(Box::new(logger)).unwrap();
log::set_max_level(log::LevelFilter::Info);
} else {
log_builder.try_init().unwrap();
}
let _guard = if let Some(sentry_url) = sentry_url {
std::env::set_var("RUST_BACKTRACE", "1");
Some(sentry::init((
sentry_url,
sentry::ClientOptions {
release: sentry::release_name!(),
..Default::default()
},
)))
} else {
None
};
System::new().block_on(run(with_sentry))
}

44
registry/src/package.rs Normal file
View file

@ -0,0 +1,44 @@
use chrono::{DateTime, Utc};
use pesde::{
manifest::target::{Target, TargetKind},
names::PackageName,
source::version_id::VersionId,
};
use serde::Serialize;
use std::time::Duration;
pub const S3_SIGN_DURATION: Duration = Duration::from_secs(60 * 60);
pub fn s3_name(name: &PackageName, version_id: &VersionId) -> String {
format!("{}+{}.tar.gz", name.escaped(), version_id.escaped())
}
#[derive(Debug, Serialize)]
pub struct TargetInfo {
kind: TargetKind,
lib: bool,
bin: bool,
}
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(),
}
}
}
#[derive(Debug, Serialize)]
pub struct PackageResponse {
pub name: String,
pub version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<TargetInfo>,
#[serde(skip_serializing_if = "String::is_empty")]
pub description: String,
pub published_at: DateTime<Utc>,
#[serde(skip_serializing_if = "String::is_empty")]
pub license: String,
}

79
registry/src/search.rs Normal file
View file

@ -0,0 +1,79 @@
use crate::AppState;
use pesde::{
names::PackageName,
source::pesde::{IndexFileEntry, PesdePackageSource},
Project,
};
use tantivy::{
doc,
schema::{IndexRecordOption, TextFieldIndexing, TextOptions, FAST, STORED, STRING},
DateTime, IndexReader, IndexWriter, Term,
};
pub fn make_search(project: &Project, source: &PesdePackageSource) -> (IndexReader, IndexWriter) {
let mut schema_builder = tantivy::schema::SchemaBuilder::new();
let field_options = TextOptions::default().set_indexing_options(
TextFieldIndexing::default()
.set_tokenizer("ngram")
.set_index_option(IndexRecordOption::WithFreqsAndPositions),
);
let id_field = schema_builder.add_text_field("id", STRING | STORED);
let scope = schema_builder.add_text_field("scope", field_options.clone());
let name = schema_builder.add_text_field("name", field_options.clone());
let description = schema_builder.add_text_field("description", field_options);
let published_at = schema_builder.add_date_field("published_at", FAST);
let search_index = tantivy::Index::create_in_ram(schema_builder.build());
search_index.tokenizers().register(
"ngram",
tantivy::tokenizer::NgramTokenizer::all_ngrams(1, 12).unwrap(),
);
let search_reader = search_index
.reader_builder()
.reload_policy(tantivy::ReloadPolicy::Manual)
.try_into()
.unwrap();
let mut search_writer = search_index.writer(50_000_000).unwrap();
for (pkg_name, mut file) in source.all_packages(project).unwrap() {
let Some((_, latest_entry)) = file.pop_last() else {
log::warn!("no versions found for {pkg_name}");
continue;
};
search_writer.add_document(doc!(
id_field => pkg_name.to_string(),
scope => pkg_name.as_str().0,
name => pkg_name.as_str().1,
description => latest_entry.description.unwrap_or_default(),
published_at => DateTime::from_timestamp_secs(latest_entry.published_at.timestamp()),
)).unwrap();
}
search_writer.commit().unwrap();
search_reader.reload().unwrap();
(search_reader, search_writer)
}
pub fn update_version(app_state: &AppState, name: &PackageName, entry: IndexFileEntry) {
let mut search_writer = app_state.search_writer.lock().unwrap();
let schema = search_writer.index().schema();
let id_field = schema.get_field("id").unwrap();
search_writer.delete_term(Term::from_field_text(id_field, &name.to_string()));
search_writer.add_document(doc!(
id_field => name.to_string(),
schema.get_field("scope").unwrap() => name.as_str().0,
schema.get_field("name").unwrap() => name.as_str().1,
schema.get_field("description").unwrap() => entry.description.unwrap_or_default(),
schema.get_field("published_at").unwrap() => DateTime::from_timestamp_secs(entry.published_at.timestamp())
)).unwrap();
search_writer.commit().unwrap();
app_state.search_reader.reload().unwrap();
}

View file

@ -67,6 +67,8 @@ pub fn get_token_login(
.header("Authorization", format!("Bearer {access_token}")) .header("Authorization", format!("Bearer {access_token}"))
.send() .send()
.context("failed to send user request")? .context("failed to send user request")?
.error_for_status()
.context("failed to get user")?
.json::<UserResponse>() .json::<UserResponse>()
.context("failed to parse user response")?; .context("failed to parse user response")?;

View file

@ -102,6 +102,8 @@ impl LoginCommand {
)?) )?)
.send() .send()
.context("failed to send device code request")? .context("failed to send device code request")?
.error_for_status()
.context("failed to get device code response")?
.json::<DeviceCodeResponse>() .json::<DeviceCodeResponse>()
.context("failed to parse device code response")?; .context("failed to parse device code response")?;
@ -146,6 +148,8 @@ impl LoginCommand {
)?) )?)
.send() .send()
.context("failed to send access token request")? .context("failed to send access token request")?
.error_for_status()
.context("failed to get access token response")?
.json::<AccessTokenResponse>() .json::<AccessTokenResponse>()
.context("failed to parse access token response")?; .context("failed to parse access token response")?;

View file

@ -75,7 +75,7 @@ impl Subcommand {
Subcommand::Init(init) => init.run(project), Subcommand::Init(init) => init.run(project),
Subcommand::Run(run) => run.run(project), Subcommand::Run(run) => run.run(project),
Subcommand::Install(install) => install.run(project, multi, reqwest), Subcommand::Install(install) => install.run(project, multi, reqwest),
Subcommand::Publish(publish) => publish.run(project), Subcommand::Publish(publish) => publish.run(project, reqwest),
Subcommand::SelfInstall(self_install) => self_install.run(project), Subcommand::SelfInstall(self_install) => self_install.run(project),
#[cfg(feature = "patches")] #[cfg(feature = "patches")]
Subcommand::Patch(patch) => patch.run(project, reqwest), Subcommand::Patch(patch) => patch.run(project, reqwest),

View file

@ -2,8 +2,12 @@ use anyhow::Context;
use clap::Args; use clap::Args;
use colored::Colorize; use colored::Colorize;
use pesde::{ use pesde::{
manifest::target::Target, scripts::ScriptName, Project, MANIFEST_FILE_NAME, MAX_ARCHIVE_SIZE, manifest::target::Target,
scripts::ScriptName,
source::{pesde::PesdePackageSource, traits::PackageSource},
Project, DEFAULT_INDEX_NAME, MANIFEST_FILE_NAME, MAX_ARCHIVE_SIZE,
}; };
use reqwest::StatusCode;
use std::path::Component; use std::path::Component;
#[derive(Debug, Args)] #[derive(Debug, Args)]
@ -14,7 +18,7 @@ pub struct PublishCommand {
} }
impl PublishCommand { impl PublishCommand {
pub fn run(self, project: Project) -> anyhow::Result<()> { pub fn run(self, project: Project, reqwest: reqwest::blocking::Client) -> anyhow::Result<()> {
let mut manifest = project let mut manifest = project
.deser_manifest() .deser_manifest()
.context("failed to read manifest")?; .context("failed to read manifest")?;
@ -258,10 +262,12 @@ impl PublishCommand {
); );
if !self.dry_run && !inquire::Confirm::new("is this information correct?").prompt()? { if !self.dry_run && !inquire::Confirm::new("is this information correct?").prompt()? {
println!("{}", "publish aborted".red().bold()); println!("\n{}", "publish aborted".red().bold());
return Ok(()); return Ok(());
} }
println!();
} }
let temp_manifest_path = project let temp_manifest_path = project
@ -308,6 +314,56 @@ impl PublishCommand {
return Ok(()); return Ok(());
} }
todo!("publishing to registry"); let source = PesdePackageSource::new(
manifest
.indices
.get(DEFAULT_INDEX_NAME)
.context("missing default index")?
.clone(),
);
source
.refresh(&project)
.context("failed to refresh source")?;
let config = source
.config(&project)
.context("failed to get source config")?;
match 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}");
Ok(())
}
Err(e)
if e.status()
.is_some_and(|status| status == StatusCode::CONFLICT) =>
{
println!("{}", "package version already exists".red().bold());
Ok(())
}
Err(e)
if e.status()
.is_some_and(|status| status == StatusCode::FORBIDDEN) =>
{
println!(
"{}",
"unauthorized to publish under this scope".red().bold()
);
Ok(())
}
Err(e) => Err(e).context("failed to get response"),
}
} }
} }

View file

@ -56,6 +56,8 @@ pub fn check_for_updates(reqwest: &reqwest::blocking::Client) -> anyhow::Result<
)) ))
.send() .send()
.context("failed to send request to GitHub API")? .context("failed to send request to GitHub API")?
.error_for_status()
.context("failed to get GitHub API response")?
.json::<Vec<Release>>() .json::<Vec<Release>>()
.context("failed to parse GitHub API response")?; .context("failed to parse GitHub API response")?;
@ -108,6 +110,8 @@ pub fn download_github_release(
)) ))
.send() .send()
.context("failed to send request to GitHub API")? .context("failed to send request to GitHub API")?
.error_for_status()
.context("failed to get GitHub API response")?
.json::<Release>() .json::<Release>()
.context("failed to parse GitHub API response")?; .context("failed to parse GitHub API response")?;
@ -128,6 +132,8 @@ pub fn download_github_release(
.header(ACCEPT, "application/octet-stream") .header(ACCEPT, "application/octet-stream")
.send() .send()
.context("failed to send request to download asset")? .context("failed to send request to download asset")?
.error_for_status()
.context("failed to download asset")?
.bytes() .bytes()
.context("failed to download asset")?; .context("failed to download asset")?;

View file

@ -152,7 +152,15 @@ fn run() -> anyhow::Result<()> {
.build()? .build()?
}; };
check_for_updates(&reqwest)?; match check_for_updates(&reqwest) {
Ok(_) => {}
Err(e) => {
println!(
"{}",
format!("failed to check for updates: {e}\n\n").red().bold()
);
}
}
let target_version = project let target_version = project
.deser_manifest() .deser_manifest()

View file

@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
manifest::{overrides::OverrideKey, target::Target}, manifest::{overrides::OverrideKey, target::Target},
names::{PackageName, PackageNames}, names::PackageName,
source::{specifiers::DependencySpecifiers, version_id::VersionId}, source::specifiers::DependencySpecifiers,
}; };
pub mod overrides; pub mod overrides;
@ -50,7 +50,10 @@ pub struct Manifest {
pub includes: BTreeSet<String>, pub includes: BTreeSet<String>,
#[cfg(feature = "patches")] #[cfg(feature = "patches")]
#[serde(default, skip_serializing)] #[serde(default, skip_serializing)]
pub patches: BTreeMap<PackageNames, BTreeMap<VersionId, RelativePathBuf>>, pub patches: BTreeMap<
crate::names::PackageNames,
BTreeMap<crate::source::version_id::VersionId, RelativePathBuf>,
>,
#[serde(default, skip_serializing)] #[serde(default, skip_serializing)]
pub pesde_version: Option<Version>, pub pesde_version: Option<Version>,

View file

@ -1,10 +1,15 @@
use gix::remote::Direction; use gix::remote::Direction;
use relative_path::RelativePathBuf;
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, fmt::Debug, hash::Hash, io::Read};
use pkg_ref::PesdePackageRef; use pkg_ref::PesdePackageRef;
use relative_path::RelativePathBuf;
use reqwest::header::ACCEPT;
use serde::{Deserialize, Serialize};
use specifier::PesdeDependencySpecifier; use specifier::PesdeDependencySpecifier;
use std::{
collections::{BTreeMap, BTreeSet},
fmt::Debug,
hash::Hash,
io::Read,
};
use crate::{ use crate::{
manifest::{ manifest::{
@ -28,7 +33,12 @@ pub struct PesdePackageSource {
repo_url: gix::Url, repo_url: gix::Url,
} }
const SCOPE_INFO_FILE: &str = "scope.toml"; pub const SCOPE_INFO_FILE: &str = "scope.toml";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScopeInfo {
pub owners: BTreeSet<u64>,
}
impl PesdePackageSource { impl PesdePackageSource {
pub fn new(repo_url: gix::Url) -> Self { pub fn new(repo_url: gix::Url) -> Self {
@ -50,21 +60,21 @@ impl PesdePackageSource {
pub(crate) fn tree<'a>( pub(crate) fn tree<'a>(
&'a self, &'a self,
repo: &'a gix::Repository, repo: &'a gix::Repository,
) -> Result<gix::Tree, Box<errors::TreeError>> { ) -> Result<gix::Tree, errors::TreeError> {
// this is a bare repo, so this is the actual path // this is a bare repo, so this is the actual path
let path = repo.path().to_path_buf(); let path = repo.path().to_path_buf();
let remote = match repo.find_default_remote(Direction::Fetch) { let remote = match repo.find_default_remote(Direction::Fetch) {
Some(Ok(remote)) => remote, Some(Ok(remote)) => remote,
Some(Err(e)) => return Err(Box::new(errors::TreeError::GetDefaultRemote(path, e))), Some(Err(e)) => return Err(errors::TreeError::GetDefaultRemote(path, Box::new(e))),
None => { None => {
return Err(Box::new(errors::TreeError::NoDefaultRemote(path))); return Err(errors::TreeError::NoDefaultRemote(path));
} }
}; };
let refspec = match remote.refspecs(Direction::Fetch).first() { let refspec = match remote.refspecs(Direction::Fetch).first() {
Some(head) => head, Some(head) => head,
None => return Err(Box::new(errors::TreeError::NoRefSpecs(path))), None => return Err(errors::TreeError::NoRefSpecs(path)),
}; };
let spec_ref = refspec.to_ref(); let spec_ref = refspec.to_ref();
@ -72,59 +82,50 @@ impl PesdePackageSource {
Some(local) => local Some(local) => local
.to_string() .to_string()
.replace('*', repo.branch_names().first().unwrap_or(&"main")), .replace('*', repo.branch_names().first().unwrap_or(&"main")),
None => return Err(Box::new(errors::TreeError::NoLocalRefSpec(path))), None => return Err(errors::TreeError::NoLocalRefSpec(path)),
}; };
let reference = match repo.find_reference(&local_ref) { let reference = match repo.find_reference(&local_ref) {
Ok(reference) => reference, Ok(reference) => reference,
Err(e) => { Err(e) => return Err(errors::TreeError::NoReference(local_ref.to_string(), e)),
return Err(Box::new(errors::TreeError::NoReference(
local_ref.to_string(),
e,
)))
}
}; };
let reference_name = reference.name().as_bstr().to_string(); let reference_name = reference.name().as_bstr().to_string();
let id = match reference.into_fully_peeled_id() { let id = match reference.into_fully_peeled_id() {
Ok(id) => id, Ok(id) => id,
Err(e) => return Err(Box::new(errors::TreeError::CannotPeel(reference_name, e))), Err(e) => return Err(errors::TreeError::CannotPeel(reference_name, e)),
}; };
let id_str = id.to_string(); let id_str = id.to_string();
let object = match id.object() { let object = match id.object() {
Ok(object) => object, Ok(object) => object,
Err(e) => { Err(e) => return Err(errors::TreeError::CannotConvertToObject(id_str, e)),
return Err(Box::new(errors::TreeError::CannotConvertToObject(
id_str, e,
)))
}
}; };
match object.peel_to_tree() { match object.peel_to_tree() {
Ok(tree) => Ok(tree), Ok(tree) => Ok(tree),
Err(e) => Err(Box::new(errors::TreeError::CannotPeelToTree(id_str, e))), Err(e) => Err(errors::TreeError::CannotPeelToTree(id_str, e)),
} }
} }
pub(crate) fn read_file< pub fn read_file<
I: IntoIterator<Item = P> + Clone, I: IntoIterator<Item = P> + Clone,
P: ToString + PartialEq<gix::bstr::BStr>, P: ToString + PartialEq<gix::bstr::BStr>,
>( >(
&self, &self,
file_path: I, file_path: I,
project: &Project, project: &Project,
) -> Result<Option<String>, Box<errors::ReadFile>> { ) -> Result<Option<String>, errors::ReadFile> {
let path = self.path(project); let path = self.path(project);
let repo = match gix::open(&path) { let repo = match gix::open(&path) {
Ok(repo) => repo, Ok(repo) => repo,
Err(e) => return Err(Box::new(errors::ReadFile::Open(path, e))), Err(e) => return Err(errors::ReadFile::Open(path, Box::new(e))),
}; };
let tree = match self.tree(&repo) { let tree = match self.tree(&repo) {
Ok(tree) => tree, Ok(tree) => tree,
Err(e) => return Err(Box::new(errors::ReadFile::Tree(path, e))), Err(e) => return Err(errors::ReadFile::Tree(path, Box::new(e))),
}; };
let file_path_str = file_path let file_path_str = file_path
@ -138,36 +139,34 @@ impl PesdePackageSource {
let entry = match tree.lookup_entry(file_path, &mut lookup_buf) { let entry = match tree.lookup_entry(file_path, &mut lookup_buf) {
Ok(Some(entry)) => entry, Ok(Some(entry)) => entry,
Ok(None) => return Ok(None), Ok(None) => return Ok(None),
Err(e) => return Err(Box::new(errors::ReadFile::Lookup(file_path_str, e))), Err(e) => return Err(errors::ReadFile::Lookup(file_path_str, e)),
}; };
let object = match entry.object() { let object = match entry.object() {
Ok(object) => object, Ok(object) => object,
Err(e) => return Err(Box::new(errors::ReadFile::Lookup(file_path_str, e))), Err(e) => return Err(errors::ReadFile::Lookup(file_path_str, e)),
}; };
let blob = object.into_blob(); let blob = object.into_blob();
let string = String::from_utf8(blob.data.clone()) let string = String::from_utf8(blob.data.clone())
.map_err(|e| Box::new(errors::ReadFile::Utf8(file_path_str, e)))?; .map_err(|e| errors::ReadFile::Utf8(file_path_str, e))?;
Ok(Some(string)) Ok(Some(string))
} }
pub fn config(&self, project: &Project) -> Result<IndexConfig, Box<errors::ConfigError>> { pub fn config(&self, project: &Project) -> Result<IndexConfig, errors::ConfigError> {
let file = self let file = self.read_file(["config.toml"], project).map_err(Box::new)?;
.read_file(["config.toml"], project)
.map_err(|e| Box::new(e.into()))?;
let string = match file { let string = match file {
Some(s) => s, Some(s) => s,
None => { None => {
return Err(Box::new(errors::ConfigError::Missing( return Err(errors::ConfigError::Missing(Box::new(
self.repo_url.clone(), self.repo_url.clone(),
))) )))
} }
}; };
let config: IndexConfig = toml::from_str(&string).map_err(|e| Box::new(e.into()))?; let config: IndexConfig = toml::from_str(&string)?;
Ok(config) Ok(config)
} }
@ -175,17 +174,17 @@ impl PesdePackageSource {
pub fn all_packages( pub fn all_packages(
&self, &self,
project: &Project, project: &Project,
) -> Result<BTreeMap<PackageName, IndexFile>, Box<errors::AllPackagesError>> { ) -> Result<BTreeMap<PackageName, IndexFile>, errors::AllPackagesError> {
let path = self.path(project); let path = self.path(project);
let repo = match gix::open(&path) { let repo = match gix::open(&path) {
Ok(repo) => repo, Ok(repo) => repo,
Err(e) => return Err(Box::new(errors::AllPackagesError::Open(path, e))), Err(e) => return Err(errors::AllPackagesError::Open(path, Box::new(e))),
}; };
let tree = match self.tree(&repo) { let tree = match self.tree(&repo) {
Ok(tree) => tree, Ok(tree) => tree,
Err(e) => return Err(Box::new(errors::AllPackagesError::Tree(path, e))), Err(e) => return Err(errors::AllPackagesError::Tree(path, Box::new(e))),
}; };
let mut packages = BTreeMap::<PackageName, IndexFile>::new(); let mut packages = BTreeMap::<PackageName, IndexFile>::new();
@ -193,12 +192,12 @@ impl PesdePackageSource {
for entry in tree.iter() { for entry in tree.iter() {
let entry = match entry { let entry = match entry {
Ok(entry) => entry, Ok(entry) => entry,
Err(e) => return Err(Box::new(errors::AllPackagesError::Decode(path, e))), Err(e) => return Err(errors::AllPackagesError::Decode(path, e)),
}; };
let object = match entry.object() { let object = match entry.object() {
Ok(object) => object, Ok(object) => object,
Err(e) => return Err(Box::new(errors::AllPackagesError::Convert(path, e))), Err(e) => return Err(errors::AllPackagesError::Convert(path, e)),
}; };
// directories will be trees, and files will be blobs // directories will be trees, and files will be blobs
@ -211,12 +210,12 @@ impl PesdePackageSource {
for inner_entry in object.into_tree().iter() { for inner_entry in object.into_tree().iter() {
let inner_entry = match inner_entry { let inner_entry = match inner_entry {
Ok(entry) => entry, Ok(entry) => entry,
Err(e) => return Err(Box::new(errors::AllPackagesError::Decode(path, e))), Err(e) => return Err(errors::AllPackagesError::Decode(path, e)),
}; };
let object = match inner_entry.object() { let object = match inner_entry.object() {
Ok(object) => object, Ok(object) => object,
Err(e) => return Err(Box::new(errors::AllPackagesError::Convert(path, e))), Err(e) => return Err(errors::AllPackagesError::Convert(path, e)),
}; };
if !matches!(object.kind, gix::object::Kind::Blob) { if !matches!(object.kind, gix::object::Kind::Blob) {
@ -230,18 +229,17 @@ impl PesdePackageSource {
} }
let blob = object.into_blob(); let blob = object.into_blob();
let string = String::from_utf8(blob.data.clone()).map_err(|e| { let string = String::from_utf8(blob.data.clone())
Box::new(errors::AllPackagesError::Utf8(package_name.to_string(), e)) .map_err(|e| errors::AllPackagesError::Utf8(package_name.to_string(), e))?;
})?;
let file: IndexFile = match toml::from_str(&string) { let file: IndexFile = match toml::from_str(&string) {
Ok(file) => file, Ok(file) => file,
Err(e) => { Err(e) => {
return Err(Box::new(errors::AllPackagesError::Deserialize( return Err(errors::AllPackagesError::Deserialize(
package_name, package_name,
path, path,
e, Box::new(e),
))) ))
} }
}; };
@ -254,6 +252,13 @@ impl PesdePackageSource {
Ok(packages) Ok(packages)
} }
#[cfg(feature = "git2")]
pub fn repo_git2(&self, project: &Project) -> Result<git2::Repository, git2::Error> {
let path = self.path(project);
git2::Repository::open_bare(&path)
}
} }
impl PackageSource for PesdePackageSource { impl PackageSource for PesdePackageSource {
@ -321,7 +326,12 @@ impl PackageSource for PesdePackageSource {
let string = match self.read_file([scope, name], project) { let string = match self.read_file([scope, name], project) {
Ok(Some(s)) => s, Ok(Some(s)) => s,
Ok(None) => return Err(Self::ResolveError::NotFound(specifier.name.to_string())), Ok(None) => return Err(Self::ResolveError::NotFound(specifier.name.to_string())),
Err(e) => return Err(Self::ResolveError::Read(specifier.name.to_string(), e)), Err(e) => {
return Err(Self::ResolveError::Read(
specifier.name.to_string(),
Box::new(e),
))
}
}; };
let entries: IndexFile = toml::from_str(&string) let entries: IndexFile = toml::from_str(&string)
@ -363,7 +373,7 @@ impl PackageSource for PesdePackageSource {
project: &Project, project: &Project,
reqwest: &reqwest::blocking::Client, reqwest: &reqwest::blocking::Client,
) -> Result<(PackageFS, Target), Self::DownloadError> { ) -> Result<(PackageFS, Target), Self::DownloadError> {
let config = self.config(project)?; let config = self.config(project).map_err(Box::new)?;
let index_file = project let index_file = project
.cas_dir .cas_dir
.join("index") .join("index")
@ -385,21 +395,20 @@ impl PackageSource for PesdePackageSource {
Err(e) => return Err(errors::DownloadError::ReadIndex(e)), Err(e) => return Err(errors::DownloadError::ReadIndex(e)),
} }
let (scope, name) = pkg_ref.name.as_str();
let url = config let url = config
.download() .download()
.replace("{PACKAGE_SCOPE}", scope) .replace("{PACKAGE}", &pkg_ref.name.to_string().replace("/", "%2F"))
.replace("{PACKAGE_NAME}", name) .replace("{PACKAGE_VERSION}", &pkg_ref.version.to_string())
.replace("{PACKAGE_VERSION}", &pkg_ref.version.to_string()); .replace("{PACKAGE_TARGET}", &pkg_ref.target.to_string());
let mut response = reqwest.get(url); let mut response = reqwest.get(url).header(ACCEPT, "application/octet-stream");
if let Some(token) = &project.auth_config.pesde_token { if let Some(token) = &project.auth_config.pesde_token {
log::debug!("using token for pesde package download"); log::debug!("using token for pesde package download");
response = response.header("Authorization", format!("Bearer {token}")); response = response.header("Authorization", format!("Bearer {token}"));
} }
let response = response.send()?; let response = response.send()?.error_for_status()?;
let bytes = response.bytes()?; let bytes = response.bytes()?;
let mut decoder = flate2::read::GzDecoder::new(bytes.as_ref()); let mut decoder = flate2::read::GzDecoder::new(bytes.as_ref());
@ -445,7 +454,7 @@ pub struct IndexConfig {
#[serde(default)] #[serde(default)]
pub git_allowed: bool, pub git_allowed: bool,
#[serde(default)] #[serde(default)]
pub custom_registry_allowed: bool, pub other_registries_allowed: bool,
pub github_oauth_client_id: String, pub github_oauth_client_id: String,
} }
@ -456,11 +465,8 @@ impl IndexConfig {
pub fn download(&self) -> String { pub fn download(&self) -> String {
self.download self.download
.as_ref() .as_deref()
.unwrap_or( .unwrap_or("{API_URL}/v0/packages/{PACKAGE}/{PACKAGE_VERSION}/{PACKAGE_TARGET}")
&"{API_URL}/v0/packages/{PACKAGE_SCOPE}/{PACKAGE_NAME}/{PACKAGE_VERSION}"
.to_string(),
)
.replace("{API_URL}", self.api()) .replace("{API_URL}", self.api())
} }
} }
@ -473,6 +479,8 @@ pub struct IndexFileEntry {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")] #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub dependencies: BTreeMap<String, (DependencySpecifiers, DependencyType)>, pub dependencies: BTreeMap<String, (DependencySpecifiers, DependencyType)>,
@ -522,14 +530,11 @@ pub mod errors {
#[error("error interacting with the filesystem")] #[error("error interacting with the filesystem")]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
#[error("error opening repository at {0}")]
Open(PathBuf, #[source] gix::open::Error),
#[error("no default remote found in repository at {0}")] #[error("no default remote found in repository at {0}")]
NoDefaultRemote(PathBuf), NoDefaultRemote(PathBuf),
#[error("error getting default remote from repository at {0}")] #[error("error getting default remote from repository at {0}")]
GetDefaultRemote(PathBuf, #[source] gix::remote::find::existing::Error), GetDefaultRemote(PathBuf, #[source] Box<gix::remote::find::existing::Error>),
#[error("no refspecs found in repository at {0}")] #[error("no refspecs found in repository at {0}")]
NoRefSpecs(PathBuf), NoRefSpecs(PathBuf),
@ -554,7 +559,7 @@ pub mod errors {
#[non_exhaustive] #[non_exhaustive]
pub enum ReadFile { pub enum ReadFile {
#[error("error opening repository at {0}")] #[error("error opening repository at {0}")]
Open(PathBuf, #[source] gix::open::Error), Open(PathBuf, #[source] Box<gix::open::Error>),
#[error("error getting tree from repository at {0}")] #[error("error getting tree from repository at {0}")]
Tree(PathBuf, #[source] Box<TreeError>), Tree(PathBuf, #[source] Box<TreeError>),
@ -595,14 +600,14 @@ pub mod errors {
Parse(#[from] toml::de::Error), Parse(#[from] toml::de::Error),
#[error("missing config file for index at {0}")] #[error("missing config file for index at {0}")]
Missing(gix::Url), Missing(Box<gix::Url>),
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[non_exhaustive] #[non_exhaustive]
pub enum AllPackagesError { pub enum AllPackagesError {
#[error("error opening repository at {0}")] #[error("error opening repository at {0}")]
Open(PathBuf, #[source] gix::open::Error), Open(PathBuf, #[source] Box<gix::open::Error>),
#[error("error getting tree from repository at {0}")] #[error("error getting tree from repository at {0}")]
Tree(PathBuf, #[source] Box<TreeError>), Tree(PathBuf, #[source] Box<TreeError>),
@ -614,7 +619,7 @@ pub mod errors {
Convert(PathBuf, #[source] gix::object::find::existing::Error), Convert(PathBuf, #[source] gix::object::find::existing::Error),
#[error("error deserializing file {0} in repository at {1}")] #[error("error deserializing file {0} in repository at {1}")]
Deserialize(String, PathBuf, #[source] toml::de::Error), Deserialize(String, PathBuf, #[source] Box<toml::de::Error>),
#[error("error parsing file for {0} as utf8")] #[error("error parsing file for {0} as utf8")]
Utf8(String, #[source] std::string::FromUtf8Error), Utf8(String, #[source] std::string::FromUtf8Error),

View file

@ -1,3 +1,8 @@
use std::{
collections::BTreeMap,
fmt::{Debug, Display},
};
use crate::{ use crate::{
manifest::{ manifest::{
target::{Target, TargetKind}, target::{Target, TargetKind},
@ -6,10 +11,6 @@ use crate::{
source::{DependencySpecifiers, PackageFS, PackageSources, ResolveResult}, source::{DependencySpecifiers, PackageFS, PackageSources, ResolveResult},
Project, Project,
}; };
use std::{
collections::BTreeMap,
fmt::{Debug, Display},
};
pub trait DependencySpecifier: Debug + Display {} pub trait DependencySpecifier: Debug + Display {}