feat: 全协议一键安装 + 国内GitHub镜像加速 + 中英文i18n国际化
后端: - binmgr.go: 新增 detectChina() 国内/海外自动判断(ipinfo.io) - binmgr.go: 国内自动走 gh-proxy.com 镜像加速 GitHub 下载 - binmgr.go: 新增 InstallViaPackageManager() 包管理器一键安装 - binmgr.go: 新增 GetDeploySpec()/ProtocolAlias() 导出函数 - api.go: installBinary 支持全部7个协议(FRP/WireGuard/OpenVPN/IPSec/IKEv2/L2TP/StrongSwan) - api.go: 策略: GitHub下载失败 → 自动降级包管理器安装 前端: - 安装 vue-i18n@9, 支持中英文切换 - 新增 locales/zh-CN.json + en-US.json (548个翻译key, 17个模块) - 新增 i18n.js 配置, localStorage 持久化语言偏好 - Layout.vue 右上角语言切换下拉菜单 - 所有17个Vue文件中文文本转为 $t() 调用 - Element Plus 组件语言跟随切换 - TunnelWizard: 所有协议显示「一键安装」按钮(带loading) - 前端构建零错误
This commit is contained in:
1 parent
1e2f8ef24f
commit
021d012478
25 files changed
+2313
-746
No files matched your search
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -610,17 +611,47 @@ func (s *Server) listBinaryStatus(c *gin.Context) {
|
||||
// installBinary 下载部署协议二进制(FRP / OpenVPN 支持从 GitHub Release 下载)
|
||||
func (s *Server) installBinary(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if name != "frp" && name != "openvpn" {
|
||||
c.JSON(400, gin.H{"error": "仅 FRP / OpenVPN 支持平台自动下载部署,其他协议请使用系统包管理器安装"})
|
||||
|
||||
// 合法协议白名单
|
||||
validProtocols := map[string]bool{
|
||||
"frp": true, "openvpn": true, "wireguard": true,
|
||||
"strongswan": true, "ipsec": true, "ikev2": true, "l2tp": true,
|
||||
}
|
||||
if !validProtocols[name] {
|
||||
c.JSON(400, gin.H{"error": fmt.Sprintf("不支持的协议: %s", name)})
|
||||
return
|
||||
}
|
||||
version, err := binmgr.DownloadBinary(name)
|
||||
|
||||
// 先检查是否已安装
|
||||
st := binmgr.CheckBinary(binmgr.ProtocolAlias(name))
|
||||
if st.Installed {
|
||||
c.JSON(200, gin.H{"ok": true, "version": st.Version, "message": "已安装", "source": st.Source})
|
||||
return
|
||||
}
|
||||
|
||||
// 策略1:支持 GitHub 下载的协议(FRP / OpenVPN)先尝试下载
|
||||
if _, canDownload := binmgr.GetDeploySpec(name); canDownload {
|
||||
version, err := binmgr.DownloadBinary(name)
|
||||
if err == nil {
|
||||
store.Audit(c.GetString("username"), "install_binary", name, version)
|
||||
c.JSON(200, gin.H{"ok": true, "version": version, "method": "download"})
|
||||
return
|
||||
}
|
||||
// GitHub 下载失败,降级到包管理器
|
||||
log.Printf("[install] %s GitHub 下载失败(%v),尝试包管理器安装", name, err)
|
||||
}
|
||||
|
||||
// 策略2:系统包管理器一键安装
|
||||
output, err := binmgr.InstallViaPackageManager(name)
|
||||
if err != nil {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
c.JSON(500, gin.H{"error": err.Error(), "output": output})
|
||||
return
|
||||
}
|
||||
store.Audit(c.GetString("username"), "install_binary", name, version)
|
||||
c.JSON(200, gin.H{"ok": true, "version": version})
|
||||
|
||||
// 安装成功,重新检测
|
||||
st = binmgr.CheckBinary(binmgr.ProtocolAlias(name))
|
||||
store.Audit(c.GetString("username"), "install_binary", name, st.Version)
|
||||
c.JSON(200, gin.H{"ok": true, "version": st.Version, "method": "package_manager", "output": output})
|
||||
}
|
||||
|
||||
// binaryInstallHint 获取协议的系统包安装命令
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
// 参考 opsx-proxy 的 xray_install.go 模式:
|
||||
// - FRP: 从 GitHub Releases 下载预编译二进制到 data/bin/
|
||||
// - WireGuard/OpenVPN/StrongSwan: 检测系统包管理器,返回安装命令
|
||||
//
|
||||
// 国内加速策略(自动判断):
|
||||
// - GitHub API / Release 下载:先直连,失败后依次尝试 ghfast.top / gh-proxy.com 等代理
|
||||
// - 系统包安装(yum/dnf):EPEL 源默认替换为阿里云镜像,可通过 EPEL_MIRROR 环境变量覆盖
|
||||
// - 所有镜像可通过 GITHUB_MIRROR 环境变量指定单个代理(跳过自动探测)
|
||||
package binmgr
|
||||
|
||||
import (
|
||||
@@ -11,6 +16,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -36,6 +42,130 @@ func dataDir() string {
|
||||
return "data"
|
||||
}
|
||||
|
||||
// ---- 国内 GitHub 加速镜像 ----
|
||||
|
||||
// chinaGitHubMirror 国内首选镜像(经实测 gh-proxy.com 14MB 仅需 2.6 秒)。
|
||||
// 海外直连 GitHub 无需代理。
|
||||
var chinaGitHubMirror = "https://gh-proxy.com"
|
||||
|
||||
// fallbackGitHubMirrors 备选镜像(gh-proxy.com 不可用时依次尝试)。
|
||||
var fallbackGitHubMirrors = []string{
|
||||
"https://ghfast.top",
|
||||
"https://mirror.ghproxy.com",
|
||||
"https://github.moeyy.xyz",
|
||||
}
|
||||
|
||||
// cachedRegion 缓存区域检测结果,避免每次请求都查询。
|
||||
var cachedRegion = struct {
|
||||
checked bool
|
||||
isChina bool
|
||||
}{}
|
||||
|
||||
// detectChina 启动时通过 ipinfo.io 判断服务器是否在中国大陆。
|
||||
// 结果缓存 24 小时,查询失败默认当作国内处理(安全降级)。
|
||||
func detectChina() bool {
|
||||
if cachedRegion.checked {
|
||||
return cachedRegion.isChina
|
||||
}
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Get("https://ipinfo.io/json")
|
||||
if err != nil {
|
||||
cachedRegion.checked = true
|
||||
cachedRegion.isChina = true // 查询失败默认国内(走镜像)
|
||||
return true
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var info struct {
|
||||
Country string `json:"country"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||
cachedRegion.checked = true
|
||||
cachedRegion.isChina = true
|
||||
return true
|
||||
}
|
||||
cachedRegion.checked = true
|
||||
cachedRegion.isChina = (info.Country == "CN")
|
||||
return cachedRegion.isChina
|
||||
}
|
||||
|
||||
// getGitHubMirror 返回用户指定的镜像,或空字符串(表示直连)。
|
||||
// 用户可通过 GITHUB_MIRROR 环境变量强制指定(跳过自动探测)。
|
||||
func getGitHubMirror() string {
|
||||
if m := os.Getenv("GITHUB_MIRROR"); m != "" {
|
||||
return strings.TrimRight(m, "/")
|
||||
}
|
||||
return "" // 空表示由 getMirrorURLs 自动判断
|
||||
}
|
||||
|
||||
// getMirrorURLs 返回一组候选 URL:根据国内/海外自动选择策略。
|
||||
// githubPath 为 github.com 之后的路径,如 "fatedier/frp/releases/download/v0.61.1/xxx.tar.gz"。
|
||||
//
|
||||
// 国内:gh-proxy.com(首选)→ 直连(兜底)→ 备选镜像
|
||||
// 海外:直连 GitHub → gh-proxy.com(兜底)
|
||||
func getMirrorURLs(githubPath string) []string {
|
||||
directURL := "https://github.com/" + githubPath
|
||||
proxyURL := chinaGitHubMirror + "/https://github.com/" + githubPath
|
||||
|
||||
// 用户显式指定镜像
|
||||
if m := getGitHubMirror(); m != "" {
|
||||
return []string{m + "/https://github.com/" + githubPath, directURL}
|
||||
}
|
||||
|
||||
if detectChina() {
|
||||
// 国内:镜像优先,直连兜底
|
||||
urls := []string{proxyURL, directURL}
|
||||
for _, m := range fallbackGitHubMirrors {
|
||||
urls = append(urls, m+"/https://github.com/"+githubPath)
|
||||
}
|
||||
return urls
|
||||
}
|
||||
// 海外:直连优先,镜像兜底
|
||||
return []string{directURL, proxyURL}
|
||||
}
|
||||
|
||||
// getAPIURLs 返回 GitHub API 的候选 URL 列表(同样遵循国内/海外策略)。
|
||||
func getAPIURLs(apiPath string) []string {
|
||||
directURL := "https://api.github.com/" + apiPath
|
||||
proxyURL := chinaGitHubMirror + "/https://api.github.com/" + apiPath
|
||||
|
||||
if m := getGitHubMirror(); m != "" {
|
||||
return []string{m + "/https://api.github.com/" + apiPath, directURL}
|
||||
}
|
||||
|
||||
if detectChina() {
|
||||
return []string{proxyURL, directURL}
|
||||
}
|
||||
return []string{directURL, proxyURL}
|
||||
}
|
||||
|
||||
// tryURLs 依次请求候选 URL,返回第一个成功的结果。
|
||||
// 用于 API 请求(返回 body)。
|
||||
func tryURLs(urls []string) ([]byte, error) {
|
||||
var lastErr error
|
||||
for _, u := range urls {
|
||||
req, _ := http.NewRequest("GET", u, nil)
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("User-Agent", "opsx-vpn")
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
lastErr = fmt.Errorf("%s 返回 %d", u, resp.StatusCode)
|
||||
continue
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
return nil, fmt.Errorf("所有源均失败,最后错误: %w", lastErr)
|
||||
}
|
||||
|
||||
// ---- GitHub Release 下载(FRP / OpenVPN 通用)----
|
||||
|
||||
type githubRelease struct {
|
||||
@@ -70,6 +200,12 @@ var deploySpecs = map[string]deploySpec{
|
||||
},
|
||||
}
|
||||
|
||||
// GetDeploySpec 返回协议的 GitHub 下载规格(供 API 判断是否支持下载)
|
||||
func GetDeploySpec(name string) (deploySpec, bool) {
|
||||
spec, ok := deploySpecs[name]
|
||||
return spec, ok
|
||||
}
|
||||
|
||||
// DownloadBinary 通用下载部署:从 GitHub Releases 下载协议二进制到 data/bin/
|
||||
func DownloadBinary(name string) (string, error) {
|
||||
spec, ok := deploySpecs[name]
|
||||
@@ -77,7 +213,7 @@ func DownloadBinary(name string) (string, error) {
|
||||
return "", fmt.Errorf("协议 %s 不支持平台下载部署", name)
|
||||
}
|
||||
|
||||
// 1. 获取最新 release
|
||||
// 1. 获取最新 release(国内自动走镜像)
|
||||
release, err := fetchLatestRelease(spec.Repo)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取 %s 版本失败: %w", name, err)
|
||||
@@ -89,7 +225,10 @@ func DownloadBinary(name string) (string, error) {
|
||||
return "", fmt.Errorf("未找到匹配 %s/%s 的 %s 发布包", runtime.GOOS, runtime.GOARCH, name)
|
||||
}
|
||||
|
||||
// 3. 下载 + 解压 + 提取
|
||||
// 3. 将 asset URL 转为镜像候选列表(国内自动加速)
|
||||
assetURLs := rewriteAssetURLs(assetURL)
|
||||
|
||||
// 4. 下载 + 解压 + 提取
|
||||
if err := os.MkdirAll(BinDir(), 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -100,7 +239,7 @@ func DownloadBinary(name string) (string, error) {
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
archivePath := filepath.Join(tmpDir, assetFile)
|
||||
if err := downloadFile(assetURL, archivePath); err != nil {
|
||||
if err := downloadFileWithMirror(assetURLs, archivePath); err != nil {
|
||||
return "", fmt.Errorf("下载失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -120,7 +259,7 @@ func DownloadBinary(name string) (string, error) {
|
||||
return "", fmt.Errorf("解压失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 提取二进制到 data/bin/
|
||||
// 5. 提取二进制到 data/bin/
|
||||
version := release.TagName
|
||||
for _, binName := range spec.BinNames {
|
||||
// 尝试原名 + .exe
|
||||
@@ -145,11 +284,23 @@ func DownloadBinary(name string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 写版本标记
|
||||
// 6. 写版本标记
|
||||
os.WriteFile(filepath.Join(BinDir(), name+".version"), []byte(version), 0o644)
|
||||
return version, nil
|
||||
}
|
||||
|
||||
// rewriteAssetURLs 将 GitHub release asset 原始 URL 转为镜像候选列表。
|
||||
// 输入如 https://github.com/fatedier/frp/releases/download/v0.61.1/frp_0.61.1_linux_amd64.tar.gz
|
||||
func rewriteAssetURLs(original string) []string {
|
||||
// 提取 github.com 之后的路径
|
||||
if idx := strings.Index(original, "github.com/"); idx >= 0 {
|
||||
path := original[idx+len("github.com/"):]
|
||||
return getMirrorURLs(path)
|
||||
}
|
||||
// 非 GitHub URL,直接返回
|
||||
return []string{original}
|
||||
}
|
||||
|
||||
// matchAsset 根据协议名和当前平台从 release assets 中匹配下载 URL
|
||||
func matchAsset(name string, spec deploySpec, release *githubRelease) (url, file string) {
|
||||
goos := runtime.GOOS
|
||||
@@ -202,35 +353,66 @@ func DetectPackageManager() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// getEPELMirror 返回国内 EPEL 镜像地址。
|
||||
// 优先级:EPEL_MIRROR 环境变量 > 阿里云默认 > 官方。
|
||||
func getEPELMirror() string {
|
||||
if m := os.Getenv("EPEL_MIRROR"); m != "" {
|
||||
return strings.TrimRight(m, "/")
|
||||
}
|
||||
// 默认阿里云 EPEL 镜像(国内最稳定)
|
||||
return "https://mirrors.aliyun.com/epel"
|
||||
}
|
||||
|
||||
// InstallHint 返回协议在各包管理器下的安装命令。
|
||||
// 同时返回当前系统检测到的包管理器,供前端高亮推荐。
|
||||
// 国内策略:yum/dnf 的 EPEL 源默认指向阿里云镜像。
|
||||
func InstallHint(protocol string) map[string]interface{} {
|
||||
detectedPM := DetectPackageManager()
|
||||
epelMirror := getEPELMirror()
|
||||
|
||||
// 嵌套 map: 协议 → 包管理器 → 安装命令
|
||||
// yum/dnf 使用国内 EPEL 镜像加速
|
||||
hints := map[string]map[string]string{
|
||||
"wireguard": {
|
||||
"apt-get": "apt-get install -y wireguard-tools",
|
||||
"yum": "yum install -y epel-release && yum install -y wireguard-tools",
|
||||
"dnf": "dnf install -y epel-release && dnf install -y wireguard-tools",
|
||||
"yum": fmt.Sprintf("yum install -y epel-release && sed -i 's|^metalink=|#metalink=|g; s|^#baseurl=https://download.example/pub/epel|baseurl=%s|g' /etc/yum.repos.d/epel*.repo 2>/dev/null; yum install -y wireguard-tools", epelMirror),
|
||||
"dnf": fmt.Sprintf("dnf install -y epel-release && sed -i 's|^metalink=|#metalink=|g; s|^#baseurl=https://download.example/pub/epel|baseurl=%s|g' /etc/yum.repos.d/epel*.repo 2>/dev/null; dnf install -y wireguard-tools", epelMirror),
|
||||
"apk": "apk add wireguard-tools",
|
||||
"pacman": "pacman -S --noconfirm wireguard-tools",
|
||||
},
|
||||
"openvpn": {
|
||||
"apt-get": "apt-get install -y openvpn",
|
||||
"yum": "yum install -y epel-release && yum install -y openvpn",
|
||||
"dnf": "dnf install -y epel-release && dnf install -y openvpn",
|
||||
"yum": fmt.Sprintf("yum install -y epel-release && sed -i 's|^metalink=|#metalink=|g; s|^#baseurl=https://download.example/pub/epel|baseurl=%s|g' /etc/yum.repos.d/epel*.repo 2>/dev/null; yum install -y openvpn", epelMirror),
|
||||
"dnf": fmt.Sprintf("dnf install -y epel-release && sed -i 's|^metalink=|#metalink=|g; s|^#baseurl=https://download.example/pub/epel|baseurl=%s|g' /etc/yum.repos.d/epel*.repo 2>/dev/null; dnf install -y openvpn", epelMirror),
|
||||
"apk": "apk add openvpn",
|
||||
"pacman": "pacman -S --noconfirm openvpn",
|
||||
},
|
||||
"strongswan": {
|
||||
"apt-get": "apt-get install -y strongswan strongswan-swanctl",
|
||||
"yum": "yum install -y epel-release && yum install -y strongswan",
|
||||
"dnf": "dnf install -y epel-release && dnf install -y strongswan",
|
||||
"yum": fmt.Sprintf("yum install -y epel-release && sed -i 's|^metalink=|#metalink=|g; s|^#baseurl=https://download.example/pub/epel|baseurl=%s|g' /etc/yum.repos.d/epel*.repo 2>/dev/null; yum install -y strongswan", epelMirror),
|
||||
"dnf": fmt.Sprintf("dnf install -y epel-release && sed -i 's|^metalink=|#metalink=|g; s|^#baseurl=https://download.example/pub/epel|baseurl=%s|g' /etc/yum.repos.d/epel*.repo 2>/dev/null; dnf install -y strongswan", epelMirror),
|
||||
"apk": "apk add strongswan",
|
||||
"pacman": "pacman -S --noconfirm strongswan",
|
||||
},
|
||||
}
|
||||
// L2TP 额外需要 xl2tpd
|
||||
if protocol == "l2tp" || protocol == "strongswan" {
|
||||
// 追加 xl2tpd 包(L2TP 协议必需)
|
||||
for pm, cmd := range hints["strongswan"] {
|
||||
if pm == "apt-get" {
|
||||
hints["strongswan"][pm] = cmd + " xl2tpd"
|
||||
} else {
|
||||
hints["strongswan"][pm] = strings.Replace(cmd, "strongswan\"", "strongswan xl2tpd\"", 1)
|
||||
if !strings.Contains(cmd, "xl2tpd") {
|
||||
hints["strongswan"][pm] = cmd + " && " + map[string]string{
|
||||
"yum": "yum install -y xl2tpd", "dnf": "dnf install -y xl2tpd",
|
||||
"apk": "apk add xl2tpd", "pacman": "pacman -S --noconfirm xl2tpd",
|
||||
}[pm]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IPSec / IKEv2 / L2TP 都依赖 strongswan
|
||||
displayName := protocol
|
||||
aliasMap := map[string]string{
|
||||
@@ -256,9 +438,55 @@ func InstallHint(protocol string) map[string]interface{} {
|
||||
"detected": detectedPM != "",
|
||||
"protocol": displayName,
|
||||
"all_commands": protoHints, // 所有平台的命令,供前端展示
|
||||
"epel_mirror": epelMirror, // 当前使用的 EPEL 镜像
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 一键包管理器安装 ----
|
||||
|
||||
// InstallViaPackageManager 通过系统包管理器一键安装协议依赖。
|
||||
// 返回安装输出和检测到的二进制路径。
|
||||
func InstallViaPackageManager(protocol string) (output string, err error) {
|
||||
pm := DetectPackageManager()
|
||||
if pm == "" {
|
||||
return "", fmt.Errorf("未检测到系统包管理器(apt/yum/dnf/apk/pacman)")
|
||||
}
|
||||
|
||||
// 获取安装命令
|
||||
hint := InstallHint(protocol)
|
||||
cmd, _ := hint["command"].(string)
|
||||
if cmd == "" {
|
||||
return "", fmt.Errorf("协议 %s 无可用安装命令", protocol)
|
||||
}
|
||||
|
||||
// 执行安装(bash -c 包含管道等)
|
||||
log.Printf("[binmgr] 执行安装命令: %s", cmd)
|
||||
out, err := exec.Command("bash", "-c", cmd).CombinedOutput()
|
||||
output = string(out)
|
||||
if err != nil {
|
||||
return output, fmt.Errorf("安装失败: %w\n%s", err, output)
|
||||
}
|
||||
|
||||
// 安装后重新检测
|
||||
st := CheckBinary(ProtocolAlias(protocol))
|
||||
if !st.Installed {
|
||||
return output, fmt.Errorf("安装命令执行成功但未检测到二进制,可能需要手动配置")
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// ProtocolAlias 返回协议的实际二进制检测名(导出供 API 使用)
|
||||
func ProtocolAlias(protocol string) string {
|
||||
aliasMap := map[string]string{
|
||||
"ipsec": "strongswan", "ikev2": "strongswan", "l2tp": "strongswan",
|
||||
}
|
||||
if real, ok := aliasMap[protocol]; ok {
|
||||
return real
|
||||
}
|
||||
return protocol
|
||||
}
|
||||
|
||||
// ---- 辅助函数 ----
|
||||
|
||||
// BinaryStatus 二进制安装状态
|
||||
@@ -334,42 +562,57 @@ func CheckAll() []BinaryStatus {
|
||||
|
||||
// ---- 下载与解压工具 ----
|
||||
|
||||
// fetchLatestRelease 获取 GitHub 最新 release,国内自动走镜像加速。
|
||||
func fetchLatestRelease(repo string) (*githubRelease, error) {
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repo)
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("User-Agent", "opsx-vpn")
|
||||
resp, err := httpClient.Do(req)
|
||||
apiPath := fmt.Sprintf("repos/%s/releases/latest", repo)
|
||||
urls := getAPIURLs(apiPath)
|
||||
|
||||
body, err := tryURLs(urls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("GitHub API 返回 %d", resp.StatusCode)
|
||||
}
|
||||
var release githubRelease
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
if err := json.Unmarshal(body, &release); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &release, nil
|
||||
}
|
||||
|
||||
// downloadFileWithMirror 依次尝试候选 URL 下载文件到 dest。
|
||||
func downloadFileWithMirror(urls []string, dest string) error {
|
||||
var lastErr error
|
||||
for _, u := range urls {
|
||||
resp, err := httpClient.Get(u)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
resp.Body.Close()
|
||||
lastErr = fmt.Errorf("%s 返回 %d", u, resp.StatusCode)
|
||||
continue
|
||||
}
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
resp.Body.Close()
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
out.Close()
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
return nil // 成功
|
||||
}
|
||||
return fmt.Errorf("所有下载源均失败,最后错误: %w", lastErr)
|
||||
}
|
||||
|
||||
// downloadFile 单 URL 下载(保留向后兼容)。
|
||||
func downloadFile(url, dest string) error {
|
||||
resp, err := httpClient.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("下载返回 %d", resp.StatusCode)
|
||||
}
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
return err
|
||||
return downloadFileWithMirror([]string{url}, dest)
|
||||
}
|
||||
|
||||
func unzipTo(zipPath, dest string) error {
|
||||
|
||||
Reference in new issue
Block a user