Localize user-facing UI, agent output, and backend messages to Simplified Chinese
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
docs / links (push) Waiting to run
web / web (push) Waiting to run
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
docs / links (push) Waiting to run
web / web (push) Waiting to run
This commit is contained in:
+22
-22
@@ -80,14 +80,14 @@ func applyStaged(p Paths) (Action, State) {
|
||||
log.Printf("[update] 暂存的新版本未通过校验,已丢弃,继续运行当前版本:%v", err)
|
||||
cleanStaged(p)
|
||||
_ = os.Remove(p.Marker)
|
||||
return Continue, State{FailedStage: true, Detail: "새 버전 검증에 실패해 폐기했습니다: " + err.Error()}
|
||||
return Continue, State{FailedStage: true, Detail: "新版本校验失败,已丢弃: " + err.Error()}
|
||||
}
|
||||
|
||||
if err := swap(p); err != nil {
|
||||
log.Printf("[update] 换装失败,继续运行当前版本:%v", err)
|
||||
cleanStaged(p)
|
||||
_ = os.Remove(p.Marker)
|
||||
return Continue, State{FailedStage: true, Detail: "버전 교체에 실패했습니다: " + err.Error()}
|
||||
return Continue, State{FailedStage: true, Detail: "版本替换失败: " + err.Error()}
|
||||
}
|
||||
|
||||
// 换装成功。保留标记,交给下一次启动(跑的就是新版)确认是否稳定。
|
||||
@@ -115,12 +115,12 @@ func confirmOrRollback(p Paths, m marker) (Action, State) {
|
||||
// 让进程按当前状态起——起不来的话用户至少能在日志里看到原因。
|
||||
log.Printf("[update] 新版本连续 %d 次启动失败,且回滚失败:%v", maxAttempts, err)
|
||||
_ = os.Remove(p.Marker)
|
||||
return Continue, State{Detail: "새 버전 시작에 실패하고 롤백도 실패했습니다: " + err.Error()}
|
||||
return Continue, State{Detail: "新版本启动失败,回滚也失败: " + err.Error()}
|
||||
}
|
||||
log.Printf("[update] 新版本连续 %d 次启动失败,已回滚到 %s,退出以重启(exit %d)",
|
||||
maxAttempts, orUnknown(m.From), ExitRestart)
|
||||
_ = os.Remove(p.Marker)
|
||||
return Restart, State{RolledBack: true, Detail: fmt.Sprintf("새 버전 시작에 실패해 이전 버전으로 롤백했습니다(%s)", orUnknown(m.From))}
|
||||
return Restart, State{RolledBack: true, Detail: fmt.Sprintf("新版本启动失败,已回滚到上一版本(%s)", orUnknown(m.From))}
|
||||
}
|
||||
if err := writeMarker(p.Marker, m); err != nil {
|
||||
log.Printf("[update] 更新升级标记失败:%v", err)
|
||||
@@ -160,14 +160,14 @@ const SettleDelay = 30 * time.Second
|
||||
func verifyStaged(p Paths) error {
|
||||
want, err := os.ReadFile(p.Sum)
|
||||
if err != nil {
|
||||
return fmt.Errorf("체크섬 읽기 실패: %w", err)
|
||||
return fmt.Errorf("校验和读取失败: %w", err)
|
||||
}
|
||||
got, err := fileSHA256(p.New)
|
||||
if err != nil {
|
||||
return fmt.Errorf("체크섬 계산 실패: %w", err)
|
||||
return fmt.Errorf("校验和计算失败: %w", err)
|
||||
}
|
||||
if !strings.EqualFold(strings.TrimSpace(string(want)), got) {
|
||||
return errors.New("SHA256 이 일치하지 않습니다(다운로드가 손상됐거나 변조됨)")
|
||||
return errors.New("SHA256 不匹配(下载已损坏或被篡改)")
|
||||
}
|
||||
return smokeTest(p.New)
|
||||
}
|
||||
@@ -176,7 +176,7 @@ func verifyStaged(p Paths) error {
|
||||
// 这能挡掉下载截断、架构选错(exec format error)、缺依赖等一大类问题。
|
||||
func smokeTest(bin string) error {
|
||||
if err := os.Chmod(bin, 0o755); err != nil {
|
||||
return fmt.Errorf("실행 권한 부여 실패: %w", err)
|
||||
return fmt.Errorf("授予执行权限失败: %w", err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
@@ -185,14 +185,14 @@ func smokeTest(bin string) error {
|
||||
cmd.Env = append(os.Environ(), smokeEnv+"=1")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if ctx.Err() != nil {
|
||||
return errors.New("스모크 테스트 시간 초과(새 바이너리가 응답하지 않음)")
|
||||
return errors.New("冒烟测试超时(新二进制文件无响应)")
|
||||
}
|
||||
if err != nil {
|
||||
snippet := strings.TrimSpace(string(out))
|
||||
if len(snippet) > 300 {
|
||||
snippet = snippet[:300] + "…"
|
||||
}
|
||||
return fmt.Errorf("스모크 테스트 실패: %v: %s", err, snippet)
|
||||
return fmt.Errorf("冒烟测试失败: %v: %s", err, snippet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -204,17 +204,17 @@ func smokeTest(bin string) error {
|
||||
func swap(p Paths) error {
|
||||
// Windows 的 rename 不会覆盖已存在的目标,上一轮升级留下的 .old 必须先清掉。
|
||||
if err := os.Remove(p.Old); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("이전 백업 %s 정리 실패: %w", p.Old, err)
|
||||
return fmt.Errorf("清理旧备份 %s 失败: %w", p.Old, err)
|
||||
}
|
||||
if err := os.Rename(p.Current, p.Old); err != nil {
|
||||
return fmt.Errorf("현재 버전 백업 실패: %w", err)
|
||||
return fmt.Errorf("当前版本备份失败: %w", err)
|
||||
}
|
||||
if err := os.Rename(p.New, p.Current); err != nil {
|
||||
// 换装失败但当前版本已经被挪走了,必须原样放回去,否则下次启动没有可执行文件。
|
||||
if rerr := os.Rename(p.Old, p.Current); rerr != nil {
|
||||
return fmt.Errorf("새 버전 설치 실패(%v), 현재 버전 복구도 실패: %w", err, rerr)
|
||||
return fmt.Errorf("新版本安装失败(%v),当前版本恢复也失败: %w", err, rerr)
|
||||
}
|
||||
return fmt.Errorf("새 버전 설치 실패: %w", err)
|
||||
return fmt.Errorf("新版本安装失败: %w", err)
|
||||
}
|
||||
_ = os.Remove(p.Sum)
|
||||
return nil
|
||||
@@ -223,16 +223,16 @@ func swap(p Paths) error {
|
||||
// rollback 把 swap 备份的旧版本换回来。
|
||||
func rollback(p Paths) error {
|
||||
if _, err := os.Stat(p.Old); err != nil {
|
||||
return fmt.Errorf("롤백할 백업이 없습니다(%s): %w", p.Old, err)
|
||||
return fmt.Errorf("没有可回滚的备份(%s): %w", p.Old, err)
|
||||
}
|
||||
// 把起不来的新版挪到 .failed 留作排查,而不是直接删掉。
|
||||
failed := p.Current + ".failed"
|
||||
_ = os.Remove(failed)
|
||||
if err := os.Rename(p.Current, failed); err != nil {
|
||||
return fmt.Errorf("실패한 버전 이동 실패: %w", err)
|
||||
return fmt.Errorf("移动失败版本失败: %w", err)
|
||||
}
|
||||
if err := os.Rename(p.Old, p.Current); err != nil {
|
||||
return fmt.Errorf("이전 버전 복구 실패: %w", err)
|
||||
return fmt.Errorf("上一版本恢复失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -245,21 +245,21 @@ func Rollback() error {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(p.Old); err != nil {
|
||||
return errors.New("롤백할 이전 버전이 없습니다(" + p.Old + " 없음)")
|
||||
return errors.New("没有可回滚的上一版本(不存在 " + p.Old + ")")
|
||||
}
|
||||
cleanStaged(p)
|
||||
if err := smokeTest(p.Old); err != nil {
|
||||
return fmt.Errorf("이전 버전을 실행할 수 없어 롤백을 거부합니다: %w", err)
|
||||
return fmt.Errorf("无法运行上一版本,拒绝回滚: %w", err)
|
||||
}
|
||||
// 交换当前与备份:回滚之后还能再滚回来。
|
||||
tmp := p.Current + ".swap"
|
||||
_ = os.Remove(tmp)
|
||||
if err := os.Rename(p.Current, tmp); err != nil {
|
||||
return fmt.Errorf("현재 버전 이동 실패: %w", err)
|
||||
return fmt.Errorf("当前版本移动失败: %w", err)
|
||||
}
|
||||
if err := os.Rename(p.Old, p.Current); err != nil {
|
||||
_ = os.Rename(tmp, p.Current)
|
||||
return fmt.Errorf("이전 버전 설치 실패: %w", err)
|
||||
return fmt.Errorf("上一版本安装失败: %w", err)
|
||||
}
|
||||
if err := os.Rename(tmp, p.Old); err != nil {
|
||||
log.Printf("[update] 回滚后整理备份失败(不影响运行):%v", err)
|
||||
@@ -293,7 +293,7 @@ func fileSHA256(path string) (string, error) {
|
||||
|
||||
func orUnknown(s string) string {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return "알 수 없는 버전"
|
||||
return "未知版本"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func NewClient(proxy string) *http.Client {
|
||||
Timeout: 30 * time.Minute, // 下载整包,不能按请求级超时卡死
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 10 {
|
||||
return fmt.Errorf("리다이렉트 횟수가 너무 많습니다")
|
||||
return fmt.Errorf("重定向次数过多")
|
||||
}
|
||||
return checkURL(req.URL)
|
||||
},
|
||||
@@ -76,10 +76,10 @@ func NewClient(proxy string) *http.Client {
|
||||
// checkURL 强制 https + 域名白名单。
|
||||
func checkURL(u *url.URL) error {
|
||||
if u.Scheme != "https" {
|
||||
return fmt.Errorf("HTTPS 가 아닌 주소를 거부합니다: %s", u.Scheme+"://"+u.Host)
|
||||
return fmt.Errorf("拒绝非 HTTPS 地址: %s", u.Scheme+"://"+u.Host)
|
||||
}
|
||||
if !allowedHosts[strings.ToLower(u.Hostname())] {
|
||||
return fmt.Errorf("GitHub 도메인이 아닌 주소를 거부합니다: %s", u.Hostname())
|
||||
return fmt.Errorf("拒绝非 GitHub 域名地址: %s", u.Hostname())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -98,26 +98,26 @@ func FetchLatest(ctx context.Context, c *http.Client) (*Release, error) {
|
||||
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GitHub 에 접속하지 못했습니다(시스템 설정에서 전역 프록시를 설정할 수 있습니다): %w", err)
|
||||
return nil, fmt.Errorf("无法连接到 GitHub(可在系统设置中配置全局代理): %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch {
|
||||
case resp.StatusCode == http.StatusForbidden, resp.StatusCode == http.StatusTooManyRequests:
|
||||
// 未认证的 GitHub API 是每 IP 每小时 60 次,共用出口 IP 时很容易撞上。
|
||||
return nil, fmt.Errorf("GitHub API 요청 제한에 걸렸습니다(시간당 60회). 잠시 후 다시 시도하세요")
|
||||
return nil, fmt.Errorf("触发了 GitHub API 请求限制(每小时 60 次)。请稍后重试")
|
||||
case resp.StatusCode == http.StatusNotFound:
|
||||
return nil, fmt.Errorf("저장소 %s 에 아직 정식 릴리스가 없습니다", Repo)
|
||||
return nil, fmt.Errorf("仓库 %s 尚未有正式发布", Repo)
|
||||
case resp.StatusCode != http.StatusOK:
|
||||
return nil, fmt.Errorf("GitHub 응답 코드 %d", resp.StatusCode)
|
||||
return nil, fmt.Errorf("GitHub 响应代码 %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var rel Release
|
||||
if err := json.NewDecoder(resp.Body).Decode(&rel); err != nil {
|
||||
return nil, fmt.Errorf("Release 해석 실패: %w", err)
|
||||
return nil, fmt.Errorf("Release 解析失败: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(rel.TagName) == "" {
|
||||
return nil, fmt.Errorf("Release 에 tag 가 없습니다")
|
||||
return nil, fmt.Errorf("Release 中没有 tag")
|
||||
}
|
||||
return &rel, nil
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ type Paths struct {
|
||||
func ResolvePaths() (Paths, error) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return Paths{}, fmt.Errorf("실행 파일 위치 확인 실패: %w", err)
|
||||
return Paths{}, fmt.Errorf("可执行文件位置检查失败: %w", err)
|
||||
}
|
||||
if resolved, err := filepath.EvalSymlinks(exe); err == nil {
|
||||
exe = resolved
|
||||
|
||||
@@ -33,19 +33,19 @@ func hasHanzi(s string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// assertKoreanError 는 오류가 존재하고, 한글을 포함하며, 중국어 한자가 없음을 단언한다.
|
||||
// %w 로 감싼 하부 stdlib 오류의 영어는 허용된다(한자만 금지).
|
||||
func assertKoreanError(t *testing.T, label string, err error) {
|
||||
// assertChineseError 는 오류가 존재하고, 한자를 포함하며, 한글이 없음을 단언한다.
|
||||
// %w 로 감싼 하부 stdlib 오류의 영어는 허용된다(한글만 금지).
|
||||
func assertChineseError(t *testing.T, label string, err error) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Fatalf("%s: 오류를 기대했으나 nil 이었습니다", label)
|
||||
}
|
||||
msg := err.Error()
|
||||
if !hasHangul(msg) {
|
||||
t.Errorf("%s: 한글이 없습니다: %q", label, msg)
|
||||
if !hasHanzi(msg) {
|
||||
t.Errorf("%s: 한자가 없습니다: %q", label, msg)
|
||||
}
|
||||
if hasHanzi(msg) {
|
||||
t.Errorf("%s: 중국어 한자가 남아 있습니다: %q", label, msg)
|
||||
if hasHangul(msg) {
|
||||
t.Errorf("%s: 한글이 남아 있습니다: %q", label, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,13 +68,13 @@ func stubClient(status int, body string) *http.Client {
|
||||
// 문구 언어는 보지 않는다. 여기서는 두 거부 경로의 오류가 한국어인지 확인한다.
|
||||
func TestCheckURLErrorsLocalized(t *testing.T) {
|
||||
nonHTTPS := checkURL(mustParse(t, "http://api.github.com/x"))
|
||||
assertKoreanError(t, "non-https", nonHTTPS)
|
||||
assertChineseError(t, "non-https", nonHTTPS)
|
||||
if !strings.Contains(nonHTTPS.Error(), "HTTPS") {
|
||||
t.Errorf("non-https 오류에 HTTPS 표기가 없습니다: %q", nonHTTPS.Error())
|
||||
}
|
||||
|
||||
nonGitHub := checkURL(mustParse(t, "https://evil.example.com/x"))
|
||||
assertKoreanError(t, "non-github-host", nonGitHub)
|
||||
assertChineseError(t, "non-github-host", nonGitHub)
|
||||
if !strings.Contains(nonGitHub.Error(), "GitHub") {
|
||||
t.Errorf("non-github 오류에 GitHub 표기가 없습니다: %q", nonGitHub.Error())
|
||||
}
|
||||
@@ -86,25 +86,25 @@ func TestFetchLatestErrorsLocalized(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
_, errRate := FetchLatest(ctx, stubClient(http.StatusForbidden, ""))
|
||||
assertKoreanError(t, "rate-limited", errRate)
|
||||
assertChineseError(t, "rate-limited", errRate)
|
||||
if !strings.Contains(errRate.Error(), "60") {
|
||||
t.Errorf("요청 제한 오류에 60 표기가 없습니다: %q", errRate.Error())
|
||||
}
|
||||
|
||||
_, errNF := FetchLatest(ctx, stubClient(http.StatusNotFound, ""))
|
||||
assertKoreanError(t, "not-found", errNF)
|
||||
assertChineseError(t, "not-found", errNF)
|
||||
|
||||
_, errStatus := FetchLatest(ctx, stubClient(http.StatusInternalServerError, ""))
|
||||
assertKoreanError(t, "bad-status", errStatus)
|
||||
assertChineseError(t, "bad-status", errStatus)
|
||||
if !strings.Contains(errStatus.Error(), "GitHub") {
|
||||
t.Errorf("상태 오류에 GitHub 표기가 없습니다: %q", errStatus.Error())
|
||||
}
|
||||
|
||||
_, errJSON := FetchLatest(ctx, stubClient(http.StatusOK, "{not json"))
|
||||
assertKoreanError(t, "bad-json", errJSON)
|
||||
assertChineseError(t, "bad-json", errJSON)
|
||||
|
||||
_, errTag := FetchLatest(ctx, stubClient(http.StatusOK, "{}"))
|
||||
assertKoreanError(t, "missing-tag", errTag)
|
||||
assertChineseError(t, "missing-tag", errTag)
|
||||
if !strings.Contains(errTag.Error(), "tag") {
|
||||
t.Errorf("tag 누락 오류에 tag 표기가 없습니다: %q", errTag.Error())
|
||||
}
|
||||
@@ -116,5 +116,5 @@ func TestFetchLatestErrorsLocalized(t *testing.T) {
|
||||
func TestStageMissingAssetLocalized(t *testing.T) {
|
||||
rel := &Release{TagName: "v9.9.9"} // Assets 비어 있음
|
||||
err := Stage(context.Background(), &http.Client{}, rel, "0.0.1", nil)
|
||||
assertKoreanError(t, "stage-missing-asset", err)
|
||||
assertChineseError(t, "stage-missing-asset", err)
|
||||
}
|
||||
|
||||
+31
-31
@@ -59,17 +59,17 @@ func Stage(ctx context.Context, c *http.Client, rel *Release, currentVersion str
|
||||
name := AssetName(rel.TagName, runtime.GOOS, runtime.GOARCH)
|
||||
asset, ok := rel.FindAsset(name)
|
||||
if !ok {
|
||||
return fmt.Errorf("이 버전은 %s/%s 용 릴리스 패키지를 제공하지 않습니다(%s 누락)", runtime.GOOS, runtime.GOARCH, name)
|
||||
return fmt.Errorf("该版本未提供适用于 %s/%s 的发布包(缺少 %s)", runtime.GOOS, runtime.GOARCH, name)
|
||||
}
|
||||
|
||||
prog(PhaseDownload, 0, "체크섬 목록을 가져오는 중…")
|
||||
prog(PhaseDownload, 0, "正在获取校验和列表…")
|
||||
sums, err := fetchSums(ctx, c, rel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
want, ok := sums[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("%s 에 %s 항목이 없어 검증되지 않은 바이너리 설치를 거부합니다", sumsAsset, name)
|
||||
return fmt.Errorf("%s 中没有 %s 条目,拒绝安装未经验证的二进制文件", sumsAsset, name)
|
||||
}
|
||||
|
||||
// 临时文件全部落在目标目录里,保证最后的 rename 是同一文件系统内的原子操作
|
||||
@@ -81,37 +81,37 @@ func Stage(ctx context.Context, c *http.Client, rel *Release, currentVersion str
|
||||
_ = os.Remove(binPath)
|
||||
}()
|
||||
|
||||
prog(PhaseDownload, 0, fmt.Sprintf("%s 내려받는 중(%s)…", name, humanSize(asset.Size)))
|
||||
prog(PhaseDownload, 0, fmt.Sprintf("正在下载 %s(%s)…", name, humanSize(asset.Size)))
|
||||
got, err := download(ctx, c, asset, zipPath, prog)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prog(PhaseVerify, -1, "SHA256 검증 중…")
|
||||
prog(PhaseVerify, -1, "正在校验 SHA256…")
|
||||
if !strings.EqualFold(got, want) {
|
||||
return fmt.Errorf("SHA256 이 일치하지 않습니다: 기대값 %s, 실제값 %s(다운로드가 손상됐거나 변조됨)", short(want), short(got))
|
||||
return fmt.Errorf("SHA256 不匹配: 期待值 %s, 实际值 %s(下载已损坏或被篡改)", short(want), short(got))
|
||||
}
|
||||
|
||||
prog(PhaseExtract, -1, "압축을 풀고 스모크 테스트 중…")
|
||||
prog(PhaseExtract, -1, "正在解压并进行冒烟测试…")
|
||||
if err := extractBinary(zipPath, binPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := smokeTest(binPath); err != nil {
|
||||
return fmt.Errorf("새 버전이 현재 시스템에서 실행되지 않습니다: %w", err)
|
||||
return fmt.Errorf("新版本在当前系统上无法运行: %w", err)
|
||||
}
|
||||
|
||||
// 暂存件自己的 sha256 单独存一份:下次启动换装前还要再校验一次,
|
||||
// 防止暂存后到重启前这段时间里文件被改动或写坏。
|
||||
binSum, err := fileSHA256(binPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("새 바이너리 체크섬 계산 실패: %w", err)
|
||||
return fmt.Errorf("新二进制文件校验和计算失败: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(p.Sum, []byte(binSum), 0o644); err != nil {
|
||||
return fmt.Errorf("체크섬 기록 실패: %w", err)
|
||||
return fmt.Errorf("校验和记录失败: %w", err)
|
||||
}
|
||||
if err := os.Rename(binPath, p.New); err != nil {
|
||||
_ = os.Remove(p.Sum)
|
||||
return fmt.Errorf("새 버전 준비 실패: %w", err)
|
||||
return fmt.Errorf("新版本准备失败: %w", err)
|
||||
}
|
||||
|
||||
if err := writeMarker(p.Marker, marker{
|
||||
@@ -120,10 +120,10 @@ func Stage(ctx context.Context, c *http.Client, rel *Release, currentVersion str
|
||||
StagedAt: time.Now().Unix(),
|
||||
}); err != nil {
|
||||
// 标记只影响自动回滚能力,暂存件本身已就位,不因此中断升级。
|
||||
prog(PhaseStaged, -1, "경고: 업데이트 표시 파일 기록에 실패해 이번 업데이트는 자동 롤백 보호를 받지 못합니다")
|
||||
prog(PhaseStaged, -1, "警告: 更新标记文件写入失败,本次更新将不受自动回滚保护")
|
||||
}
|
||||
|
||||
prog(PhaseStaged, 100, "새 버전이 준비되었습니다. 재시작 중…")
|
||||
prog(PhaseStaged, 100, "新版本已准备就绪。正在重启…")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -131,21 +131,21 @@ func Stage(ctx context.Context, c *http.Client, rel *Release, currentVersion str
|
||||
func fetchSums(ctx context.Context, c *http.Client, rel *Release) (map[string]string, error) {
|
||||
asset, ok := rel.FindAsset(sumsAsset)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("이 릴리스에 %s 항목이 없어 무결성을 검증할 수 없으므로 업데이트를 거부합니다", sumsAsset)
|
||||
return nil, fmt.Errorf("该发布中没有 %s 条目,无法验证完整性,拒绝更新", sumsAsset)
|
||||
}
|
||||
body, err := get(ctx, c, asset.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s 내려받기 실패: %w", sumsAsset, err)
|
||||
return nil, fmt.Errorf("%s 下载失败: %w", sumsAsset, err)
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
raw, err := io.ReadAll(io.LimitReader(body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s 읽기 실패: %w", sumsAsset, err)
|
||||
return nil, fmt.Errorf("%s 读取失败: %w", sumsAsset, err)
|
||||
}
|
||||
out := parseSums(string(raw))
|
||||
if len(out) == 0 {
|
||||
return nil, fmt.Errorf("%s 내용이 비어 있거나 형식을 인식할 수 없습니다", sumsAsset)
|
||||
return nil, fmt.Errorf("%s 内容为空或格式无法识别", sumsAsset)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -191,26 +191,26 @@ func isHexSHA256(s string) bool {
|
||||
func download(ctx context.Context, c *http.Client, a Asset, dst string, prog Progress) (string, error) {
|
||||
body, err := get(ctx, c, a.URL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s 내려받기 실패: %w", a.Name, err)
|
||||
return "", fmt.Errorf("%s 下载失败: %w", a.Name, err)
|
||||
}
|
||||
defer body.Close()
|
||||
|
||||
f, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("임시 파일 생성 실패: %w", err)
|
||||
return "", fmt.Errorf("临时文件创建失败: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
h := sha256.New()
|
||||
pw := &progressWriter{total: a.Size, prog: prog, name: a.Name, last: time.Now()}
|
||||
if _, err := io.Copy(io.MultiWriter(f, h, pw), body); err != nil {
|
||||
return "", fmt.Errorf("다운로드가 중단되었습니다: %w", err)
|
||||
return "", fmt.Errorf("下载已中断: %w", err)
|
||||
}
|
||||
if err := f.Sync(); err != nil {
|
||||
return "", fmt.Errorf("디스크 기록 실패: %w", err)
|
||||
return "", fmt.Errorf("磁盘写入失败: %w", err)
|
||||
}
|
||||
if a.Size > 0 && pw.written != a.Size {
|
||||
return "", fmt.Errorf("다운로드가 완전하지 않습니다: 기대 %d바이트, 실제 %d바이트", a.Size, pw.written)
|
||||
return "", fmt.Errorf("下载不完整: 期待 %d 字节, 实际 %d 字节", a.Size, pw.written)
|
||||
}
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
@@ -247,7 +247,7 @@ func extractBinary(zipPath, dst string) error {
|
||||
}
|
||||
zr, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("릴리스 패키지 열기 실패: %w", err)
|
||||
return fmt.Errorf("发布包打开失败: %w", err)
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
@@ -257,29 +257,29 @@ func extractBinary(zipPath, dst string) error {
|
||||
}
|
||||
rc, err := entry.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s 읽기 실패: %w", entry.Name, err)
|
||||
return fmt.Errorf("%s 读取失败: %w", entry.Name, err)
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
f, err := os.OpenFile(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("새 바이너리 쓰기 실패: %w", err)
|
||||
return fmt.Errorf("新二进制文件写入失败: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
n, err := io.Copy(f, io.LimitReader(rc, maxBinarySize+1))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s 압축 해제 실패: %w", entry.Name, err)
|
||||
return fmt.Errorf("%s 解压失败: %w", entry.Name, err)
|
||||
}
|
||||
if n > maxBinarySize {
|
||||
return fmt.Errorf("릴리스 패키지 안의 실행 파일이 %s 보다 커서 압축 해제를 거부합니다", humanSize(maxBinarySize))
|
||||
return fmt.Errorf("发布包内的可执行文件超过 %s,拒绝解压", humanSize(maxBinarySize))
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("릴리스 패키지 안의 %s 파일이 비어 있습니다", want)
|
||||
return fmt.Errorf("发布包内的 %s 文件为空", want)
|
||||
}
|
||||
return f.Sync()
|
||||
}
|
||||
return fmt.Errorf("릴리스 패키지에서 %s 파일을 찾지 못했습니다", want)
|
||||
return fmt.Errorf("在发布包中未找到 %s 文件", want)
|
||||
}
|
||||
|
||||
// checkWritable 提前确认目录可写。没有这一步,非 root 运行、或二进制被放在系统
|
||||
@@ -287,7 +287,7 @@ func extractBinary(zipPath, dst string) error {
|
||||
func checkWritable(dir string) error {
|
||||
probe, err := os.CreateTemp(dir, ".artex-update-probe-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("프로그램 디렉터리 %s 에 쓸 수 없어 자동 업데이트를 할 수 없습니다(권한을 확인하거나 수동 업데이트를 사용하세요): %w", dir, err)
|
||||
return fmt.Errorf("无法写入程序目录 %s,无法自动更新(请检查权限或使用手动更新): %w", dir, err)
|
||||
}
|
||||
name := probe.Name()
|
||||
_ = probe.Close()
|
||||
@@ -314,7 +314,7 @@ func (w *progressWriter) Write(b []byte) (int, error) {
|
||||
if w.total > 0 {
|
||||
pct = int(w.written * 100 / w.total)
|
||||
}
|
||||
w.prog(PhaseDownload, pct, fmt.Sprintf("내려받는 중 %s / %s", humanSize(w.written), humanSize(w.total)))
|
||||
w.prog(PhaseDownload, pct, fmt.Sprintf("正在下载 %s / %s", humanSize(w.written), humanSize(w.total)))
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user