2024-03-04 21:18:49 +01:00
|
|
|
use actix_multipart::form::{bytes::Bytes, MultipartForm};
|
2024-03-06 21:40:11 +01:00
|
|
|
use actix_web::{web, HttpResponse, Responder};
|
2024-03-16 17:26:58 +01:00
|
|
|
use chrono::Utc;
|
2024-03-04 21:18:49 +01:00
|
|
|
use flate2::read::GzDecoder;
|
2024-03-10 19:02:33 +01:00
|
|
|
use log::error;
|
2024-03-04 21:18:49 +01:00
|
|
|
use reqwest::StatusCode;
|
|
|
|
use rusty_s3::S3Action;
|
2024-03-16 17:26:58 +01:00
|
|
|
use tantivy::{doc, DateTime, Term};
|
2024-03-04 21:18:49 +01:00
|
|
|
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)]
|
2024-03-06 21:40:11 +01:00
|
|
|
pub struct CreateForm {
|
2024-03-04 21:18:49 +01:00
|
|
|
#[multipart(limit = "4 MiB")]
|
|
|
|
tarball: Bytes,
|
|
|
|
}
|
|
|
|
|
2024-03-06 21:40:11 +01:00
|
|
|
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(®istry.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!(
|
2024-03-16 17:26:58 +01:00
|
|
|
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(),
|
|
|
|
schema.get_field("published_at").unwrap() => DateTime::from_timestamp_secs(Utc::now().timestamp()),
|
|
|
|
)
|
2024-03-04 21:18:49 +01:00
|
|
|
).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
|
|
|
|
)))
|
|
|
|
}
|
|
|
|
|
2024-03-06 21:40:11 +01:00
|
|
|
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> {
|
2024-03-10 19:02:33 +01:00
|
|
|
let (scope, name, mut version) = path.into_inner();
|
2024-03-04 21:18:49 +01:00
|
|
|
|
|
|
|
let package_name = PackageName::new(&scope, &name)?;
|
|
|
|
|
|
|
|
{
|
|
|
|
let index = app_state.index.lock().unwrap();
|
|
|
|
|
2024-03-10 19:02:33 +01:00
|
|
|
match index.package(&package_name)? {
|
|
|
|
Some(package) => {
|
|
|
|
if version == "latest" {
|
|
|
|
version = package
|
|
|
|
.iter()
|
|
|
|
.max_by(|a, b| a.version.cmp(&b.version))
|
|
|
|
.map(|v| v.version.to_string())
|
|
|
|
.unwrap();
|
|
|
|
} else {
|
|
|
|
if !package.iter().any(|v| v.version.to_string() == version) {
|
|
|
|
return Ok(HttpResponse::NotFound().finish());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
None => return Ok(HttpResponse::NotFound().finish()),
|
2024-03-04 21:18:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2024-03-10 19:02:33 +01:00
|
|
|
error!(
|
|
|
|
"package {}@{} not found in S3, but found in index",
|
|
|
|
package_name, version
|
|
|
|
);
|
|
|
|
return Ok(HttpResponse::InternalServerError().finish());
|
2024-03-04 21:18:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Err(e.into());
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(HttpResponse::Ok().body(response.bytes().await?))
|
|
|
|
}
|
2024-03-10 19:02:33 +01:00
|
|
|
|
|
|
|
pub async fn get_package_versions(
|
|
|
|
app_state: web::Data<AppState>,
|
|
|
|
path: web::Path<(String, String)>,
|
|
|
|
) -> Result<impl Responder, errors::Errors> {
|
|
|
|
let (scope, name) = path.into_inner();
|
|
|
|
|
|
|
|
let package_name = PackageName::new(&scope, &name)?;
|
|
|
|
|
|
|
|
{
|
|
|
|
let index = app_state.index.lock().unwrap();
|
|
|
|
|
|
|
|
match index.package(&package_name)? {
|
|
|
|
Some(package) => {
|
|
|
|
let versions = package
|
|
|
|
.iter()
|
2024-03-16 17:26:58 +01:00
|
|
|
.map(|v| (v.version.to_string(), v.published_at.timestamp()))
|
|
|
|
.collect::<Vec<_>>();
|
2024-03-10 19:02:33 +01:00
|
|
|
|
|
|
|
Ok(HttpResponse::Ok().json(versions))
|
|
|
|
}
|
|
|
|
None => Ok(HttpResponse::NotFound().finish()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|