Files
shell-rs/src/app/paths.rs
T
XuzandClaude Opus 5.5 cd3d41c496 接入 Aptabase 匿名使用统计,「设置 › 关于」新增开关
- 自己调 Aptabase 的批量接口 POST /api/v0/events(没有纯 Rust SDK)。只发版本、系统名和版本、CPU 架构、设置快照(取值固定,主机等数量只报档位)和功能使用次数;不发主机、凭据、命令、文件名、路径,也没有任何标识
- 四种事件:app_installed(首次启动,送达后记进 analytics.json)、app_started、app_active(窗口到前台且当天 UTC 还没报过)、usage(43 项功能计数,从第一次使用起满 4 小时汇总一条)
- 不用定时器:只在启动、窗口激活、用到功能、改设置时动作,满期的汇总和失败重试都等下一个信号;计数存盘,退出不等网络、不丢数据
- 内网断网不受影响:联网、系统检测、写文件都在后台线程,连接 5 秒、整体 15 秒超时,失败不提示,重试从 1 分钟起翻倍、最长 6 小时,4xx 不重试
- 「设置 › 关于」新增「隐私」组的「发送匿名使用统计」,默认开,关掉清空本机攒下的计数
- 只有发布构建发;开发构建设 SHELLRS_ANALYTICS_DEBUG=1 才发调试事件,汇总周期 1 分钟、失败 10 秒后重试
- 手册新增「隐私」一节列出全部上报内容,README 同步

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:04:38 +08:00

122 lines
4.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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"))
}
/// The main window's last size, maximized state and place, for the next
/// launch (设置 › 应用 › 窗口). Kept apart from the settings: it changes
/// whenever the window moves.
pub fn window_state_path() -> PathBuf {
data_dir().join("window.json")
}
/// What 匿名使用统计 keeps between runs: whether this installation was
/// reported, and the uses not sent yet. Beside the database, so a fresh
/// data directory is a fresh installation.
pub fn analytics_path() -> PathBuf {
data_dir().join("analytics.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"))
);
}
}