Smail/test/tools/format-json/main.go
smail 560605a8e1
Some checks are pending
Security Checks / security (push) Waiting to run
统一配置JSON缩进排版并保持摘要与历史兼容
2026-09-08 21:08:36 +08:00

113 lines
3.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 (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"git.pcux.cn/xuans/Smail/utils/atomicfile"
)
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
directory := flag.String("config", "", "已停止后端的配置目录")
backup := flag.String("backup", "", "保存原字节的独立备份目录")
apply := flag.Bool("apply", false, "确认停服,执行排版;默认只检查")
flag.Parse()
if !filepath.IsAbs(*directory) || !filepath.IsAbs(*backup) {
return errors.New("配置及备份目录必须使用明确的绝对路径")
}
info, err := os.Lstat(*directory)
if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return errors.New("配置目录不可用或是链接")
}
names := []string{"smtp.json", "email.json", "ssl.json", "plugins.json", "config.json"}
original, formatted := map[string][]byte{}, map[string][]byte{}
for _, name := range names {
path := filepath.Join(*directory, name)
info, err := os.Lstat(path)
if err != nil || !info.Mode().IsRegular() || info.Size() > 4<<20 {
return errors.New("配置文件不可用、过大或不是普通文件")
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
var result bytes.Buffer
// 不解析再编码业务值,保留中文、密文、数字及转义的原始字节。
if err := json.Indent(&result, data, "", " "); err != nil {
return fmt.Errorf("%s 不是有效 JSON,未写入", name)
}
original[name], formatted[name] = data, result.Bytes()
}
var root struct {
Sections map[string]struct {
File string `json:"file"`
SHA256 string `json:"sha256"`
} `json:"sections"`
}
if err := json.Unmarshal(original["config.json"], &root); err != nil || len(root.Sections) != 4 {
return errors.New("仅支持已完成分区的五文件配置")
}
for _, name := range names[:4] {
reference := root.Sections[name[:len(name)-5]]
if reference.File != name || reference.SHA256 != digest(original[name]) {
return fmt.Errorf("%s 原有摘要不匹配,未写入", name)
}
old := []byte(`"sha256": "` + reference.SHA256 + `"`)
next := []byte(`"sha256": "` + digest(formatted[name]) + `"`)
if bytes.Count(formatted["config.json"], old) != 1 {
return errors.New("根配置摘要位置不唯一,未写入")
}
formatted["config.json"] = bytes.Replace(formatted["config.json"], old, next, 1)
}
changed := false
for _, name := range names {
changed = changed || !bytes.Equal(original[name], formatted[name])
}
if !*apply || !changed {
fmt.Printf("检查通过:五个 JSON 可读取,是否需要排版:%t。\n", changed)
return nil
}
// 备份目录必须不存在,拒绝覆盖之前的备份。
if err := os.Mkdir(*backup, 0700); err != nil {
return errors.New("无法创建独立备份目录,未改写配置")
}
for _, name := range names {
if err := atomicfile.Write(filepath.Join(*backup, name), original[name], 0600); err != nil {
return errors.New("备份未完成,未改写配置")
}
}
for _, name := range names {
data, err := os.ReadFile(filepath.Join(*directory, name))
if err != nil || !bytes.Equal(data, original[name]) {
return errors.New("备份期间配置发生变化,未改写配置")
}
}
for _, name := range names {
if err := atomicfile.Write(filepath.Join(*directory, name), formatted[name], 0600); err != nil {
return errors.New("写入未完成,请保持停服并使用本次备份恢复五个文件")
}
}
fmt.Println("五个 JSON 已分行缩进,摘要已同步;配置值、代次及密文未改变。")
return nil
}
func digest(data []byte) string {
value := sha256.Sum256(data)
return hex.EncodeToString(value[:])
}