Skip to content
Draft
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
17 changes: 17 additions & 0 deletions crates/cli/src/commands/rm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -436,4 +436,21 @@ mod tests {
let options = delete_request_options(&args);
assert!(options.force_delete);
}

#[test]
fn test_delete_request_options_keep_force_delete_disabled_without_purge() {
let args = RmArgs {
paths: vec!["test/bucket/object.txt".to_string()],
recursive: false,
force: true,
dry_run: false,
incomplete: false,
versions: false,
bypass: false,
purge: false,
};

let options = delete_request_options(&args);
assert!(!options.force_delete);
}
}
82 changes: 82 additions & 0 deletions crates/cli/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2408,6 +2408,7 @@ mod version_operations {
std::fs::write(temp_file.path(), "versioned delete content").expect("Failed to write");

let normal_key = "normal-delete.txt";
let force_key = "force-delete.txt";
let purge_key = "purge-delete.txt";

let upload_output = run_rc(
Expand Down Expand Up @@ -2475,6 +2476,72 @@ mod version_operations {
"Expected latest version to be a delete marker after normal rm"
);

let force_upload_output = run_rc(
&[
"cp",
temp_file
.path()
.to_str()
.expect("Temp file path should be UTF-8"),
&format!("test/{}/{}", bucket_name, force_key),
],
config_dir.path(),
);
assert!(
force_upload_output.status.success(),
"Failed to upload force delete object: {}",
String::from_utf8_lossy(&force_upload_output.stderr)
);

let force_delete_output = run_rc(
&[
"rm",
&format!("test/{}/{}", bucket_name, force_key),
"--force",
"--json",
],
config_dir.path(),
);
assert!(
force_delete_output.status.success(),
"Failed to force delete versioned object: {}",
String::from_utf8_lossy(&force_delete_output.stderr)
);

let force_versions_output = run_rc(
&[
"version",
"list",
&format!("test/{}/{}", bucket_name, force_key),
"--json",
],
config_dir.path(),
);
assert!(
force_versions_output.status.success(),
"Failed to list versions after force rm: {}",
String::from_utf8_lossy(&force_versions_output.stderr)
);

let force_stdout = String::from_utf8_lossy(&force_versions_output.stdout);
let force_versions: serde_json::Value =
serde_json::from_str(&force_stdout).expect("Invalid JSON version list");
let force_versions = force_versions
.as_array()
.expect("Version list should be a JSON array");
assert_eq!(
force_versions.len(),
2,
"Expected --force rm to keep the object version and create a delete marker"
);
assert!(
force_versions.iter().any(|entry| {
entry["is_delete_marker"].as_bool() == Some(true)
&& entry["is_latest"].as_bool() == Some(true)
}),
"Expected latest version to be a delete marker after force rm"
);

let purge_upload_output = run_rc(
&[
"cp",
Expand Down Expand Up @@ -2548,6 +2615,21 @@ mod version_operations {
String::from_utf8_lossy(&normal_cleanup_output.stderr)
);

let force_cleanup_output = run_rc(
&[
"rm",
&format!("test/{}/{}", bucket_name, force_key),
"--purge",
"--json",
],
config_dir.path(),
);
assert!(
force_cleanup_output.status.success(),
"Failed to purge force cleanup object: {}",
String::from_utf8_lossy(&force_cleanup_output.stderr)
);

cleanup_bucket(config_dir.path(), &bucket_name);
}
}
Expand Down
36 changes: 36 additions & 0 deletions crates/s3/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3195,6 +3195,42 @@ mod tests {
assert_eq!(request.headers().get("x-rustfs-force-delete"), Some("true"));
}

#[tokio::test]
async fn delete_objects_without_force_delete_omits_rustfs_header() {
let response = http::Response::builder()
.status(200)
.body(SdkBody::from(
r#"<?xml version="1.0" encoding="UTF-8"?>
<DeleteResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/" />"#,
))
.expect("build delete objects response");
let (client, request_receiver) = test_s3_client(Some(response));

let _ = client
.delete_objects_with_options(
"bucket",
vec!["key.txt".to_string()],
DeleteRequestOptions::default(),
)
.await;

let request = request_receiver.expect_request();
assert!(request.headers().get("x-rustfs-force-delete").is_none());
}

#[tokio::test]
async fn delete_objects_with_empty_keys_skips_http_request() {
let (client, request_receiver) = test_s3_client(None);

let deleted = client
.delete_objects_with_options("bucket", Vec::new(), DeleteRequestOptions::default())
.await
.expect("empty delete should succeed");

assert!(deleted.is_empty());
request_receiver.expect_no_request();
}

#[tokio::test]
async fn read_next_part_fills_buffer_until_eof() {
use tokio::io::AsyncWriteExt;
Expand Down