Files
shell-rs/src/app/paths.rs
T
Xuz ef83812f45 表结构发布前重构,「会话」在代码和数据库里统一改名为 Host
- 数据库升到第 13 版:主机的登录、连接方式、代理和备注合进 hosts 一行,只有一对多的跳板、书签和端口转发单独成表;全部表改为 STRICT,并用 CHECK 写明约束;用凭据的主机不再冗余存用户名,凭据外键不再级联,在用的凭据删不掉
- 表名 sessions → hosts、session_jumps → host_jumps,session_id → host_id,地址列 host → address;schema 比对去掉排版,以后加字段可以直接 ADD COLUMN
- 删除开发期的历史迁移代码:版本 1–12 的库直接拒绝打开、不动文件
- 代码里 Session / SessionId / SessionStore 等改为 Host*,模块 session/ → host/,Host.host → Host.address,菜单命令与界面元素 id 同步改名
- 外部 CLI 的参数、JSON 字段和错误码改为 host(host_not_found),协议版本升到 2
- CLAUDE.md、AGENTS.md、README 与 Agent skill 同步更新
2026-10-01 14:04:30 +08:00

108 lines
3.5 KiB
Rust

//! Where ShellRS keeps its data on disk.
use std::path::{Path, PathBuf};
/// Overrides the data directory. Set by tests and handy during development.
const DATA_DIR_ENV: &str = "SHELLRS_DATA_DIR";
/// The directory holding ShellRS's own files, e.g.
/// `~/Library/Application Support/shellrs` on macOS. Falls back to the
/// current directory on the platforms where `dirs` has nothing to offer.
pub fn data_dir() -> PathBuf {
if let Some(dir) = std::env::var_os(DATA_DIR_ENV) {
return PathBuf::from(dir);
}
dirs::data_dir()
.map(|dir| dir.join("shellrs"))
.unwrap_or_else(|| PathBuf::from("."))
}
/// The SQLite file holding hosts and groups. The parent directory is
/// created if it does not exist yet.
pub fn database_path() -> std::io::Result<PathBuf> {
let dir = data_dir();
std::fs::create_dir_all(&dir)?;
Ok(dir.join("shellrs.db"))
}
/// Where the private keys pasted into or generated by the credential form
/// are kept, beside the database. Created when the first one is saved.
pub fn keys_dir() -> PathBuf {
data_dir().join("keys")
}
/// Where updates are downloaded to: the user's cache folder. With
/// `SHELLRS_DATA_DIR` set it is beside the data instead, so a test or
/// development instance never shares downloads with the installed app.
pub fn updates_dir() -> PathBuf {
if std::env::var_os(DATA_DIR_ENV).is_some() {
return data_dir().join("updates");
}
dirs::cache_dir()
.map(|dir| dir.join("shellrs").join("updates"))
.unwrap_or_else(|| data_dir().join("updates"))
}
/// The interface settings, a small JSON file beside the database.
pub fn settings_path() -> std::io::Result<PathBuf> {
let dir = data_dir();
std::fs::create_dir_all(&dir)?;
Ok(dir.join("settings.json"))
}
/// Where the running app listens for the `shellrs` command. The command
/// and the app both ask here, so both follow `SHELLRS_DATA_DIR`.
pub fn cli_socket_path() -> PathBuf {
cli_endpoint(&data_dir())
}
/// Where an app keeping its data in `dir` listens for the `shellrs`
/// command: a socket in that directory, or on Windows a named pipe.
///
/// Pipe names are one namespace for the whole machine, so the name is made
/// from the data directory. That lives in the user's profile, so each user,
/// and each `SHELLRS_DATA_DIR`, gets a pipe of its own.
pub fn cli_endpoint(dir: &Path) -> PathBuf {
if cfg!(windows) {
PathBuf::from(pipe_name(dir))
} else {
dir.join("cli.sock")
}
}
/// `\\.\pipe\shellrs-cli-` and a hash of the directory. Windows paths
/// ignore case, so the hash does too.
fn pipe_name(dir: &Path) -> String {
use sha2::{Digest as _, Sha256};
let dir = std::path::absolute(dir).unwrap_or_else(|_| dir.to_path_buf());
let digest = Sha256::digest(dir.to_string_lossy().to_lowercase().as_bytes());
let hex: String = digest[..8]
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
format!(r"\\.\pipe\shellrs-cli-{hex}")
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::pipe_name;
#[test]
fn each_data_directory_gets_a_pipe_of_its_own() {
let pipe = pipe_name(Path::new("/Users/me/AppData/Roaming/shellrs"));
assert!(pipe.starts_with(r"\\.\pipe\shellrs-cli-"));
assert_eq!(pipe.len(), r"\\.\pipe\shellrs-cli-".len() + 16);
assert_eq!(
pipe,
pipe_name(Path::new("/USERS/me/appdata/Roaming/SHELLRS"))
);
assert_ne!(
pipe,
pipe_name(Path::new("/Users/other/AppData/Roaming/shellrs"))
);
}
}