mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-21 11:56:38 +00:00
fix(sftp): avoid metadata on multipart copy (#2935)
Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
@@ -34,10 +34,10 @@ use bytes::Bytes;
|
|||||||
use futures_util::stream::{self, StreamExt};
|
use futures_util::stream::{self, StreamExt};
|
||||||
use s3s::dto::{
|
use s3s::dto::{
|
||||||
AbortMultipartUploadInput, AbortMultipartUploadOutput, CompleteMultipartUploadInput, CompleteMultipartUploadOutput,
|
AbortMultipartUploadInput, AbortMultipartUploadOutput, CompleteMultipartUploadInput, CompleteMultipartUploadOutput,
|
||||||
CopyObjectInput, CopyObjectOutput, CreateBucketOutput, CreateMultipartUploadInput, CreateMultipartUploadOutput,
|
CopyObjectInput, CopyObjectOutput, CopyPartResult, CreateBucketOutput, CreateMultipartUploadInput,
|
||||||
DeleteBucketOutput, DeleteObjectOutput, ETag, GetObjectOutput, HeadBucketOutput, HeadObjectOutput, ListBucketsOutput,
|
CreateMultipartUploadOutput, DeleteBucketOutput, DeleteObjectOutput, ETag, GetObjectOutput, HeadBucketOutput,
|
||||||
ListObjectsV2Input, ListObjectsV2Output, PutObjectInput, PutObjectOutput, StreamingBlob, Timestamp, UploadPartCopyInput,
|
HeadObjectOutput, ListBucketsOutput, ListObjectsV2Input, ListObjectsV2Output, PutObjectInput, PutObjectOutput, StreamingBlob,
|
||||||
UploadPartCopyOutput, UploadPartInput, UploadPartOutput,
|
Timestamp, UploadPartCopyInput, UploadPartCopyOutput, UploadPartInput, UploadPartOutput,
|
||||||
};
|
};
|
||||||
use std::collections::{HashMap, VecDeque};
|
use std::collections::{HashMap, VecDeque};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
@@ -318,6 +318,18 @@ impl DummyBackend {
|
|||||||
self.inner.lock().expect("lock").upload_part.push_back(Err(err));
|
self.inner.lock().expect("lock").upload_part.push_back(Err(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Queue an upload_part_copy Ok response carrying the given ETag.
|
||||||
|
pub fn queue_upload_part_copy_ok(&self, e_tag: impl Into<String>) {
|
||||||
|
let out = UploadPartCopyOutput {
|
||||||
|
copy_part_result: Some(CopyPartResult {
|
||||||
|
e_tag: Some(ETag::Strong(e_tag.into())),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
self.inner.lock().expect("lock").upload_part_copy.push_back(Ok(out));
|
||||||
|
}
|
||||||
|
|
||||||
/// Queue a complete_multipart_upload Ok response.
|
/// Queue a complete_multipart_upload Ok response.
|
||||||
pub fn queue_complete_multipart_upload_ok(&self) {
|
pub fn queue_complete_multipart_upload_ok(&self) {
|
||||||
self.inner
|
self.inner
|
||||||
|
|||||||
@@ -65,13 +65,13 @@ pub(super) fn file_handle(bucket: &str, key: &str, size: u64, attrs: FileAttribu
|
|||||||
|
|
||||||
/// Build a HandleState::Write with the given bucket, key, and
|
/// Build a HandleState::Write with the given bucket, key, and
|
||||||
/// WritePhase, ready to be inserted directly into a driver handle
|
/// WritePhase, ready to be inserted directly into a driver handle
|
||||||
/// table without running open_write. Default FileAttributes are used.
|
/// table without running open_write. Default FSTAT attrs and empty OPEN attrs are used.
|
||||||
pub(super) fn write_handle(bucket: &str, key: &str, phase: WritePhase) -> HandleState {
|
pub(super) fn write_handle(bucket: &str, key: &str, phase: WritePhase) -> HandleState {
|
||||||
HandleState::Write {
|
HandleState::Write {
|
||||||
bucket: bucket.to_string(),
|
bucket: bucket.to_string(),
|
||||||
key: key.to_string(),
|
key: key.to_string(),
|
||||||
attrs: FileAttributes::default(),
|
attrs: FileAttributes::default(),
|
||||||
open_attrs: FileAttributes::default(),
|
open_attrs: FileAttributes::empty(),
|
||||||
phase,
|
phase,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,9 +159,7 @@ pub(super) fn fstat_reported_size(phase: &WritePhase, part_size: u64, cached_siz
|
|||||||
/// Drop runs the same AbortMultipartUpload for both.
|
/// Drop runs the same AbortMultipartUpload for both.
|
||||||
///
|
///
|
||||||
/// attrs is required by the HandleState::Write variant layout but
|
/// attrs is required by the HandleState::Write variant layout but
|
||||||
/// Drop does not read it. Callers pass a clone of the live attrs, or
|
/// Drop does not read it. Callers pass a clone of the live attrs.
|
||||||
/// FileAttributes::default() at the write_dispatch_begin_streaming
|
|
||||||
/// site where no live attrs is in scope.
|
|
||||||
pub(super) fn build_write_tombstone(
|
pub(super) fn build_write_tombstone(
|
||||||
bucket: &str,
|
bucket: &str,
|
||||||
key: &str,
|
key: &str,
|
||||||
@@ -173,7 +171,7 @@ pub(super) fn build_write_tombstone(
|
|||||||
bucket: bucket.to_string(),
|
bucket: bucket.to_string(),
|
||||||
key: key.to_string(),
|
key: key.to_string(),
|
||||||
attrs: attrs.clone(),
|
attrs: attrs.clone(),
|
||||||
open_attrs: FileAttributes::default(),
|
open_attrs: FileAttributes::empty(),
|
||||||
phase: WritePhase::Failed {
|
phase: WritePhase::Failed {
|
||||||
upload_id,
|
upload_id,
|
||||||
abort_authorized,
|
abort_authorized,
|
||||||
@@ -952,7 +950,7 @@ impl<S: StorageBackend + Send + Sync + 'static> SftpDriver<S> {
|
|||||||
// close_abort_or_skip can honour a Deny-Abort policy without a
|
// close_abort_or_skip can honour a Deny-Abort policy without a
|
||||||
// second IAM probe per error path.
|
// second IAM probe per error path.
|
||||||
let mp = self
|
let mp = self
|
||||||
.start_multipart_upload(dst_bucket, dst_key, &FileAttributes::default())
|
.start_multipart_upload(dst_bucket, dst_key, &FileAttributes::empty())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let result: Result<Vec<CompletedPart>, SftpError> = async {
|
let result: Result<Vec<CompletedPart>, SftpError> = async {
|
||||||
@@ -1590,7 +1588,7 @@ mod tests {
|
|||||||
|
|
||||||
with_test_auth_override(
|
with_test_auth_override(
|
||||||
|_, _, _| true,
|
|_, _, _| true,
|
||||||
driver.write_dispatch_begin_streaming(&handle_id, &mut phase, "b", "k", &FileAttributes::default()),
|
driver.write_dispatch_begin_streaming(&handle_id, &mut phase, "b", "k", &FileAttributes::empty()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("begin_streaming must succeed on queued Create Ok");
|
.expect("begin_streaming must succeed on queued Create Ok");
|
||||||
@@ -1630,7 +1628,7 @@ mod tests {
|
|||||||
backend.queue_create_multipart_upload_ok("UP-ALLOW");
|
backend.queue_create_multipart_upload_ok("UP-ALLOW");
|
||||||
let driver = build_driver(backend, TEST_PART_SIZE);
|
let driver = build_driver(backend, TEST_PART_SIZE);
|
||||||
|
|
||||||
let mp = with_test_auth_override(|_, _, _| true, driver.start_multipart_upload("b", "k", &FileAttributes::default()))
|
let mp = with_test_auth_override(|_, _, _| true, driver.start_multipart_upload("b", "k", &FileAttributes::empty()))
|
||||||
.await
|
.await
|
||||||
.expect("start_multipart_upload must succeed on Allow");
|
.expect("start_multipart_upload must succeed on Allow");
|
||||||
assert_eq!(mp.upload_id, "UP-ALLOW");
|
assert_eq!(mp.upload_id, "UP-ALLOW");
|
||||||
@@ -1672,7 +1670,7 @@ mod tests {
|
|||||||
backend.queue_create_multipart_upload_ok("UP-NO-ATTRS");
|
backend.queue_create_multipart_upload_ok("UP-NO-ATTRS");
|
||||||
let driver = build_driver(backend.clone(), TEST_PART_SIZE);
|
let driver = build_driver(backend.clone(), TEST_PART_SIZE);
|
||||||
|
|
||||||
with_test_auth_override(|_, _, _| true, driver.start_multipart_upload("b", "k", &FileAttributes::default()))
|
with_test_auth_override(|_, _, _| true, driver.start_multipart_upload("b", "k", &FileAttributes::empty()))
|
||||||
.await
|
.await
|
||||||
.expect("start_multipart_upload must succeed");
|
.expect("start_multipart_upload must succeed");
|
||||||
|
|
||||||
@@ -1691,7 +1689,7 @@ mod tests {
|
|||||||
// a WORM-shaped IAM policy a principal can meet in production.
|
// a WORM-shaped IAM policy a principal can meet in production.
|
||||||
let mp = with_test_auth_override(
|
let mp = with_test_auth_override(
|
||||||
|action, _bucket, _object| !matches!(action, S3Action::AbortMultipartUpload),
|
|action, _bucket, _object| !matches!(action, S3Action::AbortMultipartUpload),
|
||||||
driver.start_multipart_upload("b", "k", &FileAttributes::default()),
|
driver.start_multipart_upload("b", "k", &FileAttributes::empty()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("Create Allow must succeed even when Abort is Deny");
|
.expect("Create Allow must succeed even when Abort is Deny");
|
||||||
@@ -1713,7 +1711,7 @@ mod tests {
|
|||||||
|
|
||||||
let err = with_test_auth_override(
|
let err = with_test_auth_override(
|
||||||
|action, _, _| !matches!(action, S3Action::CreateMultipartUpload),
|
|action, _, _| !matches!(action, S3Action::CreateMultipartUpload),
|
||||||
driver.start_multipart_upload("b", "k", &FileAttributes::default()),
|
driver.start_multipart_upload("b", "k", &FileAttributes::empty()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect_err("Deny on CreateMultipartUpload must fail fast");
|
.expect_err("Deny on CreateMultipartUpload must fail fast");
|
||||||
@@ -1724,6 +1722,37 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn multipart_copy_starts_upload_without_sftp_open_metadata() {
|
||||||
|
let backend = Arc::new(DummyBackend::new());
|
||||||
|
backend.queue_create_multipart_upload_ok("UP-COPY");
|
||||||
|
backend.queue_upload_part_copy_ok("etag-copy-1");
|
||||||
|
backend.queue_complete_multipart_upload_ok();
|
||||||
|
let driver = build_driver(backend.clone(), TEST_PART_SIZE);
|
||||||
|
|
||||||
|
with_test_auth_override(
|
||||||
|
|_, _, _| true,
|
||||||
|
driver.multipart_copy("src-bucket", "src-key", "dst-bucket", "dst-key", TEST_PART_SIZE),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("multipart copy must complete");
|
||||||
|
|
||||||
|
let create_calls = backend.create_multipart_calls();
|
||||||
|
assert_eq!(create_calls.len(), 1);
|
||||||
|
assert_eq!(create_calls[0].bucket, "dst-bucket");
|
||||||
|
assert_eq!(create_calls[0].key, "dst-key");
|
||||||
|
assert!(
|
||||||
|
create_calls[0].metadata.is_none(),
|
||||||
|
"server-side multipart copy is not an SFTP OPEN path and must not write OPEN metadata"
|
||||||
|
);
|
||||||
|
|
||||||
|
let complete_calls = backend.complete_multipart_calls();
|
||||||
|
assert_eq!(complete_calls.len(), 1);
|
||||||
|
assert_eq!(complete_calls[0].upload_id, "UP-COPY");
|
||||||
|
assert_eq!(complete_calls[0].part_count, 1);
|
||||||
|
assert!(backend.abort_multipart_calls().is_empty(), "successful multipart copy must not abort");
|
||||||
|
}
|
||||||
|
|
||||||
// --- upload_multipart_bytes ---
|
// --- upload_multipart_bytes ---
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -1912,7 +1941,7 @@ mod tests {
|
|||||||
let driver = build_driver(backend.clone(), TEST_PART_SIZE);
|
let driver = build_driver(backend.clone(), TEST_PART_SIZE);
|
||||||
|
|
||||||
driver
|
driver
|
||||||
.commit_write("b", "k", &FileAttributes::default(), b"hello".to_vec())
|
.commit_write("b", "k", &FileAttributes::empty(), b"hello".to_vec())
|
||||||
.await
|
.await
|
||||||
.expect("commit_write must succeed");
|
.expect("commit_write must succeed");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user