diff --git a/crates/uv-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index 4bd4e9dd5ac87..5d675998a1653 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -639,7 +639,9 @@ pub async fn upload( } } -/// Validate a file against a registry. +/// Validate a distribution before uploading. +/// +/// Returns `true` if the file should be uploaded, `false` if it already exists on the server. pub async fn validate( file: &Path, form_metadata: &FormMetadata, @@ -648,7 +650,7 @@ pub async fn validate( store: &PyxTokenStore, client: &BaseClient, credentials: &Credentials, -) -> Result<(), PublishError> { +) -> Result { if store.is_known_url(registry) { debug!("Performing validation request for {registry}"); @@ -674,16 +676,45 @@ pub async fn validate( ) })?; + let status_code = response.status(); + debug!("Response code for {validation_url}: {status_code}"); + + if status_code.is_success() { + #[derive(Deserialize)] + struct ValidateResponse { + exists: bool, + } + + // Check if the file already exists. + match response.text().await { + Ok(body) => { + trace!("Response content for {validation_url}: {body}"); + if let Ok(response) = serde_json::from_str::(&body) { + if response.exists { + debug!("File already uploaded: {raw_filename}"); + return Ok(false); + } + } + } + Err(err) => { + trace!("Failed to read response content for {validation_url}: {err}"); + } + } + return Ok(true); + } + + // Handle error response. handle_response(&validation_url, response) .await .map_err(|err| { PublishError::Validate(file.to_path_buf(), registry.clone().into(), err.into()) })?; + + Ok(true) } else { debug!("Skipping validation request for unsupported publish URL: {registry}"); + Ok(true) } - - Ok(()) } /// Upload a file using the two-phase upload protocol for pyx. diff --git a/crates/uv/src/commands/publish.rs b/crates/uv/src/commands/publish.rs index 31a4ad7cf0c1a..26e55673632b4 100644 --- a/crates/uv/src/commands/publish.rs +++ b/crates/uv/src/commands/publish.rs @@ -239,7 +239,7 @@ pub(crate) async fn publish( let uploaded = if direct { if dry_run { // For dry run, call validate since we won't call reserve. - uv_publish::validate( + let should_upload = uv_publish::validate( &group.file, &form_metadata, &group.raw_filename, @@ -249,6 +249,13 @@ pub(crate) async fn publish( &credentials, ) .await?; + if !should_upload { + writeln!( + printer.stderr(), + "{}", + "File already exists, skipping".dimmed() + )?; + } continue; } @@ -268,7 +275,7 @@ pub(crate) async fn publish( .await? } else { // Run validation checks on the file, but don't upload it (if possible). - uv_publish::validate( + let should_upload = uv_publish::validate( &group.file, &form_metadata, &group.raw_filename, @@ -283,20 +290,25 @@ pub(crate) async fn publish( continue; } - let reporter = PublishReporter::single(printer); - upload( - &group, - &form_metadata, - &publish_url, - &upload_client, - retry_policy, - &credentials, - check_url_client.as_ref(), - &download_concurrency, - // 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. + // If validation indicates the file already exists, skip the upload. + if !should_upload { + false + } else { + let reporter = PublishReporter::single(printer); + upload( + &group, + &form_metadata, + &publish_url, + &upload_client, + retry_policy, + &credentials, + check_url_client.as_ref(), + &download_concurrency, + // 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");