pesde/registry/src/endpoints/packages.rs

203 lines
6.2 KiB
Rust
Raw Normal View History

2024-03-04 21:18:49 +01:00
use actix_multipart::form::{bytes::Bytes, MultipartForm};
use actix_web::{web, HttpResponse, Responder};
2024-03-04 21:18:49 +01:00
use flate2::read::GzDecoder;
use reqwest::StatusCode;
use rusty_s3::S3Action;
use tantivy::{doc, Term};
use tar::Archive;
use pesde::{
dependencies::DependencySpecifier, index::Index, manifest::Manifest, package_name::PackageName,
IGNORED_FOLDERS, MANIFEST_FILE_NAME,
};
use crate::{commit_signature, errors, AppState, UserId, S3_EXPIRY};
#[derive(MultipartForm)]
pub struct CreateForm {
2024-03-04 21:18:49 +01:00
#[multipart(limit = "4 MiB")]
tarball: Bytes,
}
pub async fn create_package(
2024-03-04 21:18:49 +01:00
form: MultipartForm<CreateForm>,
app_state: web::Data<AppState>,
user_id: web::ReqData<UserId>,
) -> Result<impl Responder, errors::Errors> {
let bytes = form.tarball.data.as_ref().to_vec();
let mut decoder = GzDecoder::new(bytes.as_slice());
let mut archive = Archive::new(&mut decoder);
let archive_entries = archive.entries()?.filter_map(|e| e.ok());
let mut manifest = None;
for mut e in archive_entries {
let Ok(path) = e.path() else {
return Ok(HttpResponse::BadRequest().json(errors::ErrorResponse {
error: "Attached file contains non-UTF-8 path".to_string(),
}));
};
let Some(path) = path.as_os_str().to_str() else {
return Ok(HttpResponse::BadRequest().json(errors::ErrorResponse {
error: "Attached file contains non-UTF-8 path".to_string(),
}));
};
match path {
MANIFEST_FILE_NAME => {
if !e.header().entry_type().is_file() {
continue;
}
let received_manifest: Manifest =
serde_yaml::from_reader(&mut e).map_err(errors::Errors::UserYaml)?;
manifest = Some(received_manifest);
}
path => {
if e.header().entry_type().is_file() {
continue;
}
if IGNORED_FOLDERS.contains(&path) {
return Ok(HttpResponse::BadRequest().json(errors::ErrorResponse {
error: format!("Attached file contains forbidden directory {}", path),
}));
}
}
}
}
let Some(manifest) = manifest else {
return Ok(HttpResponse::BadRequest().json(errors::ErrorResponse {
error: format!("Attached file doesn't contain {MANIFEST_FILE_NAME}"),
}));
};
let (scope, name) = manifest.name.parts();
{
let mut index = app_state.index.lock().unwrap();
let config = index.config()?;
for (dependency, _) in manifest.dependencies().iter() {
match dependency {
DependencySpecifier::Git(_) => {
if !config.git_allowed {
return Ok(HttpResponse::BadRequest().json(errors::ErrorResponse {
error: "Git dependencies are not allowed on this registry".to_string(),
}));
}
}
DependencySpecifier::Registry(registry) => {
if index.package(&registry.name).unwrap().is_none() {
return Ok(HttpResponse::BadRequest().json(errors::ErrorResponse {
error: format!("Dependency {} not found", registry.name),
}));
}
}
};
}
let success = index.create_package_version(&manifest, &user_id.0)?;
if !success {
return Ok(HttpResponse::BadRequest().json(errors::ErrorResponse {
error: format!(
"Version {} of {} already exists",
manifest.version, manifest.name
),
}));
}
index.commit_and_push(
&format!("Add version {}@{}", manifest.name, manifest.version),
&commit_signature(),
)?;
}
{
let mut search_writer = app_state.search_writer.lock().unwrap();
let schema = search_writer.index().schema();
let name_field = schema.get_field("name").unwrap();
search_writer.delete_term(Term::from_field_text(
name_field,
&manifest.name.to_string(),
));
search_writer.add_document(
doc!(
name_field => manifest.name.to_string(),
schema.get_field("version").unwrap() => manifest.version.to_string(),
schema.get_field("description").unwrap() => manifest.description.unwrap_or_default(),
)
).unwrap();
search_writer.commit().unwrap();
}
let url = app_state
.s3_bucket
.put_object(
Some(&app_state.s3_credentials),
&*format!("{scope}-{name}-{}.tar.gz", manifest.version),
)
.sign(S3_EXPIRY);
app_state.reqwest_client.put(url).body(bytes).send().await?;
Ok(HttpResponse::Ok().body(format!(
"Successfully published {}@{}",
manifest.name, manifest.version
)))
}
pub async fn get_package_version(
2024-03-04 21:18:49 +01:00
app_state: web::Data<AppState>,
path: web::Path<(String, String, String)>,
) -> Result<impl Responder, errors::Errors> {
let (scope, name, version) = path.into_inner();
let package_name = PackageName::new(&scope, &name)?;
{
let index = app_state.index.lock().unwrap();
if !index.package(&package_name)?.is_some() {
return Ok(HttpResponse::NotFound().finish());
}
}
let url = app_state
.s3_bucket
.get_object(
Some(&app_state.s3_credentials),
&*format!("{scope}-{name}-{version}.tar.gz"),
)
.sign(S3_EXPIRY);
let response = match app_state
.reqwest_client
.get(url)
.send()
.await?
.error_for_status()
{
Ok(response) => response,
Err(e) => {
if let Some(status) = e.status() {
if status == StatusCode::NOT_FOUND {
return Ok(HttpResponse::NotFound().finish());
}
}
return Err(e.into());
}
};
Ok(HttpResponse::Ok().body(response.bytes().await?))
}