mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-11 23:56:53 +00:00
fix(ftps): Fix basename extraction and implement recursive delete (#1920)
Co-authored-by: houseme <housemecn@gmail.com> Co-authored-by: heihutu <30542132+heihutu@users.noreply.github.com>
This commit is contained in:
@@ -141,6 +141,80 @@ where
|
|||||||
|
|
||||||
Ok((bucket, key))
|
Ok((bucket, key))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Recursively delete all objects in a bucket, then delete the bucket itself.
|
||||||
|
async fn delete_bucket_recursively(
|
||||||
|
&self,
|
||||||
|
bucket: &str,
|
||||||
|
session_context: &crate::common::session::SessionContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
// First, delete all objects in the bucket (with pagination)
|
||||||
|
let mut continuation_token = None;
|
||||||
|
loop {
|
||||||
|
let mut list_input = ListObjectsV2Input::builder().bucket(bucket.to_string());
|
||||||
|
|
||||||
|
if let Some(token) = continuation_token {
|
||||||
|
list_input = list_input.continuation_token(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
let list_input = list_input.build().map_err(|e| {
|
||||||
|
Error::new(ErrorKind::PermanentFileNotAvailable, format!("Failed to build ListObjectsV2Input: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if let Ok(output) = self
|
||||||
|
.storage
|
||||||
|
.list_objects_v2(
|
||||||
|
list_input,
|
||||||
|
&session_context.principal.user_identity.credentials.access_key,
|
||||||
|
&session_context.principal.user_identity.credentials.secret_key,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
// Delete all objects in this page
|
||||||
|
if let Some(objects) = output.contents {
|
||||||
|
for obj in objects {
|
||||||
|
if let Some(obj_key) = obj.key {
|
||||||
|
let _ = self
|
||||||
|
.storage
|
||||||
|
.delete_object(
|
||||||
|
bucket,
|
||||||
|
&obj_key,
|
||||||
|
&session_context.principal.user_identity.credentials.access_key,
|
||||||
|
&session_context.principal.user_identity.credentials.secret_key,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are more objects
|
||||||
|
if !output.is_truncated.unwrap_or(false) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continuation_token = Some(output.next_continuation_token);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then delete the bucket
|
||||||
|
match self
|
||||||
|
.storage
|
||||||
|
.delete_bucket(
|
||||||
|
bucket,
|
||||||
|
&session_context.principal.user_identity.credentials.access_key,
|
||||||
|
&session_context.principal.user_identity.credentials.secret_key,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) if e.to_string().contains("NoSuchBucket") => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to delete bucket '{}': {}", bucket, e);
|
||||||
|
Err(Error::new(ErrorKind::PermanentFileNotAvailable, format!("Delete bucket failed: {}", e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -240,9 +314,13 @@ where
|
|||||||
.await
|
.await
|
||||||
.map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?;
|
.map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?;
|
||||||
|
|
||||||
|
let prefix_with_slash = prefix
|
||||||
|
.clone()
|
||||||
|
.map(|p| if p.ends_with('/') { p.to_string() } else { format!("{}/", p) });
|
||||||
|
|
||||||
let list_input = ListObjectsV2Input::builder()
|
let list_input = ListObjectsV2Input::builder()
|
||||||
.bucket(bucket)
|
.bucket(bucket)
|
||||||
.prefix(prefix.map(|p| p.to_string()))
|
.prefix(prefix_with_slash.clone())
|
||||||
.delimiter(Some("/".to_string()))
|
.delimiter(Some("/".to_string()))
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
@@ -265,7 +343,27 @@ where
|
|||||||
if let Some(objects) = output.contents {
|
if let Some(objects) = output.contents {
|
||||||
for obj in objects {
|
for obj in objects {
|
||||||
if let Some(key) = obj.key {
|
if let Some(key) = obj.key {
|
||||||
let filename = PathBuf::from(key.as_str());
|
// Filter: only show files directly in current directory
|
||||||
|
// Skip files in subdirectories (they should be accessed via cd)
|
||||||
|
let should_show = if prefix.is_none() {
|
||||||
|
// Root directory: only show files without "/"
|
||||||
|
!key.contains('/')
|
||||||
|
} else {
|
||||||
|
// Subdirectory: show files starting with prefix
|
||||||
|
key.starts_with(&prefix_with_slash.clone().unwrap_or_default())
|
||||||
|
};
|
||||||
|
|
||||||
|
if !should_show {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let filename = PathBuf::from(key.as_str())
|
||||||
|
.file_name()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::new(ErrorKind::PermanentFileNotAvailable, format!("Invalid filename: {}", key))
|
||||||
|
})
|
||||||
|
.map(PathBuf::from)?;
|
||||||
|
|
||||||
let size = obj.size.unwrap_or(0) as u64;
|
let size = obj.size.unwrap_or(0) as u64;
|
||||||
let modified = obj.last_modified.map(|dt: s3s::dto::Timestamp| {
|
let modified = obj.last_modified.map(|dt: s3s::dto::Timestamp| {
|
||||||
// Convert s3s Timestamp to SystemTime
|
// Convert s3s Timestamp to SystemTime
|
||||||
@@ -291,7 +389,13 @@ where
|
|||||||
if let Some(common_prefixes) = output.common_prefixes {
|
if let Some(common_prefixes) = output.common_prefixes {
|
||||||
for prefix in common_prefixes {
|
for prefix in common_prefixes {
|
||||||
if let Some(prefix_str) = prefix.prefix {
|
if let Some(prefix_str) = prefix.prefix {
|
||||||
let dir_name = PathBuf::from(prefix_str.as_str().trim_end_matches('/'));
|
let dir_name = PathBuf::from(prefix_str.as_str().trim_end_matches('/'))
|
||||||
|
.file_name()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::new(ErrorKind::PermanentFileNotAvailable, format!("Invalid directory: {}", prefix_str))
|
||||||
|
})
|
||||||
|
.map(PathBuf::from)?;
|
||||||
|
|
||||||
let metadata = FtpsMetadata {
|
let metadata = FtpsMetadata {
|
||||||
size: 0,
|
size: 0,
|
||||||
modified: Some(std::time::SystemTime::now()),
|
modified: Some(std::time::SystemTime::now()),
|
||||||
@@ -455,6 +559,11 @@ where
|
|||||||
.map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?;
|
.map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?;
|
||||||
|
|
||||||
if let Some(key) = key {
|
if let Some(key) = key {
|
||||||
|
// Authorize delete object
|
||||||
|
authorize_operation(session_context, &S3Action::DeleteObject, &bucket, Some(&key))
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?;
|
||||||
|
|
||||||
// Delete file
|
// Delete file
|
||||||
match self
|
match self
|
||||||
.storage
|
.storage
|
||||||
@@ -473,8 +582,18 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Delete directory (bucket) - not supported in typical FTP
|
// Delete directory (bucket)
|
||||||
Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Directory deletion not supported"))
|
// If path ends with '/', treat it as bucket deletion request
|
||||||
|
if path_str.ends_with('/') {
|
||||||
|
// Authorize delete bucket
|
||||||
|
authorize_operation(session_context, &S3Action::DeleteBucket, &bucket, None)
|
||||||
|
.await
|
||||||
|
.map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?;
|
||||||
|
|
||||||
|
self.delete_bucket_recursively(&bucket, session_context).await
|
||||||
|
} else {
|
||||||
|
Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Directory deletion not supported"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,23 +635,27 @@ where
|
|||||||
.parse_s3_path(&path_str)
|
.parse_s3_path(&path_str)
|
||||||
.map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?;
|
.map_err(|e| Error::new(ErrorKind::PermanentFileNotAvailable, format!("{}: {}", "Invalid path", e)))?;
|
||||||
|
|
||||||
// Delete bucket for directory
|
// Authorize delete bucket
|
||||||
match self
|
authorize_operation(session_context, &S3Action::DeleteBucket, &bucket, None)
|
||||||
.storage
|
|
||||||
.delete_bucket(
|
|
||||||
&bucket,
|
|
||||||
&session_context.principal.user_identity.credentials.access_key,
|
|
||||||
&session_context.principal.user_identity.credentials.secret_key,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
{
|
.map_err(|_| Error::new(ErrorKind::PermanentFileNotAvailable, "Access denied"))?;
|
||||||
|
|
||||||
|
// Try to delete bucket recursively
|
||||||
|
match self.delete_bucket_recursively(&bucket, session_context).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
debug!("Successfully removed directory/bucket '{}'", path_str);
|
debug!("Successfully removed directory/bucket '{}'", path_str);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to remove directory/bucket '{}': {}", path_str, e);
|
// Check if error is NoSuchBucket - treat as success (idempotent)
|
||||||
Err(Error::new(ErrorKind::PermanentFileNotAvailable, format!("Rmdir failed: {}", e)))
|
let error_msg = e.to_string();
|
||||||
|
if error_msg.contains("NoSuchBucket") || error_msg.contains("does not exist") {
|
||||||
|
debug!("Bucket '{}' already deleted", bucket);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
error!("Failed to remove directory/bucket '{}': {}", path_str, e);
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -568,6 +691,9 @@ where
|
|||||||
let to_str = to.as_ref().to_string_lossy();
|
let to_str = to.as_ref().to_string_lossy();
|
||||||
debug!("FTPS rename request for user '{}' from '{}' to '{}'", user.username, from_str, to_str);
|
debug!("FTPS rename request for user '{}' from '{}' to '{}'", user.username, from_str, to_str);
|
||||||
|
|
||||||
Err(Error::new(ErrorKind::PermanentFileNotAvailable, "Atomic rename not supported in S3"))
|
Err(Error::new(
|
||||||
|
ErrorKind::CommandNotImplemented,
|
||||||
|
"Rename operation not supported in S3 backend",
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user