uv/crates/uv/src/commands/publish.rs

238 lines
9.0 KiB
Rust

use crate::commands::reporters::PublishReporter;
use crate::commands::{human_readable_bytes, ExitStatus};
use crate::printer::Printer;
use anyhow::{bail, Context, Result};
use console::Term;
use owo_colors::OwoColorize;
use std::fmt::Write;
use std::iter;
use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, info};
use url::Url;
use uv_cache::Cache;
use uv_client::{AuthIntegration, BaseClientBuilder, Connectivity, RegistryClientBuilder};
use uv_configuration::{KeyringProviderType, TrustedHost, TrustedPublishing};
use uv_distribution_types::{Index, IndexCapabilities, IndexLocations, IndexUrl};
use uv_publish::{
check_trusted_publishing, files_for_publishing, upload, CheckUrlClient, TrustedPublishResult,
};
use uv_warnings::warn_user_once;
pub(crate) async fn publish(
paths: Vec<String>,
publish_url: Url,
trusted_publishing: TrustedPublishing,
keyring_provider: KeyringProviderType,
allow_insecure_host: &[TrustedHost],
username: Option<String>,
password: Option<String>,
check_url: Option<IndexUrl>,
cache: &Cache,
connectivity: Connectivity,
native_tls: bool,
printer: Printer,
) -> Result<ExitStatus> {
if connectivity.is_offline() {
bail!("Unable to publish files in offline mode");
}
let files = files_for_publishing(paths)?;
match files.len() {
0 => bail!("No files found to publish"),
1 => writeln!(printer.stderr(), "Publishing 1 file to {publish_url}")?,
n => writeln!(printer.stderr(), "Publishing {n} files {publish_url}")?,
}
// * For the uploads themselves, we roll our own retries due to
// https://github.com/seanmonstar/reqwest/issues/2416, but for trusted publishing, we want
// the default retries.
// * We want to allow configuring TLS for the registry, while for trusted publishing we know the
// defaults are correct.
// * For the uploads themselves, we know we need an authorization header and we can't nor
// shouldn't try cloning the request to make an unauthenticated request first, but we want
// keyring integration. For trusted publishing, we use an OIDC auth routine without keyring
// or other auth integration.
let upload_client = BaseClientBuilder::new()
.retries(0)
.keyring(keyring_provider)
.native_tls(native_tls)
.allow_insecure_host(allow_insecure_host.to_vec())
// Don't try cloning the request to make an unauthenticated request first.
.auth_integration(AuthIntegration::OnlyAuthenticated)
// Set a very high timeout for uploads, connections are often 10x slower on upload than
// download. 15 min is taken from the time a trusted publishing token is valid.
.default_timeout(Duration::from_secs(15 * 60))
.build();
let oidc_client = BaseClientBuilder::new()
.auth_integration(AuthIntegration::NoAuthMiddleware)
.wrap_existing(&upload_client);
// Initialize the registry client.
let check_url_client = if let Some(index_url) = &check_url {
let index_urls = IndexLocations::new(
vec![Index::from_index_url(index_url.clone())],
Vec::new(),
false,
)
.index_urls();
let registry_client_builder = RegistryClientBuilder::new(cache.clone())
.native_tls(native_tls)
.connectivity(connectivity)
.index_urls(index_urls)
.keyring(keyring_provider)
.allow_insecure_host(allow_insecure_host.to_vec());
Some(CheckUrlClient {
index_url: index_url.clone(),
registry_client_builder,
client: &upload_client,
index_capabilities: IndexCapabilities::default(),
cache,
})
} else {
None
};
// If applicable, attempt obtaining a token for trusted publishing.
let trusted_publishing_token = check_trusted_publishing(
username.as_deref(),
password.as_deref(),
keyring_provider,
trusted_publishing,
&publish_url,
&oidc_client,
)
.await?;
let (username, mut password) =
if let TrustedPublishResult::Configured(password) = &trusted_publishing_token {
(Some("__token__".to_string()), Some(password.to_string()))
} else {
if username.is_none() && password.is_none() {
prompt_username_and_password()?
} else {
(username, password)
}
};
if password.is_some() && username.is_none() {
bail!(
"Attempted to publish with a password, but no username. Either provide a username \
with `--user` (`UV_PUBLISH_USERNAME`), or use `--token` (`UV_PUBLISH_TOKEN`) instead \
of a password."
);
}
if username.is_none() && password.is_none() && keyring_provider == KeyringProviderType::Disabled
{
if let TrustedPublishResult::Ignored(err) = trusted_publishing_token {
// The user has configured something incorrectly:
// * The user forgot to configure credentials.
// * The user forgot to forward the secrets as env vars (or used the wrong ones).
// * The trusted publishing configuration is wrong.
writeln!(
printer.stderr(),
"Note: Neither credentials nor keyring are configured, and there was an error \
fetching the trusted publishing token. If you don't want to use trusted \
publishing, you can ignore this error, but you need to provide credentials."
)?;
writeln!(
printer.stderr(),
"{}: {err}",
"Trusted publishing error".red().bold()
)?;
for source in iter::successors(std::error::Error::source(&err), |&err| err.source()) {
writeln!(
printer.stderr(),
" {}: {}",
"Caused by".red().bold(),
source.to_string().trim()
)?;
}
}
}
// If applicable, fetch the password from the keyring eagerly to avoid user confusion about
// missing keyring entries later.
if let Some(keyring_provider) = keyring_provider.to_provider() {
if password.is_none() {
if let Some(username) = &username {
debug!("Fetching password from keyring");
if let Some(keyring_password) = keyring_provider
.fetch(&publish_url, username)
.await
.as_ref()
.and_then(|credentials| credentials.password())
{
password = Some(keyring_password.to_string());
} else {
warn_user_once!(
"Keyring has no password for URL `{publish_url}` and username `{username}`"
);
}
}
} else if check_url.is_none() {
warn_user_once!(
"Using `--keyring-provider` with a password or token and no check url has no effect"
);
} else {
// We may be using the keyring for the simple index.
}
}
for (file, raw_filename, filename) in files {
if let Some(check_url_client) = &check_url_client {
if uv_publish::check_url(check_url_client, &file, &filename).await? {
writeln!(printer.stderr(), "File {filename} already exists, skipping")?;
continue;
}
}
let size = fs_err::metadata(&file)?.len();
let (bytes, unit) = human_readable_bytes(size);
writeln!(
printer.stderr(),
"{} {filename} {}",
"Uploading".bold().green(),
format!("({bytes:.1}{unit})").dimmed()
)?;
let reporter = PublishReporter::single(printer);
let uploaded = upload(
&file,
&raw_filename,
&filename,
&publish_url,
&upload_client,
username.as_deref(),
password.as_deref(),
check_url_client.as_ref(),
// Needs to be an `Arc` because the reqwest `Body` static lifetime requirement
Arc::new(reporter),
)
.await?; // Filename and/or URL are already attached, if applicable.
info!("Upload succeeded");
if !uploaded {
writeln!(
printer.stderr(),
"{}",
"File already exists, skipping".dimmed()
)?;
}
}
Ok(ExitStatus::Success)
}
fn prompt_username_and_password() -> Result<(Option<String>, Option<String>)> {
let term = Term::stderr();
if !term.is_term() {
return Ok((None, None));
}
let username_prompt = "Enter username ('__token__' if using a token): ";
let password_prompt = "Enter password: ";
let username = uv_console::input(username_prompt, &term).context("Failed to read username")?;
let password =
uv_console::password(password_prompt, &term).context("Failed to read password")?;
Ok((Some(username), Some(password)))
}