j_cli/util/
version_check.rs1use crate::constants;
2use serde::Deserialize;
3use std::fs;
4use std::path::PathBuf;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7#[derive(Debug, Deserialize)]
9struct VersionCache {
10 last_check: u64,
12 latest_version: String,
14 current_version: String,
16}
17
18fn cache_file_path() -> PathBuf {
20 crate::config::YamlConfig::data_dir().join(constants::VERSION_CHECK_CACHE_FILE)
21}
22
23fn current_timestamp() -> u64 {
25 SystemTime::now()
26 .duration_since(UNIX_EPOCH)
27 .unwrap_or_default()
28 .as_secs()
29}
30
31fn is_newer_version(current: &str, latest: &str) -> bool {
33 let parse_version = |v: &str| -> Vec<u32> {
34 let v = v.trim_start_matches('v');
35 v.split('.')
36 .filter_map(|s| s.parse().ok())
37 .collect()
38 };
39
40 let current_parts = parse_version(current);
41 let latest_parts = parse_version(latest);
42
43 for i in 0..std::cmp::max(current_parts.len(), latest_parts.len()) {
44 let c = current_parts.get(i).unwrap_or(&0);
45 let l = latest_parts.get(i).unwrap_or(&0);
46 if l > c {
47 return true;
48 }
49 if l < c {
50 return false;
51 }
52 }
53 false
54}
55
56fn read_cache() -> Option<VersionCache> {
58 let path = cache_file_path();
59 if !path.exists() {
60 return None;
61 }
62
63 let content = fs::read_to_string(&path).ok()?;
64 serde_json::from_str(&content).ok()
65}
66
67fn cache_needs_refresh() -> bool {
69 let current_version = constants::VERSION;
70 let now = current_timestamp();
71
72 match read_cache() {
73 Some(c) => {
74 c.current_version != current_version
75 || now.saturating_sub(c.last_check) >= constants::VERSION_CHECK_INTERVAL_SECS
76 }
77 None => true,
78 }
79}
80
81pub fn check_cached() -> Option<String> {
84 let current_version = constants::VERSION;
85 let cache = read_cache()?;
86
87 if is_newer_version(current_version, &cache.latest_version) {
88 Some(cache.latest_version)
89 } else {
90 None
91 }
92}
93
94pub fn refresh_cache_in_background() {
98 if !cache_needs_refresh() {
99 return;
100 }
101
102 let cache_path = cache_file_path();
103 let current_version = constants::VERSION;
104 let url = constants::GITHUB_RELEASES_API;
105
106 if let Some(parent) = cache_path.parent() {
108 let _ = fs::create_dir_all(parent);
109 }
110
111 let script_path = cache_path.with_extension("sh");
113 let script_content = build_check_script(url, current_version, &cache_path.to_string_lossy());
114
115 if fs::write(&script_path, &script_content).is_err() {
116 return;
117 }
118
119 let _ = std::process::Command::new("/bin/sh")
121 .arg("-c")
122 .arg(format!(
123 "nohup /bin/sh \"{}\" >/dev/null 2>&1 &",
124 script_path.to_string_lossy()
125 ))
126 .stdout(std::process::Stdio::null())
127 .stderr(std::process::Stdio::null())
128 .spawn();
129}
130
131fn build_check_script(url: &str, current_version: &str, cache_path: &str) -> String {
133 format!(
134 r##"
135 #!/bin/sh
136 # 版本检查脚本(由 j-cli 自动生成,执行后自动删除)
137 RESPONSE=$(curl -s -S -L --connect-timeout 3 --max-time 8 \
138 -H "Accept: application/vnd.github.v3+json" \
139 -H "User-Agent: j-cli" \
140 "{url}")
141
142 if [ $? -ne 0 ]; then
143 rm -f "$0"
144 exit 0
145 fi
146
147 TAG=$(echo "$RESPONSE" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"$/\1/' | sed 's/^v//')
148
149 if [ -n "$TAG" ]; then
150 NOW=$(date +%s)
151 cat > "{cache_path}" << CACHE_EOF
152 {{
153 "last_check": $NOW,
154 "latest_version": "$TAG",
155 "current_version": "{current_version}"
156 }}
157 CACHE_EOF
158 fi
159
160 # 清理临时脚本自身
161 rm -f "$0"
162 "##,
163 url = url,
164 current_version = current_version,
165 cache_path = cache_path,
166 )
167}
168
169#[allow(dead_code)]
171pub fn check_and_hint() {
172 if let Some(latest_version) = check_cached() {
173 print_update_hint(&latest_version);
174 }
175 refresh_cache_in_background();
176}
177
178pub fn print_update_hint(latest_version: &str) {
180 eprintln!();
181 eprintln!("┌─────────────────────────────────────────────────────────┐");
182 eprintln!("│ 🎉 有新版本可用! │");
183 eprintln!("│ │");
184 eprintln!("│ 当前版本: {:<43}│", constants::VERSION);
185 eprintln!("│ 最新版本: {:<43}│", latest_version);
186 eprintln!("│ │");
187 eprintln!("│ 更新方式: │");
188 eprintln!("│ cargo install j-cli │");
189 eprintln!("│ 或访问: https://github.com/{}/releases │", constants::GITHUB_REPO);
190 eprintln!("└─────────────────────────────────────────────────────────┘");
191 eprintln!();
192}