1use std::sync::Arc;
16
17use proton_sdk::cache::CacheRepository;
18use proton_sdk::error::Result;
19use proton_sdk::ids::{NodeUid, ShareId, VolumeId};
20use serde::{Deserialize, Serialize};
21
22use crate::node::Node;
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CachedNodeInfo {
32 pub node: Node,
33 pub membership_share_id: Option<ShareId>,
35 pub name_hash_digest: String,
37}
38
39const CLIENT_UID_KEY: &str = "client:id";
40const MAIN_VOLUME_ID_KEY: &str = "volume:main:id";
41const MY_FILES_SHARE_ID_KEY: &str = "share:my-files:id";
42
43fn node_key(uid: &NodeUid) -> String {
44 format!("node:{uid}")
45}
46
47#[derive(Clone)]
50pub struct DriveEntityCache {
51 repo: Arc<dyn CacheRepository>,
52}
53
54impl DriveEntityCache {
55 pub fn new(repo: Arc<dyn CacheRepository>) -> Self {
57 Self { repo }
58 }
59
60 pub async fn client_uid(&self) -> Result<Option<String>> {
63 self.repo.get(CLIENT_UID_KEY).await
64 }
65
66 pub async fn set_client_uid(&self, client_uid: &str) -> Result<()> {
68 self.repo.set(CLIENT_UID_KEY, client_uid, &[]).await
69 }
70
71 pub async fn main_volume_id(&self) -> Result<Option<VolumeId>> {
73 self.get_json(MAIN_VOLUME_ID_KEY).await
74 }
75
76 pub async fn set_main_volume_id(&self, volume_id: &VolumeId) -> Result<()> {
78 self.set_json(MAIN_VOLUME_ID_KEY, volume_id).await
79 }
80
81 pub async fn my_files_share_id(&self) -> Result<Option<ShareId>> {
83 self.get_json(MY_FILES_SHARE_ID_KEY).await
84 }
85
86 pub async fn set_my_files_share_id(&self, share_id: &ShareId) -> Result<()> {
88 self.set_json(MY_FILES_SHARE_ID_KEY, share_id).await
89 }
90
91 pub async fn set_node(
94 &self,
95 uid: &NodeUid,
96 node: &Node,
97 membership_share_id: Option<&ShareId>,
98 name_hash_digest: &str,
99 ) -> Result<()> {
100 let info = CachedNodeInfo {
101 node: node.clone(),
102 membership_share_id: membership_share_id.cloned(),
103 name_hash_digest: name_hash_digest.to_owned(),
104 };
105 self.set_json(&node_key(uid), &info).await
106 }
107
108 pub async fn try_get_node(&self, uid: &NodeUid) -> Result<Option<CachedNodeInfo>> {
110 self.get_json(&node_key(uid)).await
111 }
112
113 pub async fn remove_node(&self, uid: &NodeUid) -> Result<()> {
115 self.repo.remove(&node_key(uid)).await
116 }
117
118 pub async fn clear(&self) -> Result<()> {
120 self.repo.clear().await
121 }
122
123 async fn get_json<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Result<Option<T>> {
124 let Some(raw) = self.repo.get(key).await? else {
125 return Ok(None);
126 };
127 match serde_json::from_str(&raw) {
128 Ok(value) => Ok(Some(value)),
129 Err(_) => {
132 self.repo.remove(key).await?;
133 Ok(None)
134 }
135 }
136 }
137
138 async fn set_json<T: Serialize>(&self, key: &str, value: &T) -> Result<()> {
139 let serialized = serde_json::to_string(value)?;
140 self.repo.set(key, &serialized, &[]).await
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147 use proton_sdk::cache::InMemoryCacheRepository;
148 use proton_sdk::ids::LinkId;
149
150 use crate::node::NodeKind;
151
152 fn uid(v: &str, l: &str) -> NodeUid {
153 NodeUid::new(VolumeId::from(v), LinkId::from(l))
154 }
155
156 fn folder_node(uid: &NodeUid) -> Node {
157 Node {
158 uid: uid.clone(),
159 parent_uid: None,
160 kind: NodeKind::Folder,
161 name: "Docs".into(),
162 creation_time: 1,
163 modification_time: 2,
164 trashed: false,
165 is_shared: false,
166 is_shared_publicly: false,
167 signature_email: None,
168 verification: crate::node::NodeVerification::default(),
169 }
170 }
171
172 #[tokio::test]
173 async fn node_round_trips_with_membership_and_hash() {
174 let cache = DriveEntityCache::new(InMemoryCacheRepository::shared());
175 let node_uid = uid("vol", "link");
176 let node = folder_node(&node_uid);
177 let share = ShareId::from("share-1");
178
179 cache
180 .set_node(&node_uid, &node, Some(&share), "abc123")
181 .await
182 .unwrap();
183
184 let cached = cache.try_get_node(&node_uid).await.unwrap().unwrap();
185 assert_eq!(cached.node.name, "Docs");
186 assert_eq!(cached.membership_share_id, Some(share));
187 assert_eq!(cached.name_hash_digest, "abc123");
188
189 cache.remove_node(&node_uid).await.unwrap();
190 assert!(cache.try_get_node(&node_uid).await.unwrap().is_none());
191 }
192
193 #[tokio::test]
194 async fn scalar_entities_round_trip() {
195 let cache = DriveEntityCache::new(InMemoryCacheRepository::shared());
196 assert!(cache.main_volume_id().await.unwrap().is_none());
197
198 cache.set_client_uid("client-xyz").await.unwrap();
199 cache
200 .set_main_volume_id(&VolumeId::from("vol-1"))
201 .await
202 .unwrap();
203 cache
204 .set_my_files_share_id(&ShareId::from("mf-share"))
205 .await
206 .unwrap();
207
208 assert_eq!(
209 cache.client_uid().await.unwrap().as_deref(),
210 Some("client-xyz")
211 );
212 assert_eq!(
213 cache.main_volume_id().await.unwrap(),
214 Some(VolumeId::from("vol-1"))
215 );
216 assert_eq!(
217 cache.my_files_share_id().await.unwrap(),
218 Some(ShareId::from("mf-share"))
219 );
220 }
221
222 #[tokio::test]
223 async fn malformed_entry_is_evicted_as_miss() {
224 let repo = InMemoryCacheRepository::shared();
225 repo.set(&node_key(&uid("v", "l")), "not json", &[])
226 .await
227 .unwrap();
228 let cache = DriveEntityCache::new(repo.clone());
229 assert!(cache.try_get_node(&uid("v", "l")).await.unwrap().is_none());
230 assert!(repo.get(&node_key(&uid("v", "l"))).await.unwrap().is_none());
232 }
233}