feat(mongodb): improve GridFS manager filtering and sorting

* docs: add GridFS manager filter and sort design

* feat: improve GridFS manager filtering and sorting
This commit is contained in:
LSD authored and GitHub committed 2026-07-06 17:17:33 +08:00
1 parent 26647ff755
commit fa5e55a376
14 files changed
+1294 -40

No files matched your search

+127 -3
View File
@@ -122,6 +122,78 @@ fn mongo_bucket_infos(names: &[String]) -> Vec<CollectionInfo> {
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GridFsBucketSortField {
Name,
FileCount,
TotalBytes,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct GridFsBucketSort {
field: GridFsBucketSortField,
descending: bool,
}
fn parse_gridfs_bucket_sort(sort: Option<&str>) -> Result<GridFsBucketSort, String> {
let Some(raw) = sort.map(str::trim).filter(|value| !value.is_empty()) else {
return Ok(GridFsBucketSort { field: GridFsBucketSortField::Name, descending: false });
};
let value: serde_json::Value =
serde_json::from_str(raw).map_err(|e| format!("Invalid GridFS bucket sort JSON: {e}"))?;
let object = value.as_object().ok_or_else(|| "GridFS bucket sort must be a JSON object".to_string())?;
if object.len() != 1 {
return Err("GridFS bucket sort must contain exactly one field".to_string());
}
let (field_name, direction) = object.iter().next().expect("checked len");
let field = match field_name.as_str() {
"name" => GridFsBucketSortField::Name,
"fileCount" => GridFsBucketSortField::FileCount,
"totalBytes" => GridFsBucketSortField::TotalBytes,
_ => return Err(format!("Unsupported GridFS bucket sort field: {field_name}")),
};
let descending = match direction {
serde_json::Value::Number(value) if value.as_i64() == Some(-1) => true,
serde_json::Value::Number(value) if value.as_i64() == Some(1) => false,
serde_json::Value::String(value) if value.eq_ignore_ascii_case("desc") || value == "-1" => true,
serde_json::Value::String(value) if value.eq_ignore_ascii_case("asc") || value == "1" => false,
_ => return Err("GridFS bucket sort direction must be 1, -1, 'asc', or 'desc'".to_string()),
};
Ok(GridFsBucketSort { field, descending })
}
fn filter_and_sort_gridfs_bucket_infos(
mut buckets: Vec<MongoGridFsBucketInfo>,
filter: Option<&str>,
sort: Option<&str>,
) -> Result<Vec<MongoGridFsBucketInfo>, String> {
if let Some(filter_text) = filter.map(str::trim).filter(|value| !value.is_empty()) {
let needle = filter_text.to_lowercase();
buckets.retain(|bucket| bucket.name.to_lowercase().contains(&needle));
}
let sort = parse_gridfs_bucket_sort(sort)?;
buckets.sort_by(|left, right| {
let name_cmp =
left.name.to_lowercase().cmp(&right.name.to_lowercase()).then_with(|| left.name.cmp(&right.name));
let ordering = match sort.field {
GridFsBucketSortField::Name => name_cmp,
GridFsBucketSortField::FileCount => left.file_count.cmp(&right.file_count).then_with(|| name_cmp),
GridFsBucketSortField::TotalBytes => left.total_bytes.cmp(&right.total_bytes).then_with(|| name_cmp),
};
if sort.descending {
ordering.reverse()
} else {
ordering
}
});
Ok(buckets)
}
pub async fn list_collections_core(
state: &AppState,
connection_id: &str,
@@ -160,11 +232,13 @@ pub async fn list_gridfs_files_core(
connection_id: &str,
database: &str,
bucket: &str,
filter: Option<&str>,
sort: Option<&str>,
) -> Result<Vec<MongoGridFsFileInfo>, String> {
ensure_document_pool(state, connection_id).await?;
let connections = state.connections.read().await;
match connections.get(connection_id).ok_or("Not found")? {
PoolKind::MongoDb(client) => mongo_driver::list_gridfs_files(client, database, bucket).await,
PoolKind::MongoDb(client) => mongo_driver::list_gridfs_files(client, database, bucket, filter, sort).await,
PoolKind::Agent(_) => Err("MongoDB legacy agent does not support GridFS file browsing".to_string()),
_ => Err("Not a MongoDB connection".to_string()),
}
@@ -174,6 +248,8 @@ pub async fn list_gridfs_buckets_core(
state: &AppState,
connection_id: &str,
database: &str,
filter: Option<&str>,
sort: Option<&str>,
) -> Result<Vec<MongoGridFsBucketInfo>, String> {
ensure_document_pool(state, connection_id).await?;
let connections = state.connections.read().await;
@@ -185,7 +261,7 @@ pub async fn list_gridfs_buckets_core(
for bucket_name in bucket_names {
buckets.push(mongo_driver::gridfs_bucket_summary(client, database, &bucket_name).await?);
}
Ok(buckets)
filter_and_sort_gridfs_bucket_infos(buckets, filter, sort)
}
PoolKind::Agent(_) => Err("MongoDB legacy agent does not support GridFS bucket browsing".to_string()),
_ => Err("Not a MongoDB connection".to_string()),
@@ -420,7 +496,10 @@ pub async fn delete_document_core(
#[cfg(test)]
mod tests {
use super::{fallback_mongo_database, mongo_gridfs_bucket_names, mongo_list_databases_unauthorized, sort_names};
use super::{
fallback_mongo_database, filter_and_sort_gridfs_bucket_infos, mongo_gridfs_bucket_names,
mongo_list_databases_unauthorized, parse_gridfs_bucket_sort, sort_names, MongoGridFsBucketInfo,
};
#[test]
fn sorts_names_case_insensitively() {
@@ -464,4 +543,49 @@ mod tests {
assert_eq!(buckets, vec!["orders".to_string(), "reports".to_string()]);
}
#[test]
fn filters_gridfs_buckets_by_case_insensitive_name_match() {
let buckets = filter_and_sort_gridfs_bucket_infos(
vec![
MongoGridFsBucketInfo { name: "images".to_string(), file_count: 4, total_bytes: 512 },
MongoGridFsBucketInfo { name: "nightly-reports".to_string(), file_count: 9, total_bytes: 4096 },
MongoGridFsBucketInfo { name: "videos".to_string(), file_count: 2, total_bytes: 8192 },
],
Some("REPORT"),
None,
)
.unwrap();
assert_eq!(
buckets.into_iter().map(|bucket| bucket.name).collect::<Vec<_>>(),
vec!["nightly-reports".to_string()]
);
}
#[test]
fn sorts_gridfs_buckets_by_total_bytes_descending() {
let buckets = filter_and_sort_gridfs_bucket_infos(
vec![
MongoGridFsBucketInfo { name: "images".to_string(), file_count: 4, total_bytes: 512 },
MongoGridFsBucketInfo { name: "nightly-reports".to_string(), file_count: 9, total_bytes: 4096 },
MongoGridFsBucketInfo { name: "videos".to_string(), file_count: 2, total_bytes: 8192 },
],
None,
Some(r#"{"totalBytes":-1}"#),
)
.unwrap();
assert_eq!(
buckets.into_iter().map(|bucket| bucket.name).collect::<Vec<_>>(),
vec!["videos".to_string(), "nightly-reports".to_string(), "images".to_string()]
);
}
#[test]
fn gridfs_bucket_sort_rejects_unknown_fields() {
let error = parse_gridfs_bucket_sort(Some(r#"{"createdAt":-1}"#)).unwrap_err();
assert!(error.contains("Unsupported GridFS bucket sort field"));
}
}