tauri-plugin-deeprespond-im 1.0.0-alpha

深应智能 IM 的 Tauri 插件:本地存储、安全存储、窗口间的锁与消息、系统事件、通知、角标、文件
//! 数据目录的布局(《1.本地存储》第 1 节),文件名的哈希规则与 Flutter SDK 相同:SHA-256 的前 16 位十六进制。
//!
//! ```text
//! {app_local_data_dir}/deeprespond_im/
//!   instance.lock  master.key  allowed_paths.json  device/device_id
//!   {appKey 的哈希}/secure.bin  users.json  {用户名的哈希}.db  {用户名的哈希}/outbox/
//! ```

use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};

use sha2::{Digest, Sha256};

use crate::error::{Error, Result};

pub fn short_hash(text: &str) -> String {
    let digest = Sha256::digest(text.as_bytes());
    hex::encode(digest)[..16].to_string()
}

/// 一个 AppKey 的目录。
pub fn app_dir(data_dir: &Path, app_key: &str) -> PathBuf {
    data_dir.join(short_hash(app_key))
}

/// 一个用户的数据库文件。
pub fn db_path(data_dir: &Path, app_key: &str, username: &str) -> PathBuf {
    app_dir(data_dir, app_key).join(format!("{}.db", short_hash(&username.to_lowercase())))
}

/// 一个用户的目录(附件等)。
pub fn user_dir(data_dir: &Path, app_key: &str, username: &str) -> PathBuf {
    app_dir(data_dir, app_key).join(short_hash(&username.to_lowercase()))
}

pub fn outbox_dir(data_dir: &Path, app_key: &str, username: &str) -> PathBuf {
    user_dir(data_dir, app_key, username).join("outbox")
}

/// 创建目录;macOS、Linux 上 deeprespond_im 目录的权限为 0700,其他系统用户不能进入(第 1 节)。
pub fn ensure_private_dir(dir: &Path) -> Result<()> {
    fs::create_dir_all(dir).map_err(|e| Error::from_io(&e))?;
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let _ = fs::set_permissions(dir, fs::Permissions::from_mode(0o700));
    }
    Ok(())
}

/// 写入临时文件后改名替换,不会写坏一半。
pub fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
    let dir = path.parent().ok_or_else(|| Error::internal("没有上级目录"))?;
    fs::create_dir_all(dir).map_err(|e| Error::from_io(&e))?;
    let tmp = dir.join(format!(".{}.tmp", path.file_name().and_then(|n| n.to_str()).unwrap_or("file")));
    {
        let mut file = fs::File::create(&tmp).map_err(|e| Error::from_io(&e))?;
        file.write_all(bytes).map_err(|e| Error::from_io(&e))?;
        file.sync_all().map_err(|e| Error::from_io(&e))?;
    }
    fs::rename(&tmp, path).map_err(|e| Error::from_io(&e))
}

/// 插件用到的三个目录。
#[derive(Clone, Debug)]
pub struct Paths {
    /// 本机应用数据目录下的 deeprespond_im(不用漫游目录,第 1 节)。
    pub data: PathBuf,
    /// 应用缓存目录下的 deeprespond_im:Blob 落盘、处理中的图片;启动时删除超过 1 天的。
    pub temp: PathBuf,
    /// 日志目录。
    pub logs: PathBuf,
}

impl Paths {
    pub fn users_json(&self, app_key: &str) -> PathBuf {
        app_dir(&self.data, app_key).join("users.json")
    }
}

/// 删除目录中超过 max_age 的文件(临时目录,第 6 节)。
pub fn clean_old_files(dir: &Path, max_age: std::time::Duration) {
    let Ok(entries) = fs::read_dir(dir) else { return };
    let now = std::time::SystemTime::now();
    for entry in entries.flatten() {
        let path = entry.path();
        let Ok(meta) = entry.metadata() else { continue };
        let old = meta.modified().ok().and_then(|m| now.duration_since(m).ok()).is_some_and(|age| age > max_age);
        if !old {
            continue;
        }
        if meta.is_dir() {
            let _ = fs::remove_dir_all(&path);
        } else {
            let _ = fs::remove_file(&path);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hash_matches_flutter() {
        // 与 Flutter SDK 的 shortHash 相同:SHA-256 的前 16 位十六进制
        assert_eq!(short_hash("org#app"), &hex::encode(Sha256::digest(b"org#app"))[..16]);
        let data = Path::new("/d");
        assert_eq!(db_path(data, "org#app", "Alice"), db_path(data, "org#app", "alice"));
        assert!(db_path(data, "org#app", "alice").to_string_lossy().ends_with(".db"));
    }
}