im-core 0.1.0

Rust IM SDK for Awiki clients built on Agent Network Protocol (ANP)
Documentation
use std::fs;
use std::path::Path;

use serde::{Deserialize, Serialize};

pub struct CoreBootstrap<'a> {
    core: &'a super::ImCore,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PathValidationReport {
    pub checked: Vec<PathCheck>,
    pub warnings: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PathCheck {
    pub kind: String,
    pub path: String,
    pub exists: bool,
    pub readable: bool,
    pub writable: Option<bool>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct LocalStateStatus {
    pub sqlite_path: String,
    pub initialized: bool,
    pub schema_version: Option<u32>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MigrationReport {
    pub sqlite_path: String,
    pub from_version: Option<u32>,
    pub to_version: u32,
    pub applied: Vec<String>,
}

impl<'a> CoreBootstrap<'a> {
    pub(crate) fn new(core: &'a super::ImCore) -> Self {
        Self { core }
    }

    pub fn validate_paths(&self) -> crate::ImResult<PathValidationReport> {
        let sdk_paths = self.core.inner().sdk_paths();
        let checked = vec![
            check_path(
                "identity_root_dir",
                &sdk_paths.identities.identity_root_dir,
                Some(true),
            ),
            check_path("registry_path", &sdk_paths.identities.registry_path, None),
            check_optional_path(
                "default_identity_path",
                sdk_paths.identities.default_identity_path.as_deref(),
            ),
            check_path("sqlite_path", &sdk_paths.local_state.sqlite_path, None),
            check_path("cache_dir", &sdk_paths.runtime.cache_dir, Some(true)),
            check_path("temp_dir", &sdk_paths.runtime.temp_dir, Some(true)),
        ];
        Ok(PathValidationReport {
            checked,
            warnings: Vec::new(),
        })
    }

    pub async fn validate_paths_async(&self) -> crate::ImResult<PathValidationReport> {
        let sdk_paths = self.core.inner().sdk_paths();
        let checked = vec![
            check_path_async(
                "identity_root_dir",
                sdk_paths.identities.identity_root_dir.clone(),
                Some(true),
            )
            .await,
            check_path_async(
                "registry_path",
                sdk_paths.identities.registry_path.clone(),
                None,
            )
            .await,
            check_optional_path_async(
                "default_identity_path",
                sdk_paths.identities.default_identity_path.clone(),
            )
            .await,
            check_path_async(
                "sqlite_path",
                sdk_paths.local_state.sqlite_path.clone(),
                None,
            )
            .await,
            check_path_async("cache_dir", sdk_paths.runtime.cache_dir.clone(), Some(true)).await,
            check_path_async("temp_dir", sdk_paths.runtime.temp_dir.clone(), Some(true)).await,
        ];
        Ok(PathValidationReport {
            checked,
            warnings: Vec::new(),
        })
    }

    pub fn initialize_local_state(&self) -> crate::ImResult<LocalStateStatus> {
        #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
        return Err(crate::ImError::unsupported("sync-bootstrap-local-state"));

        #[cfg(any(not(feature = "sqlite"), feature = "blocking"))]
        {
            let sqlite_path = &self.core.inner().sdk_paths().local_state.sqlite_path;
            if let Some(parent) = sqlite_path.parent() {
                fs::create_dir_all(parent)?;
            }
            let schema_version = initialize_local_state_schema(sqlite_path)?;
            Ok(LocalStateStatus {
                sqlite_path: sqlite_path.display().to_string(),
                initialized: true,
                schema_version,
            })
        }
    }

    pub async fn initialize_local_state_async(&self) -> crate::ImResult<LocalStateStatus> {
        let sqlite_path = self
            .core
            .inner()
            .sdk_paths()
            .local_state
            .sqlite_path
            .clone();
        if let Some(parent) = sqlite_path.parent() {
            tokio::fs::create_dir_all(parent).await?;
        }
        let schema_version = initialize_local_state_schema_async(self.core).await?;
        Ok(LocalStateStatus {
            sqlite_path: sqlite_path.display().to_string(),
            initialized: true,
            schema_version,
        })
    }

    pub fn migrate_local_state(&self) -> crate::ImResult<MigrationReport> {
        #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
        return Err(crate::ImError::unsupported("sync-bootstrap-local-state"));

        #[cfg(any(not(feature = "sqlite"), feature = "blocking"))]
        {
            let status = self.initialize_local_state()?;
            #[cfg(feature = "sqlite")]
            let applied = {
                let identities = self
                    .core
                    .identities()
                    .list()?
                    .into_iter()
                    .map(|identity| {
                        let mut credential_names = Vec::new();
                        credential_names.push(identity.id.as_str().to_string());
                        if let Some(alias) = identity.local_alias.as_deref() {
                            if !credential_names.iter().any(|known| known == alias) {
                                credential_names.push(alias.to_string());
                            }
                        }
                        crate::internal::local_state::schema::OwnerIdentityBackfill {
                            identity_id: identity.id.as_str().to_string(),
                            owner_did: identity.did.as_str().to_string(),
                            credential_names,
                        }
                    })
                    .collect::<Vec<_>>();
                let connection = crate::internal::local_state::open_writable(
                    &self.core.inner().sdk_paths().local_state.sqlite_path,
                )?;
                let updated = crate::internal::local_state::schema::backfill_owner_identity_ids(
                    &connection,
                    &identities,
                )?;
                if updated == 0 {
                    Vec::new()
                } else {
                    vec![format!("owner_identity_id_backfill:{updated}")]
                }
            };
            #[cfg(not(feature = "sqlite"))]
            let applied = Vec::new();
            Ok(MigrationReport {
                sqlite_path: status.sqlite_path,
                from_version: status.schema_version,
                to_version: status.schema_version.unwrap_or_default(),
                applied,
            })
        }
    }

    pub async fn migrate_local_state_async(&self) -> crate::ImResult<MigrationReport> {
        let status = self.initialize_local_state_async().await?;
        #[cfg(feature = "sqlite")]
        let applied = {
            let identities = self
                .core
                .identities()
                .list_async()
                .await?
                .into_iter()
                .map(|identity| {
                    let mut credential_names = Vec::new();
                    credential_names.push(identity.id.as_str().to_string());
                    if let Some(alias) = identity.local_alias.as_deref() {
                        if !credential_names.iter().any(|known| known == alias) {
                            credential_names.push(alias.to_string());
                        }
                    }
                    crate::internal::local_state::schema::OwnerIdentityBackfill {
                        identity_id: identity.id.as_str().to_string(),
                        owner_did: identity.did.as_str().to_string(),
                        credential_names,
                    }
                })
                .collect::<Vec<_>>();
            let db = self.core.inner().local_state_db().await?;
            let updated = db.backfill_owner_identity_ids(identities).await?;
            if updated == 0 {
                Vec::new()
            } else {
                vec![format!("owner_identity_id_backfill:{updated}")]
            }
        };
        #[cfg(not(feature = "sqlite"))]
        let applied = Vec::new();
        Ok(MigrationReport {
            sqlite_path: status.sqlite_path,
            from_version: status.schema_version,
            to_version: status.schema_version.unwrap_or_default(),
            applied,
        })
    }
}

fn check_optional_path(kind: &str, path: Option<&Path>) -> PathCheck {
    match path {
        Some(path) => check_path(kind, path, None),
        None => PathCheck {
            kind: kind.to_string(),
            path: String::new(),
            exists: false,
            readable: false,
            writable: None,
        },
    }
}

fn check_path(kind: &str, path: &Path, writable: Option<bool>) -> PathCheck {
    let exists = path.exists();
    let readable = fs::metadata(path).is_ok();
    PathCheck {
        kind: kind.to_string(),
        path: path.display().to_string(),
        exists,
        readable,
        writable,
    }
}

async fn check_optional_path_async(kind: &str, path: Option<std::path::PathBuf>) -> PathCheck {
    match path {
        Some(path) => check_path_async(kind, path, None).await,
        None => PathCheck {
            kind: kind.to_string(),
            path: String::new(),
            exists: false,
            readable: false,
            writable: None,
        },
    }
}

async fn check_path_async(
    kind: &str,
    path: std::path::PathBuf,
    writable: Option<bool>,
) -> PathCheck {
    let exists = tokio::fs::try_exists(&path).await.unwrap_or(false);
    let readable = tokio::fs::metadata(&path).await.is_ok();
    PathCheck {
        kind: kind.to_string(),
        path: path.display().to_string(),
        exists,
        readable,
        writable,
    }
}

#[cfg(all(feature = "sqlite", feature = "blocking"))]
fn initialize_local_state_schema(sqlite_path: &Path) -> crate::ImResult<Option<u32>> {
    let connection = crate::internal::local_state::open_writable(sqlite_path)?;
    let schema_version = crate::internal::local_state::schema::current_schema_version(&connection)?;
    Ok(Some(schema_version as u32))
}

#[cfg(all(feature = "sqlite", not(feature = "blocking")))]
fn initialize_local_state_schema(_sqlite_path: &Path) -> crate::ImResult<Option<u32>> {
    Err(crate::ImError::unsupported("sync-bootstrap-local-state"))
}

#[cfg(feature = "sqlite")]
async fn initialize_local_state_schema_async(core: &super::ImCore) -> crate::ImResult<Option<u32>> {
    let db = core.inner().local_state_db().await?;
    let schema_version = db.current_schema_version().await?;
    Ok(Some(schema_version as u32))
}

#[cfg(not(feature = "sqlite"))]
fn initialize_local_state_schema(_sqlite_path: &Path) -> crate::ImResult<Option<u32>> {
    Ok(None)
}

#[cfg(not(feature = "sqlite"))]
async fn initialize_local_state_schema_async(
    _core: &super::ImCore,
) -> crate::ImResult<Option<u32>> {
    Ok(None)
}