252 lines
7.6 KiB
Rust
252 lines
7.6 KiB
Rust
use crate::{
|
|
auth,
|
|
db::{DbVersionFeature, PgU32},
|
|
db_error,
|
|
index::{update_crate_from_db, BaseDependency},
|
|
internal_error, Auth, Errors, RespResult, State,
|
|
};
|
|
|
|
use async_trait::async_trait;
|
|
use axum::{
|
|
body::{Body, Bytes},
|
|
extract::{FromRequest, RequestParts},
|
|
Extension, Json,
|
|
};
|
|
use semver::Version;
|
|
use serde::{Deserialize, Serialize};
|
|
use sqlx::{query, query_as, types::Json as SqlxJson, Connection};
|
|
use std::{collections::HashMap, fmt::Write, sync::Arc};
|
|
use tokio::fs;
|
|
|
|
pub struct NewCrateRequest {
|
|
json_data: NewCrateJsonData,
|
|
crate_file: Vec<u8>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl FromRequest<Body> for NewCrateRequest {
|
|
type Rejection = Errors;
|
|
|
|
async fn from_request(req: &mut RequestParts<Body>) -> Result<Self, Self::Rejection> {
|
|
let body = Bytes::from_request(req).await.unwrap();
|
|
|
|
let mut offset = 0;
|
|
|
|
let json_len_bytes = body
|
|
.get(offset..offset + 4)
|
|
.ok_or_else(|| Errors::new("Not enough bytes when reading length of JSON data"))?;
|
|
let json_len = u32::from_le_bytes(json_len_bytes.try_into().unwrap()) as usize;
|
|
offset += 4;
|
|
|
|
let json_bytes = body
|
|
.get(offset..offset + json_len)
|
|
.ok_or_else(|| Errors::new("Not enough bytes when reading JSON data"))?;
|
|
let json_data = serde_json::from_slice::<NewCrateJsonData>(json_bytes)
|
|
.map_err(|e| Errors::new(format_args!("Error while parsing JSON data: {e}")))?;
|
|
offset += json_len;
|
|
|
|
let crate_len_bytes = body
|
|
.get(offset..offset + 4)
|
|
.ok_or_else(|| Errors::new("Not enough bytes when reading length of .crate file"))?;
|
|
let crate_len = u32::from_le_bytes(crate_len_bytes.try_into().unwrap()) as usize;
|
|
offset += 4;
|
|
|
|
let crate_file = body
|
|
.get(offset..offset + crate_len)
|
|
.ok_or_else(|| Errors::new("Not enough bytes when reading .crate file"))?
|
|
.to_vec();
|
|
offset += crate_len;
|
|
|
|
if body.len() != offset {
|
|
return Err(Errors::new("Too much data provided"));
|
|
}
|
|
|
|
Ok(Self {
|
|
json_data,
|
|
crate_file,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct NewCrateJsonData {
|
|
name: String,
|
|
vers: Version,
|
|
deps: Vec<Dependency2>,
|
|
features: HashMap<String, Vec<String>>,
|
|
authors: Vec<String>,
|
|
description: Option<String>,
|
|
documentation: Option<String>,
|
|
homepage: Option<String>,
|
|
readme: Option<String>,
|
|
readme_file: Option<String>,
|
|
keywords: Vec<String>,
|
|
categories: Vec<String>,
|
|
license: Option<String>,
|
|
license_file: Option<String>,
|
|
repository: Option<String>,
|
|
badges: HashMap<String, HashMap<String, String>>,
|
|
links: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct Dependency2 {
|
|
#[serde(flatten)]
|
|
base: BaseDependency,
|
|
explicit_name_in_toml: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
pub struct NewCrateResponse {
|
|
warnings: Warnings,
|
|
}
|
|
|
|
#[derive(Serialize)]
|
|
struct Warnings {
|
|
invalid_categories: Vec<String>,
|
|
invalid_badges: Vec<String>,
|
|
other: Vec<String>,
|
|
}
|
|
|
|
pub async fn new_crate(
|
|
mut request: NewCrateRequest,
|
|
Auth(auth_user): Auth,
|
|
Extension(state): Extension<Arc<State>>,
|
|
) -> RespResult<Json<NewCrateResponse>> {
|
|
if !request.json_data.name.is_ascii() {
|
|
return Err(Errors::new("Crate name must be ASCII").into());
|
|
}
|
|
|
|
request.json_data.name.make_ascii_lowercase();
|
|
|
|
let mut db = state.db.acquire().await.map_err(db_error)?;
|
|
|
|
let crate_id = {
|
|
let mut trans = db.begin().await.map_err(db_error)?;
|
|
|
|
let (crate_id,): (PgU32,) = match query_as("SELECT id FROM crates WHERE name = $1 LIMIT 1")
|
|
.bind(&request.json_data.name)
|
|
.fetch_optional(&mut *trans)
|
|
.await
|
|
.map_err(db_error)?
|
|
{
|
|
Some(v) => {
|
|
auth(&request.json_data.name, &auth_user, &state).await?;
|
|
v
|
|
}
|
|
None => query_as(
|
|
"INSERT INTO crates (name, publisher, owners) VALUES ($1, $2, $3) RETURNING id",
|
|
)
|
|
.bind(&request.json_data.name)
|
|
.bind(auth_user.id)
|
|
.bind(&[auth_user.id][..])
|
|
.fetch_one(&mut *trans)
|
|
.await
|
|
.map_err(db_error)?,
|
|
};
|
|
|
|
let hash = hmac_sha256::Hash::hash(&request.crate_file);
|
|
let mut cksum = String::new();
|
|
for byte in hash {
|
|
write!(cksum, "{byte:02x}").expect("Formatting to a String failed");
|
|
}
|
|
|
|
let (version_id,): (PgU32,) = query_as(
|
|
r"INSERT INTO versions (
|
|
vers, cksum, yanked, links,
|
|
crate_id, features,
|
|
authors, description, documentation,
|
|
homepage, readme, readme_file,
|
|
keywords, categories,
|
|
license, license_file,
|
|
repository, badges
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
|
RETURNING id",
|
|
)
|
|
.bind(&request.json_data.vers.to_string())
|
|
.bind(&cksum)
|
|
.bind(false)
|
|
.bind(&request.json_data.links)
|
|
.bind(crate_id)
|
|
.bind(
|
|
&request
|
|
.json_data
|
|
.features
|
|
.iter()
|
|
.map(|v| DbVersionFeature {
|
|
feature: v.0.clone(),
|
|
enables: v.1.clone(),
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
.bind(&request.json_data.authors)
|
|
.bind(&request.json_data.description)
|
|
.bind(&request.json_data.documentation)
|
|
.bind(&request.json_data.homepage)
|
|
.bind(&request.json_data.readme)
|
|
.bind(&request.json_data.readme_file)
|
|
.bind(&request.json_data.keywords)
|
|
.bind(&request.json_data.categories)
|
|
.bind(&request.json_data.license)
|
|
.bind(&request.json_data.license_file)
|
|
.bind(&request.json_data.repository)
|
|
.bind(SqlxJson(&request.json_data.badges))
|
|
.fetch_one(&mut *trans)
|
|
.await
|
|
.map_err(db_error)?;
|
|
|
|
for dep in &request.json_data.deps {
|
|
query(
|
|
r"INSERT INTO deps (
|
|
name, version_req, optional, default_features,
|
|
target, kind, registry, package, feature, version_id
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
|
|
)
|
|
.bind(&dep.base.name)
|
|
.bind(&dep.base.version_req.to_string())
|
|
.bind(&dep.base.optional)
|
|
.bind(&dep.base.default_features)
|
|
.bind(&dep.base.target)
|
|
.bind(&dep.base.kind.to_db_repr())
|
|
.bind(&dep.base.registry)
|
|
.bind(&dep.explicit_name_in_toml)
|
|
.bind(&dep.base.features)
|
|
.bind(version_id)
|
|
.execute(&mut *trans)
|
|
.await
|
|
.map_err(db_error)?;
|
|
}
|
|
|
|
trans.commit().await.map_err(db_error)?;
|
|
|
|
crate_id
|
|
};
|
|
|
|
update_crate_from_db(
|
|
crate_id,
|
|
&state,
|
|
&format!(
|
|
"Add version {} of '{}'",
|
|
request.json_data.vers, request.json_data.name
|
|
),
|
|
)
|
|
.await?;
|
|
|
|
let mut path = state.crate_dir.clone();
|
|
path.push(&request.json_data.name);
|
|
path.push(request.json_data.vers.to_string());
|
|
fs::create_dir_all(&path).await.map_err(internal_error)?;
|
|
path.push("crate.crate");
|
|
fs::write(path, request.crate_file)
|
|
.await
|
|
.map_err(internal_error)?;
|
|
|
|
Ok(Json(NewCrateResponse {
|
|
warnings: Warnings {
|
|
invalid_categories: Vec::new(),
|
|
invalid_badges: Vec::new(),
|
|
other: Vec::new(),
|
|
},
|
|
}))
|
|
}
|