- 自己调 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>
122 lines
4.1 KiB
Rust
122 lines
4.1 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"))
|
||
}
|
||
|
||
/// 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"))
|
||
);
|
||
}
|
||
}
|