1use std::fs;
2use std::path::Path;
3
4use serde::{Deserialize, Serialize};
5
6pub struct CoreBootstrap<'a> {
7 core: &'a super::ImCore,
8}
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct PathValidationReport {
12 pub checked: Vec<PathCheck>,
13 pub warnings: Vec<String>,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct PathCheck {
18 pub kind: String,
19 pub path: String,
20 pub exists: bool,
21 pub readable: bool,
22 pub writable: Option<bool>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct LocalStateStatus {
27 pub sqlite_path: String,
28 pub initialized: bool,
29 pub schema_version: Option<u32>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33pub struct MigrationReport {
34 pub sqlite_path: String,
35 pub from_version: Option<u32>,
36 pub to_version: u32,
37 pub applied: Vec<String>,
38}
39
40impl<'a> CoreBootstrap<'a> {
41 pub(crate) fn new(core: &'a super::ImCore) -> Self {
42 Self { core }
43 }
44
45 pub fn validate_paths(&self) -> crate::ImResult<PathValidationReport> {
46 let sdk_paths = self.core.inner().sdk_paths();
47 let checked = vec![
48 check_path(
49 "identity_root_dir",
50 &sdk_paths.identities.identity_root_dir,
51 Some(true),
52 ),
53 check_path("registry_path", &sdk_paths.identities.registry_path, None),
54 check_optional_path(
55 "default_identity_path",
56 sdk_paths.identities.default_identity_path.as_deref(),
57 ),
58 check_path("sqlite_path", &sdk_paths.local_state.sqlite_path, None),
59 check_path("cache_dir", &sdk_paths.runtime.cache_dir, Some(true)),
60 check_path("temp_dir", &sdk_paths.runtime.temp_dir, Some(true)),
61 ];
62 Ok(PathValidationReport {
63 checked,
64 warnings: Vec::new(),
65 })
66 }
67
68 pub async fn validate_paths_async(&self) -> crate::ImResult<PathValidationReport> {
69 let sdk_paths = self.core.inner().sdk_paths();
70 let checked = vec![
71 check_path_async(
72 "identity_root_dir",
73 sdk_paths.identities.identity_root_dir.clone(),
74 Some(true),
75 )
76 .await,
77 check_path_async(
78 "registry_path",
79 sdk_paths.identities.registry_path.clone(),
80 None,
81 )
82 .await,
83 check_optional_path_async(
84 "default_identity_path",
85 sdk_paths.identities.default_identity_path.clone(),
86 )
87 .await,
88 check_path_async(
89 "sqlite_path",
90 sdk_paths.local_state.sqlite_path.clone(),
91 None,
92 )
93 .await,
94 check_path_async("cache_dir", sdk_paths.runtime.cache_dir.clone(), Some(true)).await,
95 check_path_async("temp_dir", sdk_paths.runtime.temp_dir.clone(), Some(true)).await,
96 ];
97 Ok(PathValidationReport {
98 checked,
99 warnings: Vec::new(),
100 })
101 }
102
103 pub fn initialize_local_state(&self) -> crate::ImResult<LocalStateStatus> {
104 #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
105 return Err(crate::ImError::unsupported("sync-bootstrap-local-state"));
106
107 #[cfg(any(not(feature = "sqlite"), feature = "blocking"))]
108 {
109 let sqlite_path = &self.core.inner().sdk_paths().local_state.sqlite_path;
110 if let Some(parent) = sqlite_path.parent() {
111 fs::create_dir_all(parent)?;
112 }
113 let schema_version = initialize_local_state_schema(sqlite_path)?;
114 Ok(LocalStateStatus {
115 sqlite_path: sqlite_path.display().to_string(),
116 initialized: true,
117 schema_version,
118 })
119 }
120 }
121
122 pub async fn initialize_local_state_async(&self) -> crate::ImResult<LocalStateStatus> {
123 let sqlite_path = self
124 .core
125 .inner()
126 .sdk_paths()
127 .local_state
128 .sqlite_path
129 .clone();
130 if let Some(parent) = sqlite_path.parent() {
131 tokio::fs::create_dir_all(parent).await?;
132 }
133 let schema_version = initialize_local_state_schema_async(self.core).await?;
134 Ok(LocalStateStatus {
135 sqlite_path: sqlite_path.display().to_string(),
136 initialized: true,
137 schema_version,
138 })
139 }
140
141 pub fn migrate_local_state(&self) -> crate::ImResult<MigrationReport> {
142 #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
143 return Err(crate::ImError::unsupported("sync-bootstrap-local-state"));
144
145 #[cfg(any(not(feature = "sqlite"), feature = "blocking"))]
146 {
147 let status = self.initialize_local_state()?;
148 #[cfg(feature = "sqlite")]
149 let applied = {
150 let identities = self
151 .core
152 .identities()
153 .list()?
154 .into_iter()
155 .map(|identity| {
156 let mut credential_names = Vec::new();
157 credential_names.push(identity.id.as_str().to_string());
158 if let Some(alias) = identity.local_alias.as_deref() {
159 if !credential_names.iter().any(|known| known == alias) {
160 credential_names.push(alias.to_string());
161 }
162 }
163 crate::internal::local_state::schema::OwnerIdentityBackfill {
164 identity_id: identity.id.as_str().to_string(),
165 owner_did: identity.did.as_str().to_string(),
166 credential_names,
167 }
168 })
169 .collect::<Vec<_>>();
170 let connection = crate::internal::local_state::open_writable(
171 &self.core.inner().sdk_paths().local_state.sqlite_path,
172 )?;
173 let updated = crate::internal::local_state::schema::backfill_owner_identity_ids(
174 &connection,
175 &identities,
176 )?;
177 if updated == 0 {
178 Vec::new()
179 } else {
180 vec![format!("owner_identity_id_backfill:{updated}")]
181 }
182 };
183 #[cfg(not(feature = "sqlite"))]
184 let applied = Vec::new();
185 Ok(MigrationReport {
186 sqlite_path: status.sqlite_path,
187 from_version: status.schema_version,
188 to_version: status.schema_version.unwrap_or_default(),
189 applied,
190 })
191 }
192 }
193
194 pub async fn migrate_local_state_async(&self) -> crate::ImResult<MigrationReport> {
195 let status = self.initialize_local_state_async().await?;
196 #[cfg(feature = "sqlite")]
197 let applied = {
198 let identities = self
199 .core
200 .identities()
201 .list_async()
202 .await?
203 .into_iter()
204 .map(|identity| {
205 let mut credential_names = Vec::new();
206 credential_names.push(identity.id.as_str().to_string());
207 if let Some(alias) = identity.local_alias.as_deref() {
208 if !credential_names.iter().any(|known| known == alias) {
209 credential_names.push(alias.to_string());
210 }
211 }
212 crate::internal::local_state::schema::OwnerIdentityBackfill {
213 identity_id: identity.id.as_str().to_string(),
214 owner_did: identity.did.as_str().to_string(),
215 credential_names,
216 }
217 })
218 .collect::<Vec<_>>();
219 let db = self.core.inner().local_state_db().await?;
220 let updated = db.backfill_owner_identity_ids(identities).await?;
221 if updated == 0 {
222 Vec::new()
223 } else {
224 vec![format!("owner_identity_id_backfill:{updated}")]
225 }
226 };
227 #[cfg(not(feature = "sqlite"))]
228 let applied = Vec::new();
229 Ok(MigrationReport {
230 sqlite_path: status.sqlite_path,
231 from_version: status.schema_version,
232 to_version: status.schema_version.unwrap_or_default(),
233 applied,
234 })
235 }
236}
237
238fn check_optional_path(kind: &str, path: Option<&Path>) -> PathCheck {
239 match path {
240 Some(path) => check_path(kind, path, None),
241 None => PathCheck {
242 kind: kind.to_string(),
243 path: String::new(),
244 exists: false,
245 readable: false,
246 writable: None,
247 },
248 }
249}
250
251fn check_path(kind: &str, path: &Path, writable: Option<bool>) -> PathCheck {
252 let exists = path.exists();
253 let readable = fs::metadata(path).is_ok();
254 PathCheck {
255 kind: kind.to_string(),
256 path: path.display().to_string(),
257 exists,
258 readable,
259 writable,
260 }
261}
262
263async fn check_optional_path_async(kind: &str, path: Option<std::path::PathBuf>) -> PathCheck {
264 match path {
265 Some(path) => check_path_async(kind, path, None).await,
266 None => PathCheck {
267 kind: kind.to_string(),
268 path: String::new(),
269 exists: false,
270 readable: false,
271 writable: None,
272 },
273 }
274}
275
276async fn check_path_async(
277 kind: &str,
278 path: std::path::PathBuf,
279 writable: Option<bool>,
280) -> PathCheck {
281 let exists = tokio::fs::try_exists(&path).await.unwrap_or(false);
282 let readable = tokio::fs::metadata(&path).await.is_ok();
283 PathCheck {
284 kind: kind.to_string(),
285 path: path.display().to_string(),
286 exists,
287 readable,
288 writable,
289 }
290}
291
292#[cfg(all(feature = "sqlite", feature = "blocking"))]
293fn initialize_local_state_schema(sqlite_path: &Path) -> crate::ImResult<Option<u32>> {
294 let connection = crate::internal::local_state::open_writable(sqlite_path)?;
295 let schema_version = crate::internal::local_state::schema::current_schema_version(&connection)?;
296 Ok(Some(schema_version as u32))
297}
298
299#[cfg(all(feature = "sqlite", not(feature = "blocking")))]
300fn initialize_local_state_schema(_sqlite_path: &Path) -> crate::ImResult<Option<u32>> {
301 Err(crate::ImError::unsupported("sync-bootstrap-local-state"))
302}
303
304#[cfg(feature = "sqlite")]
305async fn initialize_local_state_schema_async(core: &super::ImCore) -> crate::ImResult<Option<u32>> {
306 let db = core.inner().local_state_db().await?;
307 let schema_version = db.current_schema_version().await?;
308 Ok(Some(schema_version as u32))
309}
310
311#[cfg(not(feature = "sqlite"))]
312fn initialize_local_state_schema(_sqlite_path: &Path) -> crate::ImResult<Option<u32>> {
313 Ok(None)
314}
315
316#[cfg(not(feature = "sqlite"))]
317async fn initialize_local_state_schema_async(
318 _core: &super::ImCore,
319) -> crate::ImResult<Option<u32>> {
320 Ok(None)
321}