j_cli/util/
version_check.rs1use crate::constants;
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7#[derive(Debug, Serialize, Deserialize)]
9struct VersionCache {
10 last_check: u64,
12 latest_version: String,
14 current_version: String,
16}
17
18#[derive(Debug, Deserialize)]
20struct GitHubRelease {
21 tag_name: String,
22}
23
24fn cache_file_path() -> PathBuf {
26 crate::config::YamlConfig::data_dir().join(constants::VERSION_CHECK_CACHE_FILE)
27}
28
29fn current_timestamp() -> u64 {
31 SystemTime::now()
32 .duration_since(UNIX_EPOCH)
33 .unwrap_or_default()
34 .as_secs()
35}
36
37fn is_newer_version(current: &str, latest: &str) -> bool {
39 let parse_version = |v: &str| -> Vec<u32> {
40 let v = v.trim_start_matches('v');
42 v.split('.')
43 .filter_map(|s| s.parse().ok())
44 .collect()
45 };
46
47 let current_parts = parse_version(current);
48 let latest_parts = parse_version(latest);
49
50 for i in 0..std::cmp::max(current_parts.len(), latest_parts.len()) {
52 let c = current_parts.get(i).unwrap_or(&0);
53 let l = latest_parts.get(i).unwrap_or(&0);
54 if l > c {
55 return true;
56 }
57 if l < c {
58 return false;
59 }
60 }
61 false
62}
63
64fn fetch_latest_version() -> Option<String> {
66 let url = constants::GITHUB_RELEASES_API;
67
68 let output = std::process::Command::new("curl")
71 .arg("-s")
72 .arg("-S")
73 .arg("-L")
74 .arg("--connect-timeout")
75 .arg("5")
76 .arg("--max-time")
77 .arg("10")
78 .arg("-H")
79 .arg("Accept: application/vnd.github.v3+json")
80 .arg("-H")
81 .arg("User-Agent: j-cli")
82 .arg(url)
83 .output()
84 .ok()?;
85
86 if !output.status.success() {
87 return None;
88 }
89
90 let response = String::from_utf8_lossy(&output.stdout);
91 let release: GitHubRelease = serde_json::from_str(&response).ok()?;
92
93 Some(release.tag_name.trim_start_matches('v').to_string())
95}
96
97fn read_cache() -> Option<VersionCache> {
99 let path = cache_file_path();
100 if !path.exists() {
101 return None;
102 }
103
104 let content = fs::read_to_string(&path).ok()?;
105 serde_json::from_str(&content).ok()
106}
107
108fn write_cache(cache: &VersionCache) {
110 let path = cache_file_path();
111 if let Some(parent) = path.parent() {
112 let _ = fs::create_dir_all(parent);
113 }
114 if let Ok(content) = serde_json::to_string_pretty(cache) {
115 let _ = fs::write(&path, content);
116 }
117}
118
119pub fn check_for_update() -> Option<String> {
121 let current_version = constants::VERSION;
122 let now = current_timestamp();
123
124 let cache = read_cache();
126
127 let need_check = match &cache {
129 Some(c) => {
130 c.current_version != current_version
132 || now - c.last_check >= constants::VERSION_CHECK_INTERVAL_SECS
133 }
134 None => true,
135 };
136
137 if !need_check {
138 if let Some(c) = cache {
140 if is_newer_version(current_version, &c.latest_version) {
141 return Some(c.latest_version);
142 }
143 }
144 return None;
145 }
146
147 let latest_version = match fetch_latest_version() {
149 Some(v) => v,
150 None => {
151 if let Some(c) = cache {
153 if is_newer_version(current_version, &c.latest_version) {
154 return Some(c.latest_version);
155 }
156 }
157 return None;
158 }
159 };
160
161 let new_cache = VersionCache {
163 last_check: now,
164 latest_version: latest_version.clone(),
165 current_version: current_version.to_string(),
166 };
167 write_cache(&new_cache);
168
169 if is_newer_version(current_version, &latest_version) {
171 Some(latest_version)
172 } else {
173 None
174 }
175}
176
177pub fn print_update_hint(latest_version: &str) {
179 eprintln!();
180 eprintln!("┌─────────────────────────────────────────────────────────┐");
181 eprintln!("│ 🎉 有新版本可用! │");
182 eprintln!("│ │");
183 eprintln!("│ 当前版本: {:<43}│", constants::VERSION);
184 eprintln!("│ 最新版本: {:<43}│", latest_version);
185 eprintln!("│ │");
186 eprintln!("│ 更新方式: │");
187 eprintln!("│ cargo install j-cli │");
188 eprintln!("│ 或访问: https://github.com/{}/releases │", constants::GITHUB_REPO);
189 eprintln!("└─────────────────────────────────────────────────────────┘");
190 eprintln!();
191}