Skip to main content

purple_ssh/app/
reload_state.rs

1//! Auto-reload mtime tracking and form conflict mtimes.
2
3use std::path::{Path, PathBuf};
4use std::time::SystemTime;
5
6use crate::ssh_config::model::SshConfigFile;
7
8/// Auto-reload mtime tracking.
9#[derive(Default)]
10pub struct ReloadState {
11    pub(in crate::app) config_path: PathBuf,
12    pub(in crate::app) last_modified: Option<SystemTime>,
13    pub(in crate::app) include_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
14    pub(in crate::app) include_dir_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
15    /// mtime of `~/.ssh/` itself. Changes when a key file is created,
16    /// renamed or removed; combined with `key_file_mtimes` this gives a
17    /// full add/remove/modify signal without needing a real watcher.
18    pub(in crate::app) keys_dir_mtime: Option<SystemTime>,
19    /// mtime per known `*.pub` (or private) key path. Touch-only edits
20    /// (re-encrypt, passphrase change) move the file mtime without
21    /// touching the parent directory, so we track both.
22    pub(in crate::app) key_file_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
23}
24
25/// Form conflict detection mtimes.
26#[derive(Default)]
27pub struct ConflictState {
28    pub form_mtime: Option<SystemTime>,
29    pub form_include_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
30    pub form_include_dir_mtimes: Vec<(PathBuf, Option<SystemTime>)>,
31    pub provider_form_mtime: Option<SystemTime>,
32}
33
34impl ReloadState {
35    pub fn config_path(&self) -> &Path {
36        &self.config_path
37    }
38
39    /// Build from a loaded config: captures initial mtimes for the main file
40    /// and every Include'd file and directory.
41    pub fn from_config(config: &SshConfigFile) -> Self {
42        let config_path = config.path.clone();
43        let last_modified = get_mtime(&config_path);
44        let include_mtimes = snapshot_include_mtimes(config);
45        let include_dir_mtimes = snapshot_include_dir_mtimes(config);
46        Self {
47            config_path,
48            last_modified,
49            include_mtimes,
50            include_dir_mtimes,
51            keys_dir_mtime: None,
52            key_file_mtimes: Vec::new(),
53        }
54    }
55}
56
57/// Get the modification time of a file.
58pub fn get_mtime(path: &Path) -> Option<SystemTime> {
59    std::fs::metadata(path).ok()?.modified().ok()
60}
61
62/// Snapshot mtimes of all resolved Include files.
63pub fn snapshot_include_mtimes(config: &SshConfigFile) -> Vec<(PathBuf, Option<SystemTime>)> {
64    config
65        .include_paths()
66        .into_iter()
67        .map(|p| {
68            let mtime = get_mtime(&p);
69            (p, mtime)
70        })
71        .collect()
72}
73
74/// Snapshot mtimes of parent directories of Include glob patterns.
75pub fn snapshot_include_dir_mtimes(config: &SshConfigFile) -> Vec<(PathBuf, Option<SystemTime>)> {
76    config
77        .include_glob_dirs()
78        .into_iter()
79        .map(|p| {
80            let mtime = get_mtime(&p);
81            (p, mtime)
82        })
83        .collect()
84}
85
86/// Snapshot the mtime of every discovered key's public-key file. The
87/// caller passes the live `discover_keys` result; we resolve each
88/// `display_path` (with the leading `~` expanded) back to an absolute
89/// path under `ssh_dir` so we can stat it cheaply on each tick.
90pub fn snapshot_key_mtimes(
91    ssh_dir: &Path,
92    keys: &[crate::ssh_keys::SshKeyInfo],
93) -> Vec<(PathBuf, Option<SystemTime>)> {
94    keys.iter()
95        .map(|k| {
96            let pub_path = ssh_dir.join(format!("{}.pub", k.name));
97            let mtime = get_mtime(&pub_path);
98            (pub_path, mtime)
99        })
100        .collect()
101}