Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/core/src/types/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ pub struct WriteOptions {
pub if_match: Option<String>,
/// Sets If-None-Match header for this write request.
///
/// Note: Certain services, like `s3`, support `if_not_exists` but not `if_none_match`.
/// Note: Certain services support `if_not_exists` but not `if_none_match`.
/// Use `if_not_exists` if you only want to check whether a file exists.
///
/// ### Capability
Expand Down
1 change: 1 addition & 0 deletions core/services/s3/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,7 @@ impl Builder for S3Builder {
write_with_content_disposition: true,
write_with_content_encoding: true,
write_with_if_match: !config.disable_write_with_if_match,
write_with_if_none_match: true,
write_with_if_not_exists: true,
write_with_user_metadata: true,

Expand Down
119 changes: 116 additions & 3 deletions core/services/s3/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,9 @@ impl S3Core {
req = req.header(IF_MATCH, if_match);
}

if args.if_not_exists() {
if let Some(if_none_match) = args.if_none_match() {
req = req.header(IF_NONE_MATCH, if_none_match);
} else if args.if_not_exists() {
req = req.header(IF_NONE_MATCH, "*");
}

Expand Down Expand Up @@ -902,6 +904,18 @@ impl S3Core {
parts: Vec<CompleteMultipartUploadRequestPart>,
args: &OpWrite,
) -> Result<Response<Buffer>> {
let req = self.s3_complete_multipart_upload_request(path, upload_id, parts, args)?;

self.send(req).await
}

fn s3_complete_multipart_upload_request(
&self,
path: &str,
upload_id: &str,
parts: Vec<CompleteMultipartUploadRequestPart>,
args: &OpWrite,
) -> Result<Request<Buffer>> {
let p = build_abs_path(&self.root, path);

let url = format!(
Expand All @@ -927,7 +941,9 @@ impl S3Core {
if let Some(if_match) = args.if_match() {
req = req.header(IF_MATCH, if_match);
}
if args.if_not_exists() {
if let Some(if_none_match) = args.if_none_match() {
req = req.header(IF_NONE_MATCH, if_none_match);
} else if args.if_not_exists() {
req = req.header(IF_NONE_MATCH, "*");
}

Expand All @@ -941,7 +957,7 @@ impl S3Core {
.body(Buffer::from(Bytes::from(content)))
.map_err(new_request_build_error)?;

self.send(req).await
Ok(req)
}

/// Abort an on-going multipart upload.
Expand Down Expand Up @@ -1287,11 +1303,108 @@ impl Display for ChecksumAlgorithm {

#[cfg(test)]
mod tests {
use std::sync::Arc;

use bytes::Buf;
use bytes::Bytes;
use reqsign_aws_v4::RequestSigner as AwsV4Signer;
use reqsign_aws_v4::StaticCredentialProvider;
use reqsign_core::Context;
use reqsign_core::ProvideCredentialChain;
use reqsign_core::Signer;

use super::*;

fn test_core() -> S3Core {
let provider =
ProvideCredentialChain::new().push(StaticCredentialProvider::new("ak", "sk"));

S3Core {
info: Arc::new(AccessorInfo::default()),
bucket: "bucket".to_string(),
endpoint: "https://s3.amazonaws.com".to_string(),
root: "/root/".to_string(),
server_side_encryption: None,
server_side_encryption_aws_kms_key_id: None,
server_side_encryption_customer_algorithm: None,
server_side_encryption_customer_key: None,
server_side_encryption_customer_key_md5: None,
default_storage_class: None,
allow_anonymous: true,
disable_list_objects_v2: false,
enable_request_payer: false,
default_acl: None,
signer: Signer::new(
Context::new(),
provider,
AwsV4Signer::new("s3", "us-east-1"),
),
checksum_algorithm: None,
}
}

#[test]
fn test_put_object_request_sets_if_none_match() {
let core = test_core();
let op = OpWrite::default().with_if_none_match("\"etag\"");

let req = core
.s3_put_object_request("path/to/file", Some(0), &op, Buffer::new())
.expect("request must build");

assert_eq!(
req.headers()
.get(IF_NONE_MATCH)
.expect("If-None-Match must be set")
.to_str()
.expect("header must be valid"),
"\"etag\""
);
}

#[test]
fn test_put_object_request_keeps_if_not_exists_header() {
let core = test_core();
let op = OpWrite::default().with_if_not_exists(true);

let req = core
.s3_put_object_request("path/to/file", Some(0), &op, Buffer::new())
.expect("request must build");

assert_eq!(
req.headers()
.get(IF_NONE_MATCH)
.expect("If-None-Match must be set")
.to_str()
.expect("header must be valid"),
"*"
);
}

#[test]
fn test_complete_multipart_upload_request_sets_if_none_match() {
let core = test_core();
let op = OpWrite::default().with_if_none_match("\"etag\"");
let parts = vec![CompleteMultipartUploadRequestPart {
part_number: 1,
etag: "\"part-etag\"".to_string(),
..Default::default()
}];

let req = core
.s3_complete_multipart_upload_request("path/to/file", "upload-id", parts, &op)
.expect("request must build");

assert_eq!(
req.headers()
.get(IF_NONE_MATCH)
.expect("If-None-Match must be set")
.to_str()
.expect("header must be valid"),
"\"etag\""
);
}

/// This example is from https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateMultipartUpload.html#API_CreateMultipartUpload_Examples
#[test]
fn test_deserialize_initiate_multipart_upload_result() {
Expand Down
Loading