use std::sync::Arc;
use proton_sdk::cache::CacheRepository;
use proton_sdk::error::Result;
use proton_sdk::ids::{NodeUid, ShareId, VolumeId};
use serde::{Deserialize, Serialize};
use crate::node::Node;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedNodeInfo {
pub node: Node,
pub membership_share_id: Option<ShareId>,
pub name_hash_digest: String,
}
const CLIENT_UID_KEY: &str = "client:id";
const MAIN_VOLUME_ID_KEY: &str = "volume:main:id";
const MY_FILES_SHARE_ID_KEY: &str = "share:my-files:id";
fn node_key(uid: &NodeUid) -> String {
format!("node:{uid}")
}
#[derive(Clone)]
pub struct DriveEntityCache {
repo: Arc<dyn CacheRepository>,
}
impl DriveEntityCache {
pub fn new(repo: Arc<dyn CacheRepository>) -> Self {
Self { repo }
}
pub async fn client_uid(&self) -> Result<Option<String>> {
self.repo.get(CLIENT_UID_KEY).await
}
pub async fn set_client_uid(&self, client_uid: &str) -> Result<()> {
self.repo.set(CLIENT_UID_KEY, client_uid, &[]).await
}
pub async fn main_volume_id(&self) -> Result<Option<VolumeId>> {
self.get_json(MAIN_VOLUME_ID_KEY).await
}
pub async fn set_main_volume_id(&self, volume_id: &VolumeId) -> Result<()> {
self.set_json(MAIN_VOLUME_ID_KEY, volume_id).await
}
pub async fn my_files_share_id(&self) -> Result<Option<ShareId>> {
self.get_json(MY_FILES_SHARE_ID_KEY).await
}
pub async fn set_my_files_share_id(&self, share_id: &ShareId) -> Result<()> {
self.set_json(MY_FILES_SHARE_ID_KEY, share_id).await
}
pub async fn set_node(
&self,
uid: &NodeUid,
node: &Node,
membership_share_id: Option<&ShareId>,
name_hash_digest: &str,
) -> Result<()> {
let info = CachedNodeInfo {
node: node.clone(),
membership_share_id: membership_share_id.cloned(),
name_hash_digest: name_hash_digest.to_owned(),
};
self.set_json(&node_key(uid), &info).await
}
pub async fn try_get_node(&self, uid: &NodeUid) -> Result<Option<CachedNodeInfo>> {
self.get_json(&node_key(uid)).await
}
pub async fn remove_node(&self, uid: &NodeUid) -> Result<()> {
self.repo.remove(&node_key(uid)).await
}
pub async fn clear(&self) -> Result<()> {
self.repo.clear().await
}
async fn get_json<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Result<Option<T>> {
let Some(raw) = self.repo.get(key).await? else {
return Ok(None);
};
match serde_json::from_str(&raw) {
Ok(value) => Ok(Some(value)),
Err(_) => {
self.repo.remove(key).await?;
Ok(None)
}
}
}
async fn set_json<T: Serialize>(&self, key: &str, value: &T) -> Result<()> {
let serialized = serde_json::to_string(value)?;
self.repo.set(key, &serialized, &[]).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use proton_sdk::cache::InMemoryCacheRepository;
use proton_sdk::ids::LinkId;
use crate::node::NodeKind;
fn uid(v: &str, l: &str) -> NodeUid {
NodeUid::new(VolumeId::from(v), LinkId::from(l))
}
fn folder_node(uid: &NodeUid) -> Node {
Node {
uid: uid.clone(),
parent_uid: None,
kind: NodeKind::Folder,
name: "Docs".into(),
creation_time: 1,
modification_time: 2,
trashed: false,
is_shared: false,
is_shared_publicly: false,
signature_email: None,
verification: crate::node::NodeVerification::default(),
}
}
#[tokio::test]
async fn node_round_trips_with_membership_and_hash() {
let cache = DriveEntityCache::new(InMemoryCacheRepository::shared());
let node_uid = uid("vol", "link");
let node = folder_node(&node_uid);
let share = ShareId::from("share-1");
cache
.set_node(&node_uid, &node, Some(&share), "abc123")
.await
.unwrap();
let cached = cache.try_get_node(&node_uid).await.unwrap().unwrap();
assert_eq!(cached.node.name, "Docs");
assert_eq!(cached.membership_share_id, Some(share));
assert_eq!(cached.name_hash_digest, "abc123");
cache.remove_node(&node_uid).await.unwrap();
assert!(cache.try_get_node(&node_uid).await.unwrap().is_none());
}
#[tokio::test]
async fn scalar_entities_round_trip() {
let cache = DriveEntityCache::new(InMemoryCacheRepository::shared());
assert!(cache.main_volume_id().await.unwrap().is_none());
cache.set_client_uid("client-xyz").await.unwrap();
cache
.set_main_volume_id(&VolumeId::from("vol-1"))
.await
.unwrap();
cache
.set_my_files_share_id(&ShareId::from("mf-share"))
.await
.unwrap();
assert_eq!(
cache.client_uid().await.unwrap().as_deref(),
Some("client-xyz")
);
assert_eq!(
cache.main_volume_id().await.unwrap(),
Some(VolumeId::from("vol-1"))
);
assert_eq!(
cache.my_files_share_id().await.unwrap(),
Some(ShareId::from("mf-share"))
);
}
#[tokio::test]
async fn malformed_entry_is_evicted_as_miss() {
let repo = InMemoryCacheRepository::shared();
repo.set(&node_key(&uid("v", "l")), "not json", &[])
.await
.unwrap();
let cache = DriveEntityCache::new(repo.clone());
assert!(cache.try_get_node(&uid("v", "l")).await.unwrap().is_none());
assert!(repo.get(&node_key(&uid("v", "l"))).await.unwrap().is_none());
}
}