Scout/cmd/scout-client/main.go
2026-08-30 21:45:23 +08:00

223 lines
5.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"scout/internal/registry"
)
type packageInfo struct {
Name string `json:"name"`
Version string `json:"version"`
SHA256 string `json:"sha256"`
Size int64 `json:"size"`
}
type updateEvent struct {
Package packageInfo `json:"package"`
}
func main() {
if len(os.Args) < 2 {
printUsage()
os.Exit(2)
}
switch os.Args[1] {
case "check":
checkCommand(os.Args[2:])
case "download":
downloadCommand(os.Args[2:])
case "watch":
watchCommand(os.Args[2:])
default:
printUsage()
os.Exit(2)
}
}
func checkCommand(args []string) {
flags := flag.NewFlagSet("check", flag.ExitOnError)
server := flags.String("server", "http://localhost:8080", "服务端地址")
name := flags.String("package", "", "包名")
current := flags.String("current", "", "当前版本")
flags.Parse(args)
requirePackage(*name)
var latest packageInfo
requestJSON(http.MethodGet, strings.TrimRight(*server, "/")+"/api/v1/packages/"+*name, nil, "", &latest)
if registry.CompareVersions(latest.Version, *current) > 0 {
fmt.Printf("发现更新:%s %s -> %s\n", latest.Name, *current, latest.Version)
return
}
fmt.Printf("当前已是最新版本:%s\n", latest.Version)
}
func downloadCommand(args []string) {
flags := flag.NewFlagSet("download", flag.ExitOnError)
server := flags.String("server", "http://localhost:8080", "服务端地址")
name := flags.String("package", "", "包名")
version := flags.String("version", "", "版本;为空时下载最新版本")
output := flags.String("output", "", "保存路径")
flags.Parse(args)
requirePackage(*name)
if *version == "" {
var latest packageInfo
requestJSON(http.MethodGet, strings.TrimRight(*server, "/")+"/api/v1/packages/"+*name, nil, "", &latest)
*version = latest.Version
}
if *output == "" {
*output = filepath.Join(".", fmt.Sprintf("%s-%s.pkg", *name, *version))
}
url := fmt.Sprintf("%s/api/v1/packages/%s/%s/download", strings.TrimRight(*server, "/"), *name, *version)
response, err := http.Get(url)
if err != nil {
fatal("请求下载失败: %v", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
fatalResponse(response)
}
file, err := os.Create(*output)
if err != nil {
fatal("创建输出文件失败: %v", err)
}
_, copyErr := io.Copy(file, response.Body)
closeErr := file.Close()
if copyErr != nil || closeErr != nil {
fatal("保存包文件失败")
}
digest, err := fileSHA256(*output)
if err != nil {
fatal("校验包文件失败: %v", err)
}
if expected := response.Header.Get("X-Scout-SHA256"); expected != "" && digest != expected {
_ = os.Remove(*output)
fatal("包文件校验失败,文件已删除")
}
fmt.Printf("下载完成:%s\nSHA-256:%s\n", *output, digest)
}
func watchCommand(args []string) {
flags := flag.NewFlagSet("watch", flag.ExitOnError)
server := flags.String("server", "http://localhost:8080", "服务端地址")
packages := flags.String("packages", "", "要监听的包名,多个包用逗号分隔")
flags.Parse(args)
url := strings.TrimRight(*server, "/") + "/api/v1/updates/stream"
if *packages != "" {
url += "?packages=" + *packages
}
response, err := http.Get(url)
if err != nil {
fatal("连接更新通知失败: %v", err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
fatalResponse(response)
}
fmt.Println("已连接更新通知,等待新包……")
readSSE(response.Body)
}
func requestJSON(method, url string, body io.Reader, token string, target any) {
request, err := http.NewRequest(method, url, body)
if err != nil {
fatal("创建请求失败: %v", err)
}
if token != "" {
request.Header.Set("Authorization", "Bearer "+token)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
fatal("请求失败: %v", err)
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
fatalResponse(response)
}
if err := json.NewDecoder(response.Body).Decode(target); err != nil {
fatal("解析服务端响应失败: %v", err)
}
}
func readSSE(reader io.Reader) {
dataLines := make([]string, 0, 1)
buffer := make([]byte, 4096)
pending := ""
for {
count, err := reader.Read(buffer)
if count > 0 {
pending += string(buffer[:count])
for {
lineEnd := strings.IndexByte(pending, '\n')
if lineEnd < 0 {
break
}
line := strings.TrimSuffix(pending[:lineEnd], "\r")
pending = pending[lineEnd+1:]
if line == "" {
if len(dataLines) > 0 {
var event updateEvent
if json.Unmarshal([]byte(strings.Join(dataLines, "\n")), &event) == nil {
fmt.Printf("收到更新:%s %s\n", event.Package.Name, event.Package.Version)
}
}
dataLines = dataLines[:0]
continue
}
if strings.HasPrefix(line, "data: ") {
dataLines = append(dataLines, strings.TrimPrefix(line, "data: "))
}
}
}
if err != nil {
if !errors.Is(err, io.EOF) {
fatal("读取更新通知失败: %v", err)
}
return
}
}
}
func fileSHA256(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func fatalResponse(response *http.Response) {
body, _ := io.ReadAll(io.LimitReader(response.Body, 1<<20))
fatal("服务端返回 %s:%s", response.Status, strings.TrimSpace(string(body)))
}
func requirePackage(name string) {
if name == "" {
fatal("必须指定 --package")
}
}
func fatal(format string, args ...any) {
fmt.Printf("错误:"+format+"\n", args...)
os.Exit(1)
}
func printUsage() {
fmt.Println("Scout 轻量化包管理客户端")
fmt.Println("用法:scout-client <check|download|watch> [参数]")
}