Skip to main content

im_core/core/
bootstrap.rs

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}