actr_platform_native/
platform.rs1use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use tokio::sync::Mutex;
8use tracing::{debug, info, warn};
9
10use actr_platform_traits::{CryptoProvider, KvStore, PlatformError, PlatformProvider};
11
12use crate::crypto::NativeCryptoProvider;
13
14const INSTANCE_UID_FILE: &str = ".hyper-instance-uid";
15
16pub struct NativePlatformProvider {
21 data_dir: PathBuf,
22 crypto: Arc<NativeCryptoProvider>,
23 data_dir_ready: Mutex<bool>,
24}
25
26impl NativePlatformProvider {
27 pub fn new(data_dir: impl Into<PathBuf>) -> Self {
32 Self {
33 data_dir: data_dir.into(),
34 crypto: Arc::new(NativeCryptoProvider),
35 data_dir_ready: Mutex::new(false),
36 }
37 }
38
39 async fn ensure_data_dir(&self) -> Result<(), PlatformError> {
40 let mut ready = self.data_dir_ready.lock().await;
41 if *ready {
42 return Ok(());
43 }
44 tokio::fs::create_dir_all(&self.data_dir)
45 .await
46 .map_err(|e| {
47 PlatformError::Io(format!(
48 "failed to create data_dir `{}`: {e}",
49 self.data_dir.display()
50 ))
51 })?;
52 *ready = true;
53 Ok(())
54 }
55}
56
57#[async_trait]
58impl PlatformProvider for NativePlatformProvider {
59 async fn instance_uid(&self) -> Result<String, PlatformError> {
60 self.ensure_data_dir().await?;
61 let uid_file = self.data_dir.join(INSTANCE_UID_FILE);
62
63 match tokio::fs::read_to_string(&uid_file).await {
64 Ok(raw) => {
65 let id = raw.trim();
66 if !id.is_empty() {
67 return Ok(id.to_string());
68 }
69 warn!("instance_uid file is empty; regenerating");
70 }
71 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
72 Err(e) => {
73 return Err(PlatformError::Io(format!(
74 "failed to read instance_uid: {e}"
75 )));
76 }
77 }
78
79 let new_id = uuid::Uuid::new_v4().to_string();
80 tokio::fs::write(&uid_file, &new_id)
81 .await
82 .map_err(|e| PlatformError::Io(format!("failed to write instance_uid: {e}")))?;
83 info!(instance_uid = %new_id, "generated new instance_uid");
84 Ok(new_id)
85 }
86
87 async fn secret_store(&self, namespace: &str) -> Result<Arc<dyn KvStore>, PlatformError> {
88 self.ensure_data_dir().await?;
89 let path = Path::new(namespace);
93 if let Some(parent) = path.parent()
94 && !parent.as_os_str().is_empty()
95 {
96 tokio::fs::create_dir_all(parent).await.map_err(|e| {
97 PlatformError::Io(format!(
98 "failed to create secret store parent `{}`: {e}",
99 parent.display()
100 ))
101 })?;
102 }
103 let store = actr_hyper::ActorStore::open(path)
104 .await
105 .map_err(|e| PlatformError::Storage(format!("failed to open ActorStore: {e}")))?;
106 debug!(namespace, "native secret store opened");
107 Ok(Arc::new(store))
108 }
109
110 fn crypto(&self) -> Arc<dyn CryptoProvider> {
111 self.crypto.clone()
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118 use tempfile::TempDir;
119
120 #[tokio::test]
121 async fn instance_uid_stable_across_calls() {
122 let dir = TempDir::new().unwrap();
123 let provider = NativePlatformProvider::new(dir.path());
124
125 let id1 = provider.instance_uid().await.unwrap();
126 let id2 = provider.instance_uid().await.unwrap();
127 assert_eq!(id1, id2);
128 }
129
130 #[tokio::test]
131 async fn instance_uid_creates_data_dir_lazily() {
132 let parent = TempDir::new().unwrap();
133 let nested = parent.path().join("a/b/c");
134 assert!(!nested.exists());
135
136 let provider = NativePlatformProvider::new(&nested);
137 provider.instance_uid().await.unwrap();
138 assert!(nested.exists());
139 }
140
141 #[tokio::test]
142 async fn secret_store_roundtrip() {
143 let dir = TempDir::new().unwrap();
144 let db_path = dir.path().join("test.db");
145 let provider = NativePlatformProvider::new(dir.path());
146
147 let store = provider
148 .secret_store(db_path.to_str().unwrap())
149 .await
150 .unwrap();
151
152 store.set("key", b"value").await.unwrap();
153 let val = store.get("key").await.unwrap();
154 assert_eq!(val, Some(b"value".to_vec()));
155 }
156}