use crate::Registry;
use fs_err::File;
use regex::Regex;
use reqwest::{self, blocking::multipart::Form, blocking::Client, StatusCode};
use sha2::{Digest, Sha256};
use std::io;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Error, Debug)]
#[error("Uploading to the registry failed")]
pub enum UploadError {
#[error("Http error")]
RewqestError(#[source] reqwest::Error),
#[error("Username or password are incorrect")]
AuthenticationError,
#[error("IO Error")]
IoError(#[source] io::Error),
#[error("Failed to upload the wheel with status {0}: {1}")]
StatusCodeError(String, String),
#[error("File already exists: {0}")]
FileExistsError(String),
#[error("Could not read the metadata from the package at {0}")]
PkgInfoError(PathBuf, #[source] python_pkginfo::Error),
}
impl From<io::Error> for UploadError {
fn from(error: io::Error) -> Self {
UploadError::IoError(error)
}
}
impl From<reqwest::Error> for UploadError {
fn from(error: reqwest::Error) -> Self {
UploadError::RewqestError(error)
}
}
fn canonicalize_name(name: &str) -> String {
Regex::new("[-_.]+")
.unwrap()
.replace(name, "-")
.to_lowercase()
}
pub fn upload(registry: &Registry, wheel_path: &Path) -> Result<(), UploadError> {
let mut wheel = File::open(&wheel_path)?;
let mut hasher = Sha256::new();
io::copy(&mut wheel, &mut hasher)?;
let hash_hex = format!("{:x}", hasher.finalize());
let dist = python_pkginfo::Distribution::new(wheel_path)
.map_err(|err| UploadError::PkgInfoError(wheel_path.to_owned(), err))?;
let metadata = dist.metadata();
let mut api_metadata = vec![
(":action", "file_upload".to_string()),
("sha256_digest", hash_hex),
("protocol_version", "1".to_string()),
("metadata_version", metadata.metadata_version.clone()),
("name", canonicalize_name(&metadata.name)),
("version", metadata.version.clone()),
("pyversion", dist.python_version().to_string()),
("filetype", dist.r#type().to_string()),
];
let mut add_option = |name, value: &Option<String>| {
if let Some(some) = value.clone() {
api_metadata.push((name, some));
}
};
add_option("summary", &metadata.summary);
add_option("description", &metadata.description);
add_option(
"description_content_type",
&metadata.description_content_type,
);
add_option("author", &metadata.author);
add_option("author_email", &metadata.author_email);
add_option("maintainer", &metadata.maintainer);
add_option("maintainer_email", &metadata.maintainer_email);
add_option("license", &metadata.license);
add_option("keywords", &metadata.keywords);
add_option("home_page", &metadata.home_page);
add_option("download_url", &metadata.download_url);
add_option("requires_path", &metadata.requires_python);
add_option("summary", &metadata.summary);
let mut add_vec = |name, values: &[String]| {
for i in values {
api_metadata.push((name, i.clone()));
}
};
add_vec("classifiers", &metadata.classifiers);
add_vec("platform", &metadata.platforms);
add_vec("requires_dist", &metadata.requires_dist);
add_vec("provides_dist", &metadata.provides_dist);
add_vec("obsoletes_dist", &metadata.obsoletes_dist);
add_vec("requires_external", &metadata.requires_external);
add_vec("project_urls", &metadata.project_urls);
let mut form = Form::new();
for (key, value) in api_metadata {
form = form.text(key, value);
}
form = form.file("content", &wheel_path)?;
let client = Client::new();
let response = client
.post(registry.url.clone())
.header(
reqwest::header::USER_AGENT,
format!("{}/{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
)
.multipart(form)
.basic_auth(registry.username.clone(), Some(registry.password.clone()))
.send()?;
let status = response.status();
if status.is_success() {
return Ok(());
}
let err_text = response.text().unwrap_or_else(|e| {
format!(
"The registry should return some text, even in case of an error, but didn't ({})",
e
)
});
if status == StatusCode::FORBIDDEN {
if err_text.contains("overwrite artifact") {
Err(UploadError::FileExistsError(err_text))
} else {
Err(UploadError::AuthenticationError)
}
} else {
let status_string = status.to_string();
if status == StatusCode::CONFLICT || (status == StatusCode::BAD_REQUEST && err_text.contains("already exists"))
|| (status == StatusCode::BAD_REQUEST && err_text.contains("updating asset"))
|| (status == StatusCode::BAD_REQUEST && err_text.contains("already been taken"))
{
Err(UploadError::FileExistsError(err_text))
} else {
Err(UploadError::StatusCodeError(status_string, err_text))
}
}
}