omnyssh_core/utils/platform.rs
1use std::path::{Path, PathBuf};
2use std::time::{Duration, SystemTime};
3
4/// Rolling log files older than this many days are pruned at startup.
5pub const LOG_RETENTION_DAYS: u64 = 7;
6
7/// Prefix shared by every rolling log file (`omnyssh.log`, `omnyssh.log.YYYY-MM-DD`).
8const LOG_FILE_PREFIX: &str = "omnyssh.log";
9
10/// Returns the path to the user's SSH config file.
11///
12/// - Linux / macOS: `~/.ssh/config`
13/// - Windows: `%USERPROFILE%\.ssh\config`
14///
15/// Uses the `dirs` crate so we never hardcode `~`.
16pub fn ssh_config_path() -> Option<PathBuf> {
17 dirs::home_dir().map(|h| h.join(".ssh").join("config"))
18}
19
20/// Returns the application config directory.
21///
22/// - Linux: `~/.config/omnyssh/`
23/// - macOS: `~/Library/Application Support/omnyssh/`
24/// - Windows: `%APPDATA%\omnyssh\`
25pub fn app_config_dir() -> Option<PathBuf> {
26 dirs::config_dir().map(|d| d.join("omnyssh"))
27}
28
29/// Returns the path to the main application config file.
30pub fn app_config_path() -> Option<PathBuf> {
31 app_config_dir().map(|d| d.join("config.toml"))
32}
33
34/// Returns the path to the hosts config file.
35pub fn hosts_config_path() -> Option<PathBuf> {
36 app_config_dir().map(|d| d.join("hosts.toml"))
37}
38
39/// Returns the path to the snippets config file.
40pub fn snippets_config_path() -> Option<PathBuf> {
41 app_config_dir().map(|d| d.join("snippets.toml"))
42}
43
44/// Removes rolling log files in `log_dir` older than `max_age_days`.
45///
46/// Best-effort and fault-tolerant: a missing directory, an unreadable entry,
47/// or a failed delete is skipped rather than propagated, so log cleanup never
48/// blocks application startup. File age is taken from the filesystem
49/// modification time, which keeps the logic identical on every OS. Only files
50/// whose name starts with `omnyssh.log` are considered, so config and other
51/// data files in the same directory are never touched.
52///
53/// Returns the number of files removed.
54pub fn cleanup_old_logs(log_dir: &Path, max_age_days: u64) -> usize {
55 let entries = match std::fs::read_dir(log_dir) {
56 Ok(entries) => entries,
57 Err(_) => return 0,
58 };
59
60 let max_age = Duration::from_secs(max_age_days * 24 * 60 * 60);
61 let now = SystemTime::now();
62 let mut removed = 0;
63
64 for entry in entries.flatten() {
65 let path = entry.path();
66
67 let is_log_file = path
68 .file_name()
69 .and_then(|name| name.to_str())
70 .map(|name| name.starts_with(LOG_FILE_PREFIX))
71 .unwrap_or(false);
72 if !is_log_file {
73 continue;
74 }
75
76 let modified = match entry.metadata().and_then(|meta| meta.modified()) {
77 Ok(modified) => modified,
78 Err(_) => continue,
79 };
80
81 // `duration_since` errors on files dated in the future (clock skew);
82 // treat those as recent and keep them.
83 if let Ok(age) = now.duration_since(modified) {
84 if age > max_age && std::fs::remove_file(&path).is_ok() {
85 removed += 1;
86 }
87 }
88 }
89
90 removed
91}