diff --git a/Changelog.md b/Changelog.md index e5235224b..911afec41 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Change manylinux default version based on target arch by messense in [#424](https://github.com/PyO3/maturin/pull/424) * Support local path dependencies in source distribution (i.e. you can now package a workspace into an sdist) * Set a more reasonable LC_ID_DYLIB entry on macOS by messense [#433](https://github.com/PyO3/maturin/pull/433) + * Add `--skip-existing` option to publish by messense [#444](https://github.com/PyO3/maturin/pull/444) ## 0.9.4 - 2021-02-18 diff --git a/Readme.md b/Readme.md index 10d5fb07c..91e021faa 100644 --- a/Readme.md +++ b/Readme.md @@ -277,6 +277,10 @@ FLAGS: --skip-auditwheel Don't check for manylinux compliance + --skip-existing + Continue uploading files if one already exists. (Only valid when uploading to PyPI. Other implementations + may not support this.) + --universal2 Control whether to build universal2 wheel for macOS or not. Only applies to macOS targets, do nothing otherwise diff --git a/src/main.rs b/src/main.rs index 2fcba726d..24819f091 100644 --- a/src/main.rs +++ b/src/main.rs @@ -147,6 +147,10 @@ struct PublishOpt { /// Do not strip the library for minimum file size #[structopt(long = "no-strip")] no_strip: bool, + /// Continue uploading files if one already exists. + /// (Only valid when uploading to PyPI. Other implementations may not support this.) + #[structopt(long = "skip-existing")] + skip_existing: bool, } #[derive(Debug, StructOpt)] @@ -423,12 +427,21 @@ fn upload_ui(build: BuildOptions, publish: &PublishOpt, no_sdist: bool) -> Resul bail!("Username and/or password are wrong"); } Err(err) => { + let filename = wheel_path.file_name().unwrap_or(&wheel_path.as_os_str()); + if let UploadError::FileExistsError(_) = err { + if publish.skip_existing { + eprintln!( + "⚠ Skipping {:?} because it appears to already exist", + filename + ); + continue; + } + } let filesize = fs::metadata(&wheel_path) .map(|x| ByteSize(x.len()).to_string()) .unwrap_or_else(|e| { format!("Failed to get the filesize of {:?}: {}", &wheel_path, e) }); - let filename = wheel_path.file_name().unwrap_or(&wheel_path.as_os_str()); return Err(err) .context(format!("💥 Failed to upload {:?} ({})", filename, filesize)); } diff --git a/src/upload.rs b/src/upload.rs index 20d08bc91..94023a4f3 100644 --- a/src/upload.rs +++ b/src/upload.rs @@ -29,6 +29,9 @@ pub enum UploadError { /// The registry returned something else than 200 #[error("Failed to upload the wheel with status {0}: {1}")] StatusCodeError(String, String), + /// File already exists + #[error("File already exists: {0}")] + FileExistsError(String), } impl From for UploadError { @@ -95,18 +98,38 @@ pub fn upload( .basic_auth(registry.username.clone(), Some(registry.password.clone())) .send()?; - if response.status().is_success() { - Ok(()) - } else if response.status() == StatusCode::FORBIDDEN { - Err(UploadError::AuthenticationError) + 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 + ) + }); + // Detect FileExistsError the way twine does + // https://github.com/pypa/twine/blob/87846e5777b380d4704704a69e1f9a7a1231451c/twine/commands/upload.py#L30 + if status == StatusCode::FORBIDDEN { + if err_text.contains("overwrite artifact") { + // Artifactory (https://jfrog.com/artifactory/) + Err(UploadError::FileExistsError(err_text)) + } else { + Err(UploadError::AuthenticationError) + } } else { - let status_string = response.status().to_string(); - 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 - ) - }); - Err(UploadError::StatusCodeError(status_string, err_text)) + let status_string = status.to_string(); + if status == StatusCode::CONFLICT // pypiserver (https://pypi.org/project/pypiserver) + // PyPI / TestPyPI + || (status == StatusCode::BAD_REQUEST && err_text.contains("already exists")) + // Nexus Repository OSS (https://www.sonatype.com/nexus-repository-oss) + || (status == StatusCode::BAD_REQUEST && err_text.contains("updating asset")) + // # Gitlab Enterprise Edition (https://about.gitlab.com) + || (status == StatusCode::BAD_REQUEST && err_text.contains("already been taken")) + { + Err(UploadError::FileExistsError(err_text)) + } else { + Err(UploadError::StatusCodeError(status_string, err_text)) + } } }