Fix DELETE cleanup cost with single-syscall rmdir walk, tighten FS segment validation, stream CopyObject

This commit is contained in:
2026-04-24 15:04:16 +08:00
parent 1ea6dfae07
commit 4f05192548
3 changed files with 65 additions and 49 deletions

View File

@@ -2395,7 +2395,7 @@ async fn copy_object_handler(
source_metadata_existing.clone() source_metadata_existing.clone()
}; };
let (_meta, mut reader) = match src_version_id.as_deref() { let (_meta, reader) = match src_version_id.as_deref() {
Some(version_id) if version_id != "null" => { Some(version_id) if version_id != "null" => {
match state match state
.storage .storage
@@ -2411,19 +2411,10 @@ async fn copy_object_handler(
Err(e) => return storage_err_response(e), Err(e) => return storage_err_response(e),
}, },
}; };
let mut data = Vec::new();
if let Err(e) = reader.read_to_end(&mut data).await {
return storage_err_response(myfsio_storage::error::StorageError::Io(e));
}
let copy_result = state let copy_result = state
.storage .storage
.put_object( .put_object(dst_bucket, dst_key, reader, Some(dst_metadata))
dst_bucket,
dst_key,
Box::pin(std::io::Cursor::new(data)),
Some(dst_metadata),
)
.await; .await;
match copy_result { match copy_result {

View File

@@ -882,17 +882,11 @@ impl FsStorageBackend {
fn cleanup_empty_parents(path: &Path, stop_at: &Path) { fn cleanup_empty_parents(path: &Path, stop_at: &Path) {
let mut parent = path.parent(); let mut parent = path.parent();
while let Some(p) = parent { while let Some(p) = parent {
if p == stop_at || !p.exists() { if p == stop_at {
break; break;
} }
match std::fs::read_dir(p) { if std::fs::remove_dir(p).is_err() {
Ok(mut entries) => { break;
if entries.next().is_some() {
break;
}
let _ = std::fs::remove_dir(p);
}
Err(_) => break,
} }
parent = p.parent(); parent = p.parent();
} }
@@ -1937,29 +1931,6 @@ impl FsStorageBackend {
obj.version_id = new_version_id; obj.version_id = new_version_id;
Ok(obj) Ok(obj)
} }
fn put_object_sync(
&self,
bucket_name: &str,
key: &str,
data: &[u8],
metadata: Option<HashMap<String, String>>,
) -> StorageResult<ObjectMeta> {
self.validate_key(key)?;
let tmp_dir = self.tmp_dir();
std::fs::create_dir_all(&tmp_dir).map_err(StorageError::Io)?;
let tmp_path = tmp_dir.join(format!("{}.tmp", Uuid::new_v4()));
let mut hasher = Md5::new();
hasher.update(data);
let etag = format!("{:x}", hasher.finalize());
std::fs::write(&tmp_path, data).map_err(StorageError::Io)?;
let new_size = data.len() as u64;
self.finalize_put_sync(bucket_name, key, &tmp_path, etag, new_size, metadata)
}
} }
impl crate::traits::StorageEngine for FsStorageBackend { impl crate::traits::StorageEngine for FsStorageBackend {
@@ -2453,9 +2424,54 @@ impl crate::traits::StorageEngine for FsStorageBackend {
}); });
} }
let data = std::fs::read(&src_path).map_err(StorageError::Io)?; self.validate_key(dst_key)?;
let tmp_dir = self.tmp_dir();
std::fs::create_dir_all(&tmp_dir).map_err(StorageError::Io)?;
let tmp_path = tmp_dir.join(format!("{}.tmp", Uuid::new_v4()));
let (etag, new_size) = {
use std::io::{BufReader, BufWriter, Read, Write};
let src_file = std::fs::File::open(&src_path).map_err(|e| {
let _ = std::fs::remove_file(&tmp_path);
StorageError::Io(e)
})?;
let mut reader = BufReader::with_capacity(256 * 1024, src_file);
let tmp_file = std::fs::File::create(&tmp_path).map_err(StorageError::Io)?;
let mut writer = BufWriter::with_capacity(256 * 1024, tmp_file);
let mut hasher = Md5::new();
let mut buf = [0u8; 256 * 1024];
let mut total: u64 = 0;
loop {
let n = reader.read(&mut buf).map_err(|e| {
let _ = std::fs::remove_file(&tmp_path);
StorageError::Io(e)
})?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
writer.write_all(&buf[..n]).map_err(|e| {
let _ = std::fs::remove_file(&tmp_path);
StorageError::Io(e)
})?;
total += n as u64;
}
writer.flush().map_err(|e| {
let _ = std::fs::remove_file(&tmp_path);
StorageError::Io(e)
})?;
(format!("{:x}", hasher.finalize()), total)
};
let src_metadata = self.read_metadata_sync(src_bucket, src_key); let src_metadata = self.read_metadata_sync(src_bucket, src_key);
self.put_object_sync(dst_bucket, dst_key, &data, Some(src_metadata)) self.finalize_put_sync(
dst_bucket,
dst_key,
&tmp_path,
etag,
new_size,
Some(src_metadata),
)
} }
async fn get_object_metadata( async fn get_object_metadata(

View File

@@ -63,7 +63,8 @@ pub fn validate_object_key(
if part.len() > 255 { if part.len() > 255 {
return Some( return Some(
"Object key contains a path segment that exceeds 255 bytes".to_string(), "Object key contains a path segment longer than 255 bytes (filesystem backend limit)"
.to_string(),
); );
} }
@@ -197,10 +198,18 @@ mod tests {
#[test] #[test]
fn test_object_key_max_length() { fn test_object_key_max_length() {
let long_key = "a".repeat(1025); let too_long_total = "a/".repeat(513) + "a";
assert!(validate_object_key(&long_key, 1024, false, None).is_some()); assert!(validate_object_key(&too_long_total, 1024, false, None).is_some());
let ok_key = "a".repeat(1024);
let too_long_segment = "a".repeat(256);
assert!(validate_object_key(&too_long_segment, 1024, false, None).is_some());
let ok_key = vec!["a".repeat(255); 4].join("/");
assert_eq!(ok_key.len(), 255 * 4 + 3);
assert!(validate_object_key(&ok_key, 1024, false, None).is_none()); assert!(validate_object_key(&ok_key, 1024, false, None).is_none());
let ok_max_segment = "a".repeat(255);
assert!(validate_object_key(&ok_max_segment, 1024, false, None).is_none());
} }
#[test] #[test]