feat: create standalone native fnOS repository
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
package cloudflared
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const minBinarySize = 10 << 20
|
||||
|
||||
// Mirror 是 cloudflared 二进制下载镜像。
|
||||
type Mirror struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Base string `json:"base"`
|
||||
}
|
||||
|
||||
// DefaultMirrors 返回内置镜像表;Base 为空表示 GitHub 官方源。
|
||||
func DefaultMirrors() []Mirror {
|
||||
return []Mirror{
|
||||
{ID: "official", Name: "GitHub 官方源"},
|
||||
{ID: "gh-proxy-org", Name: "gh-proxy.org", Base: "https://gh-proxy.org/"},
|
||||
{ID: "gh-proxy-hk", Name: "gh-proxy 香港", Base: "https://hk.gh-proxy.org/"},
|
||||
{ID: "gh-proxy-cdn", Name: "gh-proxy CDN", Base: "https://cdn.gh-proxy.org/"},
|
||||
{ID: "gh-proxy-edgeone", Name: "gh-proxy EdgeOne", Base: "https://edgeone.gh-proxy.org/"},
|
||||
}
|
||||
}
|
||||
|
||||
func findMirror(id string) Mirror {
|
||||
id = strings.TrimSpace(id)
|
||||
for _, mirror := range DefaultMirrors() {
|
||||
if mirror.ID == id {
|
||||
return mirror
|
||||
}
|
||||
}
|
||||
return DefaultMirrors()[0]
|
||||
}
|
||||
|
||||
func binaryFileName() string {
|
||||
name := "cloudflared-" + runtime.GOOS + "-" + runtime.GOARCH
|
||||
if runtime.GOOS == "windows" {
|
||||
name += ".exe"
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// DownloadURL 生成指定镜像的下载地址;version 为空表示 latest。
|
||||
func DownloadURL(mirror Mirror, version string) string {
|
||||
official := "https://github.com/cloudflare/cloudflared/releases/latest/download/" + binaryFileName()
|
||||
if strings.TrimSpace(version) != "" {
|
||||
official = "https://github.com/cloudflare/cloudflared/releases/download/" + strings.TrimSpace(version) + "/" + binaryFileName()
|
||||
}
|
||||
if strings.TrimSpace(mirror.Base) == "" {
|
||||
return official
|
||||
}
|
||||
return mirror.Base + official
|
||||
}
|
||||
|
||||
func (s *Service) binaryPath() string {
|
||||
name := "cloudflared"
|
||||
if runtime.GOOS == "windows" {
|
||||
name += ".exe"
|
||||
}
|
||||
return filepath.Join(s.cfg.DataDir, "cloudflared", "bin", name)
|
||||
}
|
||||
|
||||
// BinaryVersion 返回本机 cloudflared 版本;未安装返回空串。
|
||||
func (s *Service) BinaryVersion() string {
|
||||
path := s.binaryPath()
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return ""
|
||||
}
|
||||
out, err := exec.Command(path, "--version").CombinedOutput()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
// DownloadBinary 下载并替换本机 cloudflared 二进制。
|
||||
func (s *Service) DownloadBinary(ctx context.Context, mirrorID string) (string, error) {
|
||||
return s.DownloadBinaryVersion(ctx, mirrorID, "")
|
||||
}
|
||||
|
||||
// DownloadBinaryVersion 下载指定版本;version 为空表示最新版。
|
||||
func (s *Service) DownloadBinaryVersion(ctx context.Context, mirrorID, version string) (string, error) {
|
||||
mirror := findMirror(mirrorID)
|
||||
target := s.binaryPath()
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
tmp := target + ".download"
|
||||
_ = os.Remove(tmp)
|
||||
if err := fetchFile(ctx, DownloadURL(mirror, version), tmp); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", err
|
||||
}
|
||||
info, err := os.Stat(tmp)
|
||||
if err != nil || info.Size() < minBinarySize {
|
||||
_ = os.Remove(tmp)
|
||||
return "", fmt.Errorf("下载文件过小,可能被镜像源截断")
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
_ = os.Chmod(tmp, 0o755)
|
||||
}
|
||||
if out, err := exec.Command(tmp, "--version").CombinedOutput(); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", fmt.Errorf("cloudflared 二进制不可执行: %s", strings.TrimSpace(string(out)))
|
||||
}
|
||||
previous := target + ".previous"
|
||||
if _, err := os.Stat(target); err == nil {
|
||||
_ = os.Remove(previous)
|
||||
if err := os.Rename(target, previous); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if err := os.Rename(tmp, target); err != nil {
|
||||
_ = os.Rename(previous, target)
|
||||
_ = os.Remove(tmp)
|
||||
return "", err
|
||||
}
|
||||
return s.BinaryVersion(), nil
|
||||
}
|
||||
|
||||
// LatestBinaryVersion 查询 Cloudflare 官方 GitHub Release 的最新版本。
|
||||
func (s *Service) LatestBinaryVersion(ctx context.Context) (string, error) {
|
||||
client := &http.Client{Timeout: 20 * time.Second}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/repos/cloudflare/cloudflared/releases/latest", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("读取最新版本失败:HTTP %d", resp.StatusCode)
|
||||
}
|
||||
var payload struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&payload); err != nil {
|
||||
return "", err
|
||||
}
|
||||
version := strings.TrimSpace(payload.TagName)
|
||||
if version == "" {
|
||||
return "", fmt.Errorf("GitHub 未返回 cloudflared 版本")
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
|
||||
// RollbackBinary 恢复上一版 cloudflared 二进制。
|
||||
func (s *Service) RollbackBinary() (string, error) {
|
||||
target := s.binaryPath()
|
||||
previous := target + ".previous"
|
||||
if _, err := os.Stat(previous); err != nil {
|
||||
return "", fmt.Errorf("没有可回滚的 cloudflared 版本")
|
||||
}
|
||||
failed := target + ".failed"
|
||||
_ = os.Remove(failed)
|
||||
if _, err := os.Stat(target); err == nil {
|
||||
if err := os.Rename(target, failed); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if err := os.Rename(previous, target); err != nil {
|
||||
_ = os.Rename(failed, target)
|
||||
return "", err
|
||||
}
|
||||
_ = os.Remove(failed)
|
||||
return s.BinaryVersion(), nil
|
||||
}
|
||||
|
||||
func fetchFile(ctx context.Context, url, path string) error {
|
||||
client := &http.Client{Timeout: 5 * time.Minute}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("下载 cloudflared 失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("下载 cloudflared 失败: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
if _, err := io.Copy(file, resp.Body); err != nil {
|
||||
return fmt.Errorf("写入 cloudflared 失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user