Skip to content

Commit 612d74f

Browse files
authored
Add PaginatedListStore (#371)
* Add PaginatedListStore * Format * More docs * Format * Fix empty Azure pagination token
1 parent 9feebb9 commit 612d74f

File tree

10 files changed

+312
-56
lines changed

10 files changed

+312
-56
lines changed

src/aws/client.rs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ use crate::client::s3::{
3333
InitiateMultipartUploadResult, ListResponse, PartMetadata,
3434
};
3535
use crate::client::{GetOptionsExt, HttpClient, HttpError, HttpResponse};
36+
use crate::list::{PaginatedListOptions, PaginatedListResult};
3637
use crate::multipart::PartId;
37-
use crate::path::DELIMITER;
3838
use crate::{
39-
Attribute, Attributes, ClientOptions, GetOptions, ListResult, MultipartId, Path,
40-
PutMultipartOpts, PutPayload, PutResult, Result, RetryConfig, TagSet,
39+
Attribute, Attributes, ClientOptions, GetOptions, MultipartId, Path, PutMultipartOpts,
40+
PutPayload, PutResult, Result, RetryConfig, TagSet,
4141
};
4242
use async_trait::async_trait;
4343
use base64::prelude::BASE64_STANDARD;
@@ -877,21 +877,19 @@ impl ListClient for Arc<S3Client> {
877877
async fn list_request(
878878
&self,
879879
prefix: Option<&str>,
880-
delimiter: bool,
881-
token: Option<&str>,
882-
offset: Option<&str>,
883-
) -> Result<(ListResult, Option<String>)> {
880+
opts: PaginatedListOptions,
881+
) -> Result<PaginatedListResult> {
884882
let credential = self.config.get_session_credential().await?;
885883
let url = self.config.bucket_endpoint.clone();
886884

887885
let mut query = Vec::with_capacity(4);
888886

889-
if let Some(token) = token {
890-
query.push(("continuation-token", token))
887+
if let Some(token) = &opts.page_token {
888+
query.push(("continuation-token", token.as_ref()))
891889
}
892890

893-
if delimiter {
894-
query.push(("delimiter", DELIMITER))
891+
if let Some(d) = &opts.delimiter {
892+
query.push(("delimiter", d.as_ref()))
895893
}
896894

897895
query.push(("list-type", "2"));
@@ -900,13 +898,20 @@ impl ListClient for Arc<S3Client> {
900898
query.push(("prefix", prefix))
901899
}
902900

903-
if let Some(offset) = offset {
904-
query.push(("start-after", offset))
901+
if let Some(offset) = &opts.offset {
902+
query.push(("start-after", offset.as_ref()))
903+
}
904+
905+
let max_keys_str;
906+
if let Some(max_keys) = &opts.max_keys {
907+
max_keys_str = max_keys.to_string();
908+
query.push(("max-keys", max_keys_str.as_ref()))
905909
}
906910

907911
let response = self
908912
.client
909913
.request(Method::GET, &url)
914+
.extensions(opts.extensions)
910915
.query(&query)
911916
.with_aws_sigv4(credential.authorizer(), None)
912917
.send_retry(&self.config.retry_config)
@@ -922,7 +927,10 @@ impl ListClient for Arc<S3Client> {
922927

923928
let token = response.next_continuation_token.take();
924929

925-
Ok((response.try_into()?, token))
930+
Ok(PaginatedListResult {
931+
result: response.try_into()?,
932+
page_token: token,
933+
})
926934
}
927935
}
928936

src/aws/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use url::Url;
3838

3939
use crate::aws::client::{CompleteMultipartMode, PutPartPayload, RequestError, S3Client};
4040
use crate::client::get::GetClientExt;
41-
use crate::client::list::ListClientExt;
41+
use crate::client::list::{ListClient, ListClientExt};
4242
use crate::client::CredentialProvider;
4343
use crate::multipart::{MultipartStore, PartId};
4444
use crate::signer::Signer;
@@ -78,6 +78,7 @@ const STORE: &str = "S3";
7878
/// [`CredentialProvider`] for [`AmazonS3`]
7979
pub type AwsCredentialProvider = Arc<dyn CredentialProvider<Credential = AwsCredential>>;
8080
use crate::client::parts::Parts;
81+
use crate::list::{PaginatedListOptions, PaginatedListResult, PaginatedListStore};
8182
pub use credential::{AwsAuthorizer, AwsCredential};
8283

8384
/// Interface for [Amazon S3](https://aws.amazon.com/s3/).
@@ -496,6 +497,17 @@ impl MultipartStore for AmazonS3 {
496497
}
497498
}
498499

500+
#[async_trait]
501+
impl PaginatedListStore for AmazonS3 {
502+
async fn list_paginated(
503+
&self,
504+
prefix: Option<&str>,
505+
opts: PaginatedListOptions,
506+
) -> Result<PaginatedListResult> {
507+
self.client.list_request(prefix, opts).await
508+
}
509+
}
510+
499511
#[cfg(test)]
500512
mod tests {
501513
use super::*;
@@ -594,6 +606,7 @@ mod tests {
594606
signing(&integration).await;
595607
s3_encryption(&integration).await;
596608
put_get_attributes(&integration).await;
609+
list_paginated(&integration, &integration).await;
597610

598611
// Object tagging is not supported by S3 Express One Zone
599612
if config.session_provider.is_none() {

src/azure/client.rs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ use crate::client::header::{get_put_result, HeaderConfig};
2424
use crate::client::list::ListClient;
2525
use crate::client::retry::RetryExt;
2626
use crate::client::{GetOptionsExt, HttpClient, HttpError, HttpRequest, HttpResponse};
27+
use crate::list::{PaginatedListOptions, PaginatedListResult};
2728
use crate::multipart::PartId;
28-
use crate::path::DELIMITER;
2929
use crate::util::{deserialize_rfc1123, GetRange};
3030
use crate::{
3131
Attribute, Attributes, ClientOptions, GetOptions, ListResult, ObjectMeta, Path, PutMode,
@@ -961,11 +961,13 @@ impl ListClient for Arc<AzureClient> {
961961
async fn list_request(
962962
&self,
963963
prefix: Option<&str>,
964-
delimiter: bool,
965-
token: Option<&str>,
966-
offset: Option<&str>,
967-
) -> Result<(ListResult, Option<String>)> {
968-
assert!(offset.is_none()); // Not yet supported
964+
opts: PaginatedListOptions,
965+
) -> Result<PaginatedListResult> {
966+
if opts.offset.is_some() {
967+
return Err(crate::Error::NotSupported {
968+
source: "Azure does not support listing with offsets".into(),
969+
});
970+
}
969971

970972
let credential = self.get_credential().await?;
971973
let url = self.config.path_url(&Path::default());
@@ -978,21 +980,29 @@ impl ListClient for Arc<AzureClient> {
978980
query.push(("prefix", prefix))
979981
}
980982

981-
if delimiter {
982-
query.push(("delimiter", DELIMITER))
983+
if let Some(delimiter) = &opts.delimiter {
984+
query.push(("delimiter", delimiter.as_ref()))
983985
}
984986

985-
if let Some(token) = token {
986-
query.push(("marker", token))
987+
if let Some(token) = &opts.page_token {
988+
query.push(("marker", token.as_ref()))
989+
}
990+
991+
let max_keys_str;
992+
if let Some(max_keys) = &opts.max_keys {
993+
max_keys_str = max_keys.to_string();
994+
query.push(("maxresults", max_keys_str.as_ref()))
987995
}
988996

989997
let sensitive = credential
990998
.as_deref()
991999
.map(|c| c.sensitive_request())
9921000
.unwrap_or_default();
1001+
9931002
let response = self
9941003
.client
9951004
.get(url.as_str())
1005+
.extensions(opts.extensions)
9961006
.query(&query)
9971007
.with_azure_authorization(&credential, &self.config.account)
9981008
.retryable(&self.config.retry_config)
@@ -1008,9 +1018,12 @@ impl ListClient for Arc<AzureClient> {
10081018
let mut response: ListResultInternal = quick_xml::de::from_reader(response.reader())
10091019
.map_err(|source| Error::InvalidListResponse { source })?;
10101020

1011-
let token = response.next_marker.take();
1021+
let token = response.next_marker.take().filter(|x| !x.is_empty());
10121022

1013-
Ok((to_list_result(response, prefix)?, token))
1023+
Ok(PaginatedListResult {
1024+
result: to_list_result(response, prefix)?,
1025+
page_token: token,
1026+
})
10141027
}
10151028
}
10161029

src/azure/mod.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use std::time::Duration;
3838
use url::Url;
3939

4040
use crate::client::get::GetClientExt;
41-
use crate::client::list::ListClientExt;
41+
use crate::client::list::{ListClient, ListClientExt};
4242
use crate::client::CredentialProvider;
4343
pub use credential::{authority_hosts, AzureAccessKey, AzureAuthorizer};
4444

@@ -50,6 +50,7 @@ mod credential;
5050
pub type AzureCredentialProvider = Arc<dyn CredentialProvider<Credential = AzureCredential>>;
5151
use crate::azure::client::AzureClient;
5252
use crate::client::parts::Parts;
53+
use crate::list::{PaginatedListOptions, PaginatedListResult, PaginatedListStore};
5354
pub use builder::{AzureConfigKey, MicrosoftAzureBuilder};
5455
pub use credential::AzureCredential;
5556

@@ -292,6 +293,17 @@ impl MultipartStore for MicrosoftAzure {
292293
}
293294
}
294295

296+
#[async_trait]
297+
impl PaginatedListStore for MicrosoftAzure {
298+
async fn list_paginated(
299+
&self,
300+
prefix: Option<&str>,
301+
opts: PaginatedListOptions,
302+
) -> Result<PaginatedListResult> {
303+
self.client.list_request(prefix, opts).await
304+
}
305+
}
306+
295307
#[cfg(test)]
296308
mod tests {
297309
use super::*;
@@ -316,6 +328,7 @@ mod tests {
316328
multipart_race_condition(&integration, false).await;
317329
multipart_out_of_order(&integration).await;
318330
signing(&integration).await;
331+
list_paginated(&integration, &integration).await;
319332

320333
let validate = !integration.client.config().disable_tagging;
321334
tagging(

src/client/list.rs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@
1616
// under the License.
1717

1818
use crate::client::pagination::stream_paginated;
19-
use crate::path::Path;
19+
use crate::list::{PaginatedListOptions, PaginatedListResult};
20+
use crate::path::{Path, DELIMITER};
2021
use crate::Result;
2122
use crate::{ListResult, ObjectMeta};
2223
use async_trait::async_trait;
2324
use futures::stream::BoxStream;
2425
use futures::{StreamExt, TryStreamExt};
26+
use std::borrow::Cow;
2527
use std::collections::BTreeSet;
2628

2729
/// A client that can perform paginated list requests
@@ -30,10 +32,8 @@ pub(crate) trait ListClient: Send + Sync + 'static {
3032
async fn list_request(
3133
&self,
3234
prefix: Option<&str>,
33-
delimiter: bool,
34-
token: Option<&str>,
35-
offset: Option<&str>,
36-
) -> Result<(ListResult, Option<String>)>;
35+
options: PaginatedListOptions,
36+
) -> Result<PaginatedListResult>;
3737
}
3838

3939
/// Extension trait for [`ListClient`] that adds common listing functionality
@@ -69,21 +69,23 @@ impl<T: ListClient + Clone> ListClientExt for T {
6969
let offset = offset.map(|x| x.to_string());
7070
let prefix = prefix
7171
.filter(|x| !x.as_ref().is_empty())
72-
.map(|p| format!("{}{}", p.as_ref(), crate::path::DELIMITER));
73-
72+
.map(|p| format!("{}{}", p.as_ref(), DELIMITER));
7473
stream_paginated(
7574
self.clone(),
7675
(prefix, offset),
77-
move |client, (prefix, offset), token| async move {
78-
let (r, next_token) = client
76+
move |client, (prefix, offset), page_token| async move {
77+
let r = client
7978
.list_request(
8079
prefix.as_deref(),
81-
delimiter,
82-
token.as_deref(),
83-
offset.as_deref(),
80+
PaginatedListOptions {
81+
offset: offset.clone(),
82+
delimiter: delimiter.then_some(Cow::Borrowed(DELIMITER)),
83+
page_token,
84+
..Default::default()
85+
},
8486
)
8587
.await?;
86-
Ok((r, (prefix, offset), next_token))
88+
Ok((r.result, (prefix, offset), r.page_token))
8789
},
8890
)
8991
.boxed()

src/gcp/client.rs

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ use crate::client::s3::{
2727
use crate::client::{GetOptionsExt, HttpClient, HttpError, HttpResponse};
2828
use crate::gcp::credential::CredentialExt;
2929
use crate::gcp::{GcpCredential, GcpCredentialProvider, GcpSigningCredentialProvider, STORE};
30+
use crate::list::{PaginatedListOptions, PaginatedListResult};
3031
use crate::multipart::PartId;
31-
use crate::path::{Path, DELIMITER};
32+
use crate::path::Path;
3233
use crate::util::hex_encode;
3334
use crate::{
34-
Attribute, Attributes, ClientOptions, GetOptions, ListResult, MultipartId, PutMode,
35-
PutMultipartOpts, PutOptions, PutPayload, PutResult, Result, RetryConfig,
35+
Attribute, Attributes, ClientOptions, GetOptions, MultipartId, PutMode, PutMultipartOpts,
36+
PutOptions, PutPayload, PutResult, Result, RetryConfig,
3637
};
3738
use async_trait::async_trait;
3839
use base64::prelude::BASE64_STANDARD;
@@ -652,38 +653,43 @@ impl ListClient for Arc<GoogleCloudStorageClient> {
652653
async fn list_request(
653654
&self,
654655
prefix: Option<&str>,
655-
delimiter: bool,
656-
page_token: Option<&str>,
657-
offset: Option<&str>,
658-
) -> Result<(ListResult, Option<String>)> {
656+
opts: PaginatedListOptions,
657+
) -> Result<PaginatedListResult> {
659658
let credential = self.get_credential().await?;
660659
let url = format!("{}/{}", self.config.base_url, self.bucket_name_encoded);
661660

662661
let mut query = Vec::with_capacity(5);
663662
query.push(("list-type", "2"));
664-
if delimiter {
665-
query.push(("delimiter", DELIMITER))
663+
if let Some(delimiter) = &opts.delimiter {
664+
query.push(("delimiter", delimiter.as_ref()))
666665
}
667666

668-
if let Some(prefix) = &prefix {
667+
if let Some(prefix) = prefix {
669668
query.push(("prefix", prefix))
670669
}
671670

672-
if let Some(page_token) = page_token {
671+
if let Some(page_token) = &opts.page_token {
673672
query.push(("continuation-token", page_token))
674673
}
675674

676675
if let Some(max_results) = &self.max_list_results {
677676
query.push(("max-keys", max_results))
678677
}
679678

680-
if let Some(offset) = offset {
681-
query.push(("start-after", offset))
679+
if let Some(offset) = &opts.offset {
680+
query.push(("start-after", offset.as_ref()))
681+
}
682+
683+
let max_keys_str;
684+
if let Some(max_keys) = &opts.max_keys {
685+
max_keys_str = max_keys.to_string();
686+
query.push(("max-keys", max_keys_str.as_ref()))
682687
}
683688

684689
let response = self
685690
.client
686691
.request(Method::GET, url)
692+
.extensions(opts.extensions)
687693
.query(&query)
688694
.with_bearer_auth(credential.as_deref())
689695
.send_retry(&self.config.retry_config)
@@ -698,6 +704,9 @@ impl ListClient for Arc<GoogleCloudStorageClient> {
698704
.map_err(|source| Error::InvalidListResponse { source })?;
699705

700706
let token = response.next_continuation_token.take();
701-
Ok((response.try_into()?, token))
707+
Ok(PaginatedListResult {
708+
result: response.try_into()?,
709+
page_token: token,
710+
})
702711
}
703712
}

0 commit comments

Comments
 (0)