Skip to main content

j_cli/util/
version_check.rs

1use crate::constants;
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::PathBuf;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7/// 版本检查缓存结构
8#[derive(Debug, Serialize, Deserialize)]
9struct VersionCache {
10    /// 最后检查时间(Unix 时间戳,秒)
11    last_check: u64,
12    /// 最新版本号
13    latest_version: String,
14    /// 当前版本号(用于判断是否需要重新检查)
15    current_version: String,
16}
17
18/// GitHub Release API 响应结构
19#[derive(Debug, Deserialize)]
20struct GitHubRelease {
21    tag_name: String,
22}
23
24/// 获取版本缓存文件路径
25fn cache_file_path() -> PathBuf {
26    crate::config::YamlConfig::data_dir().join(constants::VERSION_CHECK_CACHE_FILE)
27}
28
29/// 获取当前 Unix 时间戳(秒)
30fn current_timestamp() -> u64 {
31    SystemTime::now()
32        .duration_since(UNIX_EPOCH)
33        .unwrap_or_default()
34        .as_secs()
35}
36
37/// 比较语义化版本号,返回 true 表示 latest > current
38fn is_newer_version(current: &str, latest: &str) -> bool {
39    let parse_version = |v: &str| -> Vec<u32> {
40        // 移除 'v' 前缀(如 v1.0.0 -> 1.0.0)
41        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    // 逐段比较
51    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
64/// 从 GitHub API 获取最新版本号
65fn fetch_latest_version() -> Option<String> {
66    let url = constants::GITHUB_RELEASES_API;
67
68    // 使用 ureq 或 std::process 调用 curl
69    // 为避免引入额外依赖,使用 curl 命令
70    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    // 返回 tag_name,去掉 v 前缀
94    Some(release.tag_name.trim_start_matches('v').to_string())
95}
96
97/// 读取缓存
98fn 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
108/// 写入缓存
109fn 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
119/// 检查是否有新版本,返回新版本号(如果有)
120pub fn check_for_update() -> Option<String> {
121    let current_version = constants::VERSION;
122    let now = current_timestamp();
123
124    // 读取缓存
125    let cache = read_cache();
126
127    // 判断是否需要重新检查
128    let need_check = match &cache {
129        Some(c) => {
130            // 如果当前版本号变了,或者超过检查间隔,需要重新检查
131            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        // 使用缓存的版本信息
139        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    // 从 GitHub 获取最新版本
148    let latest_version = match fetch_latest_version() {
149        Some(v) => v,
150        None => {
151            // 网络请求失败,但如果有缓存就使用缓存
152            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    // 写入缓存
162    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    // 比较版本
170    if is_newer_version(current_version, &latest_version) {
171        Some(latest_version)
172    } else {
173        None
174    }
175}
176
177/// 打印新版本提示
178pub 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}