use crate::prelude::*;
use base64::prelude::*;
use beet_core::prelude::*;
use bytes::Bytes;
use js_sys::wasm_bindgen::JsCast;
#[derive(Debug, Clone)]
pub struct LocalStorageProvider;
impl LocalStorageProvider {
pub fn new() -> Self { Self }
fn bucket_prefix(bucket_name: &str) -> String {
format!("bucket:{}:", bucket_name)
}
fn storage_key(bucket_name: &str, path: &RoutePath) -> String {
Self::bucket_prefix(bucket_name).xtend(&path.to_string())
}
fn local_storage() -> web_sys::Storage {
js_sys::global()
.unchecked_into::<web_sys::Window>()
.local_storage()
.expect("LocalStorage is not available")
.expect("Failed to access localStorage")
}
}
impl<T: TableRow> TableProvider<T> for LocalStorageProvider {
fn box_clone_table(&self) -> Box<dyn TableProvider<T>> {
Box::new(self.clone())
}
}
impl BucketProvider for LocalStorageProvider {
fn box_clone(&self) -> Box<dyn BucketProvider> { Box::new(self.clone()) }
fn region(&self) -> Option<String> { None }
fn bucket_exists(
&self,
bucket_name: &str,
) -> SendBoxedFuture<Result<bool>> {
let prefix = Self::bucket_prefix(bucket_name);
Box::pin(async move {
let storage = Self::local_storage();
for i in 0..storage.length().unwrap_or(0) {
if let Some(key) = storage.key(i).ok().flatten() {
if key.starts_with(&prefix) {
return Ok(true);
}
}
}
Ok(false)
})
}
fn bucket_create(&self, _bucket_name: &str) -> SendBoxedFuture<Result<()>> {
Box::pin(async { Ok(()) })
}
fn bucket_remove(&self, bucket_name: &str) -> SendBoxedFuture<Result<()>> {
let prefix = Self::bucket_prefix(bucket_name);
Box::pin(async move {
let storage = Self::local_storage();
let keys: Vec<String> = (0..storage.length().unwrap_or(0))
.filter_map(|i| storage.key(i).ok().flatten())
.filter(|k| k.starts_with(&prefix))
.collect();
for key in keys {
storage.remove_item(&key).ok();
}
Ok(())
})
}
fn insert(
&self,
bucket_name: &str,
path: &RoutePath,
body: Bytes,
) -> SendBoxedFuture<Result<()>> {
let key = Self::storage_key(bucket_name, path);
let value = BASE64_STANDARD.encode(body);
Box::pin(async move {
Self::local_storage().set_item(&key, &value).map_jserr()?;
Ok(())
})
}
fn exists(
&self,
bucket_name: &str,
path: &RoutePath,
) -> SendBoxedFuture<Result<bool>> {
let key = Self::storage_key(bucket_name, path);
Box::pin(async move {
let storage = Self::local_storage();
let value = storage.get_item(&key).map_jserr()?;
Ok(value.is_some())
})
}
fn list(
&self,
bucket_name: &str,
) -> SendBoxedFuture<Result<Vec<RoutePath>>> {
let prefix = Self::bucket_prefix(bucket_name);
Box::pin(async move {
let storage = Self::local_storage();
let keys: Vec<RoutePath> = (0..storage.length().unwrap_or(0))
.filter_map(|i| storage.key(i).ok().flatten())
.filter_map(|k| k.strip_prefix(&prefix).map(RoutePath::new))
.collect();
Ok(keys)
})
}
fn get(
&self,
bucket_name: &str,
path: &RoutePath,
) -> SendBoxedFuture<Result<Bytes>> {
let key = Self::storage_key(bucket_name, path);
Box::pin(async move {
let value = Self::local_storage().get_item(&key).map_jserr()?;
match value {
Some(val) => {
let bytes = BASE64_STANDARD.decode(val)?;
Ok(Bytes::from(bytes))
}
None => bevybail!("Object not found: {}", key),
}
})
}
fn remove(
&self,
bucket_name: &str,
path: &RoutePath,
) -> SendBoxedFuture<Result<()>> {
let key = Self::storage_key(bucket_name, path);
let this = self.clone();
let bucket_name = bucket_name.to_string();
let path = path.clone();
Box::pin(async move {
match this.exists(&bucket_name, &path).await? {
true => {
Self::local_storage().remove_item(&key).map_jserr()?;
Ok(())
}
false => {
bevybail!("Object not found: {}", key)
}
}
})
}
fn public_url(
&self,
_bucket_name: &str,
_path: &RoutePath,
) -> SendBoxedFuture<Result<Option<String>>> {
Box::pin(async move { Ok(None) })
}
}
#[cfg(test)]
mod test {
use crate::prelude::*;
#[beet_core::test]
async fn works() {
let provider = LocalStorageProvider::new();
bucket_test::run(provider).await;
}
}