mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-01 17:58:22 +00:00
c00e5491b5
refactor(s3-client): remove the superseded per-algorithm checksum plumbing Deletes the write-only RequestMetadata.add_crc pipeline (assigned but never read since the port), the dead MinIO-parity Checksum constructors and CompletePart accessor, and key_capitalized (identical to key). The five hand-rolled x-amz-checksum-* response-header if-lets in the streaming and multipart paths collapse into one checksum_header_value helper, ChecksumMode's inherent to_string becomes a Display impl, and checksum.rs drops its file-wide allow blanket now that the file is lint-clean. Refs rustfs/backlog#1844 (PR2 of 3).
478 lines
19 KiB
Rust
478 lines
19 KiB
Rust
// Copyright 2024 RustFS Team
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
#![allow(unused_variables)]
|
|
#![allow(unused_mut)]
|
|
#![allow(unused_assignments)]
|
|
#![allow(unused_must_use)]
|
|
#![allow(clippy::all)]
|
|
|
|
use http::{HeaderMap, HeaderName, StatusCode};
|
|
use http_body_util::BodyExt;
|
|
use hyper::body::Bytes;
|
|
use s3s::S3ErrorCode;
|
|
use std::collections::HashMap;
|
|
use time::OffsetDateTime;
|
|
use tracing::warn;
|
|
use uuid::Uuid;
|
|
|
|
use crate::checksum::{ChecksumMode, checksum_header_value};
|
|
use crate::utils::base64_encode;
|
|
use crate::{
|
|
api_error_response::{
|
|
err_entity_too_large, err_entity_too_small, err_invalid_argument, http_resp_to_error_response, to_error_response,
|
|
},
|
|
api_put_object::PutObjectOptions,
|
|
api_put_object_common::optimal_part_info,
|
|
api_s3_datatypes::{
|
|
CompleteMultipartUpload, CompleteMultipartUploadResult, CompletePart, InitiateMultipartUploadResult, ObjectPart,
|
|
},
|
|
constants::{ISO8601_DATEFORMAT, MAX_PART_SIZE, MAX_SINGLE_PUT_OBJECT_SIZE},
|
|
transition_api::{ReaderImpl, RequestMetadata, TransitionClient, UploadInfo},
|
|
};
|
|
use rustfs_utils::path::trim_etag;
|
|
use s3s::header::X_AMZ_EXPIRATION;
|
|
|
|
impl TransitionClient {
|
|
pub async fn put_object_multipart(
|
|
&self,
|
|
bucket_name: &str,
|
|
object_name: &str,
|
|
mut reader: ReaderImpl,
|
|
size: i64,
|
|
opts: &PutObjectOptions,
|
|
) -> Result<UploadInfo, std::io::Error> {
|
|
let info = self
|
|
.put_object_multipart_no_stream(bucket_name, object_name, &mut reader, opts)
|
|
.await;
|
|
if let Err(err) = &info {
|
|
let err_resp = to_error_response(err);
|
|
if err_resp.code == S3ErrorCode::AccessDenied && err_resp.message.contains("Access Denied") {
|
|
if size > MAX_SINGLE_PUT_OBJECT_SIZE {
|
|
return Err(std::io::Error::other(err_entity_too_large(
|
|
size,
|
|
MAX_SINGLE_PUT_OBJECT_SIZE,
|
|
bucket_name,
|
|
object_name,
|
|
)));
|
|
}
|
|
return self.put_object_gcs(bucket_name, object_name, reader, size, opts).await;
|
|
}
|
|
}
|
|
Ok(info?)
|
|
}
|
|
|
|
pub async fn put_object_multipart_no_stream(
|
|
&self,
|
|
bucket_name: &str,
|
|
object_name: &str,
|
|
reader: &mut ReaderImpl,
|
|
opts: &PutObjectOptions,
|
|
) -> Result<UploadInfo, std::io::Error> {
|
|
let mut total_uploaded_size: i64 = 0;
|
|
let mut compl_multipart_upload = CompleteMultipartUpload::default();
|
|
|
|
let ret = optimal_part_info(-1, opts.part_size)?;
|
|
let (total_parts_count, part_size, _) = ret;
|
|
|
|
let (mut hash_algos, mut hash_sums) = self.hash_materials(opts.send_content_md5, !opts.disable_content_sha256);
|
|
let upload_id = self.new_upload_id(bucket_name, object_name, opts).await?;
|
|
let mut opts = opts.clone();
|
|
opts.user_metadata.remove("X-Amz-Checksum-Algorithm");
|
|
|
|
let mut part_number = 1;
|
|
let mut parts_info = HashMap::<i64, ObjectPart>::new();
|
|
let mut buf = Vec::<u8>::with_capacity(part_size as usize);
|
|
let mut custom_header = HeaderMap::new();
|
|
while part_number <= total_parts_count {
|
|
match reader {
|
|
ReaderImpl::Body(content_body) => {
|
|
buf = content_body.to_vec();
|
|
}
|
|
ReaderImpl::ObjectBody(content_body) => {
|
|
buf = content_body.read_all().await?;
|
|
}
|
|
}
|
|
let length = buf.len();
|
|
|
|
for (k, v) in hash_algos.iter_mut() {
|
|
let hash = v.hash_encode(&buf[..length]);
|
|
hash_sums.insert(k.to_string(), hash.as_ref().to_vec());
|
|
}
|
|
|
|
//let rd = newHook(bytes.NewReader(buf[..length]), opts.progress);
|
|
let rd = Bytes::from(buf.clone());
|
|
|
|
let md5_base64: String;
|
|
let sha256_hex: String;
|
|
|
|
//if hash_sums["md5"] != nil {
|
|
md5_base64 = base64_encode(&hash_sums["md5"]);
|
|
//}
|
|
//if hash_sums["sha256"] != nil {
|
|
sha256_hex = hex_simd::encode_to_string(hash_sums["sha256"].clone(), hex_simd::AsciiCase::Lower);
|
|
//}
|
|
if hash_sums.len() == 0 {
|
|
let mut crc = opts.auto_checksum.hasher()?;
|
|
crc.update(&buf[..length]);
|
|
let csum = crc.finalize();
|
|
|
|
if let Ok(header_name) = HeaderName::from_bytes(opts.auto_checksum.key().as_bytes()) {
|
|
if let Ok(header_value) = base64_encode(csum.as_ref()).parse() {
|
|
custom_header.insert(header_name, header_value);
|
|
} else {
|
|
warn!("Failed to parse checksum value");
|
|
}
|
|
} else {
|
|
warn!("Invalid header name: {}", opts.auto_checksum.key());
|
|
}
|
|
}
|
|
|
|
let mut p = UploadPartParams {
|
|
bucket_name: bucket_name.to_string(),
|
|
object_name: object_name.to_string(),
|
|
upload_id: upload_id.clone(),
|
|
reader: ReaderImpl::Body(rd),
|
|
part_number,
|
|
md5_base64,
|
|
sha256_hex,
|
|
size: length as i64,
|
|
//sse: opts.server_side_encryption,
|
|
stream_sha256: !opts.disable_content_sha256,
|
|
custom_header: custom_header.clone(),
|
|
trailer: HeaderMap::new(),
|
|
};
|
|
let obj_part = self.upload_part(&mut p).await?;
|
|
|
|
parts_info.insert(part_number, obj_part);
|
|
total_uploaded_size += length as i64;
|
|
part_number += 1;
|
|
}
|
|
|
|
let mut all_parts = Vec::<ObjectPart>::with_capacity(parts_info.len());
|
|
for i in 1..part_number {
|
|
let part = parts_info[&i].clone();
|
|
all_parts.push(part.clone());
|
|
compl_multipart_upload.parts.push(CompletePart {
|
|
etag: part.etag,
|
|
part_num: part.part_num,
|
|
checksum_crc32: part.checksum_crc32,
|
|
checksum_crc32c: part.checksum_crc32c,
|
|
checksum_sha1: part.checksum_sha1,
|
|
checksum_sha256: part.checksum_sha256,
|
|
checksum_crc64nvme: part.checksum_crc64nvme,
|
|
..Default::default()
|
|
});
|
|
}
|
|
|
|
compl_multipart_upload.parts.sort();
|
|
let opts = PutObjectOptions {
|
|
//server_side_encryption: opts.server_side_encryption,
|
|
auto_checksum: opts.auto_checksum,
|
|
..Default::default()
|
|
};
|
|
//apply_auto_checksum(&mut opts, all_parts);
|
|
|
|
let mut upload_info = self
|
|
.complete_multipart_upload(bucket_name, object_name, &upload_id, compl_multipart_upload, &opts)
|
|
.await?;
|
|
|
|
upload_info.size = total_uploaded_size;
|
|
Ok(upload_info)
|
|
}
|
|
|
|
pub async fn initiate_multipart_upload(
|
|
&self,
|
|
bucket_name: &str,
|
|
object_name: &str,
|
|
opts: &PutObjectOptions,
|
|
) -> Result<InitiateMultipartUploadResult, std::io::Error> {
|
|
let mut url_values = HashMap::new();
|
|
url_values.insert("uploads".to_string(), "".to_string());
|
|
|
|
if opts.internal.source_version_id != "" {
|
|
if !opts.internal.source_version_id.is_empty() {
|
|
if let Err(err) = Uuid::parse_str(&opts.internal.source_version_id) {
|
|
return Err(std::io::Error::other(err_invalid_argument(&err.to_string())));
|
|
}
|
|
}
|
|
url_values.insert("versionId".to_string(), opts.internal.source_version_id.clone());
|
|
}
|
|
|
|
let custom_header = opts.header();
|
|
|
|
let mut req_metadata = RequestMetadata {
|
|
bucket_name: bucket_name.to_string(),
|
|
object_name: object_name.to_string(),
|
|
query_values: url_values,
|
|
custom_header,
|
|
content_body: ReaderImpl::Body(Bytes::new()),
|
|
content_length: 0,
|
|
content_md5_base64: "".to_string(),
|
|
content_sha256_hex: "".to_string(),
|
|
stream_sha256: false,
|
|
trailer: HeaderMap::new(),
|
|
pre_sign_url: Default::default(),
|
|
extra_pre_sign_header: Default::default(),
|
|
bucket_location: Default::default(),
|
|
expires: Default::default(),
|
|
};
|
|
|
|
let resp = self.execute_method(http::Method::POST, &mut req_metadata).await?;
|
|
|
|
let resp_status = resp.status();
|
|
let h = resp.headers().clone();
|
|
|
|
//if resp.is_none() {
|
|
if resp.status() != StatusCode::OK {
|
|
return Err(std::io::Error::other(http_resp_to_error_response(
|
|
resp_status,
|
|
&h,
|
|
vec![],
|
|
bucket_name,
|
|
object_name,
|
|
)));
|
|
}
|
|
//}
|
|
// Parse the CreateMultipartUpload response for the UploadId. Returning a
|
|
// default (empty) result here made every multipart transition fail at the
|
|
// first UploadPart with "UploadID cannot be empty" (rustfs/rustfs#4811).
|
|
let mut body_vec = Vec::new();
|
|
let mut body = resp.into_body();
|
|
while let Some(frame) = body.frame().await {
|
|
let frame = frame.map_err(|e| std::io::Error::other(e.to_string()))?;
|
|
if let Some(data) = frame.data_ref() {
|
|
body_vec.extend_from_slice(data);
|
|
}
|
|
}
|
|
let initiate_multipart_upload_result =
|
|
quick_xml::de::from_str::<InitiateMultipartUploadResult>(&String::from_utf8_lossy(&body_vec))
|
|
.map_err(|e| std::io::Error::other(format!("failed to parse CreateMultipartUpload response: {e}")))?;
|
|
if initiate_multipart_upload_result.upload_id.is_empty() {
|
|
return Err(std::io::Error::other("CreateMultipartUpload response missing UploadId"));
|
|
}
|
|
Ok(initiate_multipart_upload_result)
|
|
}
|
|
|
|
pub async fn upload_part(&self, p: &mut UploadPartParams) -> Result<ObjectPart, std::io::Error> {
|
|
if p.size > MAX_PART_SIZE {
|
|
return Err(std::io::Error::other(err_entity_too_large(
|
|
p.size,
|
|
MAX_PART_SIZE,
|
|
&p.bucket_name,
|
|
&p.object_name,
|
|
)));
|
|
}
|
|
if p.size <= -1 {
|
|
return Err(std::io::Error::other(err_entity_too_small(p.size, &p.bucket_name, &p.object_name)));
|
|
}
|
|
if p.part_number <= 0 {
|
|
return Err(std::io::Error::other(err_invalid_argument(
|
|
"Part number cannot be negative or equal to zero.",
|
|
)));
|
|
}
|
|
if p.upload_id == "" {
|
|
return Err(std::io::Error::other(err_invalid_argument("UploadID cannot be empty.")));
|
|
}
|
|
|
|
let mut url_values = HashMap::new();
|
|
url_values.insert("partNumber".to_string(), p.part_number.to_string());
|
|
url_values.insert("uploadId".to_string(), p.upload_id.clone());
|
|
|
|
let buf = match &mut p.reader {
|
|
ReaderImpl::Body(content_body) => content_body.to_vec(),
|
|
ReaderImpl::ObjectBody(content_body) => content_body.read_all().await?,
|
|
};
|
|
let mut req_metadata = RequestMetadata {
|
|
bucket_name: p.bucket_name.clone(),
|
|
object_name: p.object_name.clone(),
|
|
query_values: url_values,
|
|
custom_header: p.custom_header.clone(),
|
|
content_body: ReaderImpl::Body(Bytes::from(buf)),
|
|
content_length: p.size,
|
|
content_md5_base64: p.md5_base64.clone(),
|
|
content_sha256_hex: p.sha256_hex.clone(),
|
|
stream_sha256: p.stream_sha256,
|
|
trailer: p.trailer.clone(),
|
|
pre_sign_url: Default::default(),
|
|
extra_pre_sign_header: Default::default(),
|
|
bucket_location: Default::default(),
|
|
expires: Default::default(),
|
|
};
|
|
|
|
let resp = self.execute_method(http::Method::PUT, &mut req_metadata).await?;
|
|
|
|
let resp_status = resp.status();
|
|
let h = resp.headers().clone();
|
|
|
|
if resp.status() != StatusCode::OK {
|
|
return Err(std::io::Error::other(http_resp_to_error_response(
|
|
resp_status,
|
|
&h,
|
|
vec![],
|
|
&p.bucket_name.clone(),
|
|
&p.object_name,
|
|
)));
|
|
}
|
|
//}
|
|
let h = resp.headers();
|
|
let mut obj_part = ObjectPart {
|
|
checksum_crc32: checksum_header_value(h, ChecksumMode::ChecksumCRC32),
|
|
checksum_crc32c: checksum_header_value(h, ChecksumMode::ChecksumCRC32C),
|
|
checksum_sha1: checksum_header_value(h, ChecksumMode::ChecksumSHA1),
|
|
checksum_sha256: checksum_header_value(h, ChecksumMode::ChecksumSHA256),
|
|
checksum_crc64nvme: checksum_header_value(h, ChecksumMode::ChecksumCRC64NVME),
|
|
..Default::default()
|
|
};
|
|
obj_part.size = p.size;
|
|
obj_part.part_num = p.part_number;
|
|
obj_part.etag = if let Some(h_etag) = h.get("ETag") {
|
|
h_etag.to_str().unwrap_or("").trim_matches('"').to_string()
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
Ok(obj_part)
|
|
}
|
|
|
|
pub async fn complete_multipart_upload(
|
|
&self,
|
|
bucket_name: &str,
|
|
object_name: &str,
|
|
upload_id: &str,
|
|
complete: CompleteMultipartUpload,
|
|
opts: &PutObjectOptions,
|
|
) -> Result<UploadInfo, std::io::Error> {
|
|
let mut url_values = HashMap::new();
|
|
url_values.insert("uploadId".to_string(), upload_id.to_string());
|
|
let complete_multipart_upload_bytes = complete.marshal_msg()?.as_bytes().to_vec();
|
|
|
|
let headers = opts.header();
|
|
|
|
let complete_multipart_upload_buffer = Bytes::from(complete_multipart_upload_bytes);
|
|
let mut req_metadata = RequestMetadata {
|
|
bucket_name: bucket_name.to_string(),
|
|
object_name: object_name.to_string(),
|
|
query_values: url_values,
|
|
custom_header: headers,
|
|
content_body: ReaderImpl::Body(complete_multipart_upload_buffer),
|
|
content_length: 100, //complete_multipart_upload_bytes.len(),
|
|
content_sha256_hex: "".to_string(), //hex_simd::encode_to_string(complete_multipart_upload_bytes, hex_simd::AsciiCase::Lower),
|
|
content_md5_base64: "".to_string(),
|
|
stream_sha256: Default::default(),
|
|
trailer: Default::default(),
|
|
pre_sign_url: Default::default(),
|
|
extra_pre_sign_header: Default::default(),
|
|
bucket_location: Default::default(),
|
|
expires: Default::default(),
|
|
};
|
|
|
|
let resp = self.execute_method(http::Method::POST, &mut req_metadata).await?;
|
|
|
|
let h = resp.headers().clone();
|
|
|
|
let complete_multipart_upload_result: CompleteMultipartUploadResult = CompleteMultipartUploadResult::default();
|
|
|
|
let exp_time = resp
|
|
.headers()
|
|
.get(X_AMZ_EXPIRATION)
|
|
.and_then(|v| v.to_str().ok())
|
|
.and_then(|s| OffsetDateTime::parse(s, ISO8601_DATEFORMAT).ok())
|
|
.unwrap_or_else(OffsetDateTime::now_utc);
|
|
let rule_id = "".to_string();
|
|
|
|
Ok(UploadInfo {
|
|
bucket: complete_multipart_upload_result.bucket,
|
|
key: complete_multipart_upload_result.key,
|
|
etag: trim_etag(&complete_multipart_upload_result.etag),
|
|
version_id: self.legacy_remote_version_id(&h)?,
|
|
location: complete_multipart_upload_result.location,
|
|
expiration: exp_time,
|
|
expiration_rule_id: rule_id,
|
|
checksum_sha256: complete_multipart_upload_result.checksum_sha256,
|
|
checksum_sha1: complete_multipart_upload_result.checksum_sha1,
|
|
checksum_crc32: complete_multipart_upload_result.checksum_crc32,
|
|
checksum_crc32c: complete_multipart_upload_result.checksum_crc32c,
|
|
checksum_crc64nvme: complete_multipart_upload_result.checksum_crc64nvme,
|
|
..Default::default()
|
|
})
|
|
}
|
|
}
|
|
|
|
pub struct UploadPartParams {
|
|
pub bucket_name: String,
|
|
pub object_name: String,
|
|
pub upload_id: String,
|
|
pub reader: ReaderImpl,
|
|
pub part_number: i64,
|
|
pub md5_base64: String,
|
|
pub sha256_hex: String,
|
|
pub size: i64,
|
|
//pub sse: encrypt.ServerSide,
|
|
pub stream_sha256: bool,
|
|
pub custom_header: HeaderMap,
|
|
pub trailer: HeaderMap,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::api_s3_datatypes::{CompleteMultipartUpload, CompletePart, InitiateMultipartUploadResult};
|
|
|
|
#[test]
|
|
fn complete_multipart_upload_serializes_s3_part_elements() {
|
|
// Regression for rustfs/rustfs#4811: without serde renames quick-xml emits
|
|
// <parts>/<part_num>/<etag>, so the remote parses zero <Part> elements and
|
|
// completes a 0-byte object (while still returning 200). The body must use
|
|
// S3 element names, and MD5-only transitions must not emit empty checksum
|
|
// elements.
|
|
let complete = CompleteMultipartUpload {
|
|
parts: vec![
|
|
CompletePart {
|
|
part_num: 1,
|
|
etag: "etag-one".to_string(),
|
|
..Default::default()
|
|
},
|
|
CompletePart {
|
|
part_num: 2,
|
|
etag: "etag-two".to_string(),
|
|
..Default::default()
|
|
},
|
|
],
|
|
};
|
|
let xml = complete.marshal_msg().expect("marshal");
|
|
assert!(xml.contains("<Part>"), "missing <Part>: {xml}");
|
|
assert!(xml.contains("<PartNumber>1</PartNumber>"), "missing PartNumber: {xml}");
|
|
assert!(xml.contains("<ETag>etag-one</ETag>"), "missing ETag: {xml}");
|
|
assert!(xml.contains("<PartNumber>2</PartNumber>"), "missing part 2: {xml}");
|
|
assert!(!xml.contains("part_num"), "leaked rust field name: {xml}");
|
|
assert!(!xml.contains("<ChecksumCRC32>"), "emitted empty checksum: {xml}");
|
|
}
|
|
|
|
#[test]
|
|
fn parses_create_multipart_upload_response() {
|
|
// Regression for rustfs/rustfs#4811: `initiate_multipart_upload` used to
|
|
// return a default (empty) result, so every multipart transition failed
|
|
// the first UploadPart with "UploadID cannot be empty". The UploadId must
|
|
// be parsed out of the S3 XML response.
|
|
let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<InitiateMultipartUploadResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
|
<Bucket>bar</Bucket>
|
|
<Key>foo/payload.bin</Key>
|
|
<UploadId>a1b2c3-d4e5-f6</UploadId>
|
|
</InitiateMultipartUploadResult>"#;
|
|
let parsed: InitiateMultipartUploadResult = quick_xml::de::from_str(xml).expect("parse");
|
|
assert_eq!(parsed.upload_id, "a1b2c3-d4e5-f6");
|
|
assert_eq!(parsed.bucket, "bar");
|
|
assert_eq!(parsed.key, "foo/payload.bin");
|
|
}
|
|
}
|