223 lines
5.8 KiB
Go
223 lines
5.8 KiB
Go
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> [参数]")
|
||
}
|