use std::collections::VecDeque;
use serde::Serialize;
use crate::clients::base::{
delete_resource, get_raw, get_resource, get_resource_required, head_exists, put_raw,
update_resource, ResourceContext,
};
use crate::common::{
create_hmac_signature, encode_path_segment, sign_storage_content, QueryParams,
};
use crate::error::ApifyClientResult;
use crate::http_client::{HttpClient, HttpMethod, HttpRequest};
use crate::models::{KeyValueStore, KeyValueStoreKey, KeyValueStoreKeysPage, KeyValueStoreRecord};
#[derive(Debug, Default, Clone)]
pub struct ListKeysOptions {
pub limit: Option<i64>,
pub exclusive_start_key: Option<String>,
pub prefix: Option<String>,
pub collection: Option<String>,
pub signature: Option<String>,
}
#[derive(Debug, Default, Clone)]
pub struct GetRecordOptions {
pub attachment: Option<bool>,
pub signature: Option<String>,
}
#[derive(Debug, Clone)]
pub struct KeyValueStoreClient {
ctx: ResourceContext,
}
impl KeyValueStoreClient {
pub(crate) fn new(http: HttpClient, base_url: &str, resource_path: &str, id: &str) -> Self {
Self {
ctx: ResourceContext::single(http, base_url, resource_path, id),
}
}
pub(crate) fn nested(http: HttpClient, base_url: &str, sub_path: &str) -> Self {
Self {
ctx: ResourceContext::collection(http, base_url, sub_path),
}
}
pub(crate) fn with_public_base(mut self, public_base_url: &str) -> Self {
self.ctx = self.ctx.with_public_origin(public_base_url);
self
}
pub async fn get(&self) -> ApifyClientResult<Option<KeyValueStore>> {
get_resource(&self.ctx, None, &QueryParams::new()).await
}
pub async fn update<T: Serialize>(&self, new_fields: &T) -> ApifyClientResult<KeyValueStore> {
update_resource(&self.ctx, None, new_fields).await
}
pub async fn delete(&self) -> ApifyClientResult<()> {
delete_resource(&self.ctx, None).await
}
pub async fn list_keys(
&self,
options: ListKeysOptions,
) -> ApifyClientResult<KeyValueStoreKeysPage> {
let mut params = QueryParams::new();
params
.add_int("limit", options.limit)
.add_str("exclusiveStartKey", options.exclusive_start_key)
.add_str("prefix", options.prefix)
.add_str("collection", options.collection)
.add_str("signature", options.signature);
get_resource_required(&self.ctx, Some("keys"), ¶ms).await
}
pub fn iterate_keys(&self, options: ListKeysOptions) -> KeyValueStoreKeysIterator {
let remaining = options.limit.filter(|&l| l > 0);
KeyValueStoreKeysIterator {
client: self.clone(),
options,
remaining,
next_exclusive_start_key: None,
buffer: VecDeque::new(),
first_page: true,
exhausted: false,
}
}
pub async fn record_exists(&self, key: &str) -> ApifyClientResult<bool> {
head_exists(
&self.ctx,
Some(&format!("records/{}", encode_path_segment(key))),
&QueryParams::new(),
)
.await
}
pub async fn get_record(&self, key: &str) -> ApifyClientResult<Option<KeyValueStoreRecord>> {
self.get_record_with_options(key, GetRecordOptions::default())
.await
}
pub async fn get_record_with_options(
&self,
key: &str,
options: GetRecordOptions,
) -> ApifyClientResult<Option<KeyValueStoreRecord>> {
let mut params = QueryParams::new();
params
.add_bool("attachment", Some(options.attachment.unwrap_or(true)))
.add_str("signature", options.signature);
let response = get_raw(
&self.ctx,
Some(&format!("records/{}", encode_path_segment(key))),
¶ms,
)
.await?;
Ok(response.map(|r| {
let content_type = r.header("content-type").map(|s| s.to_string());
KeyValueStoreRecord {
key: key.to_string(),
value: r.body,
content_type,
}
}))
}
pub async fn set_record_raw(
&self,
key: &str,
value: Vec<u8>,
content_type: &str,
) -> ApifyClientResult<()> {
put_raw(
&self.ctx,
Some(&format!("records/{}", encode_path_segment(key))),
&QueryParams::new(),
value,
content_type,
)
.await
}
pub async fn set_record_json<T: Serialize>(
&self,
key: &str,
value: &T,
) -> ApifyClientResult<()> {
let bytes = serde_json::to_vec(value)?;
self.set_record_raw(key, bytes, "application/json; charset=utf-8")
.await
}
pub async fn get_record_public_url(&self, key: &str) -> ApifyClientResult<String> {
let mut params = QueryParams::new();
if let Some(store) = self.get().await? {
if let Some(secret) = store
.extra
.get("urlSigningSecretKey")
.and_then(|v| v.as_str())
{
params.add_str("signature", Some(create_hmac_signature(secret, key)));
}
}
Ok(params.apply_to_url(
&self
.ctx
.public_url(Some(&format!("records/{}", encode_path_segment(key)))),
))
}
pub async fn create_keys_public_url(
&self,
expires_in_secs: Option<i64>,
) -> ApifyClientResult<String> {
let mut params = QueryParams::new();
if let Some(store) = self.get().await? {
if let Some(secret) = store
.extra
.get("urlSigningSecretKey")
.and_then(|v| v.as_str())
{
let signature = sign_storage_content(secret, &store.id, expires_in_secs);
params.add_str("signature", Some(signature));
}
}
Ok(params.apply_to_url(&self.ctx.public_url(Some("keys"))))
}
pub async fn delete_record(&self, key: &str) -> ApifyClientResult<()> {
let url = self
.ctx
.url(Some(&format!("records/{}", encode_path_segment(key))));
self.ctx
.http
.call(HttpRequest {
method: HttpMethod::Delete,
url,
headers: Default::default(),
body: None,
timeout: crate::clients::base::DEFAULT_REQUEST_TIMEOUT,
})
.await?;
Ok(())
}
}
pub const KEY_LIST_MAX_LIMIT: i64 = 1000;
pub struct KeyValueStoreKeysIterator {
client: KeyValueStoreClient,
options: ListKeysOptions,
remaining: Option<i64>,
next_exclusive_start_key: Option<String>,
buffer: VecDeque<KeyValueStoreKey>,
first_page: bool,
exhausted: bool,
}
impl KeyValueStoreKeysIterator {
pub async fn next(&mut self) -> ApifyClientResult<Option<KeyValueStoreKey>> {
if let Some(item) = self.buffer.pop_front() {
return Ok(Some(item));
}
if self.exhausted {
return Ok(None);
}
let mut page_options = self.options.clone();
page_options.limit = self.remaining.map(|rem| rem.min(KEY_LIST_MAX_LIMIT));
if !self.first_page {
page_options.exclusive_start_key = self.next_exclusive_start_key.clone();
}
self.first_page = false;
let page = self.client.list_keys(page_options).await?;
let is_truncated = page.is_truncated;
let mut items = page.items;
let received = items.len() as i64;
if let Some(rem) = self.remaining {
if received > rem {
items.truncate(rem as usize);
}
}
if let Some(rem) = self.remaining.as_mut() {
*rem -= received;
}
self.next_exclusive_start_key = page.next_exclusive_start_key;
if received == 0
|| !is_truncated
|| self.next_exclusive_start_key.is_none()
|| matches!(self.remaining, Some(r) if r <= 0)
{
self.exhausted = true;
}
self.buffer.extend(items);
Ok(self.buffer.pop_front())
}
}