This commit is contained in:
2026-08-15 23:53:13 +08:00
parent e5b88fcf6c
commit c4b114762b
5 changed files with 288 additions and 12 deletions
+85 -3
View File
@@ -15,7 +15,10 @@ import (
)
func protoMap(message proto.Message) (map[string]any, error) {
raw, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(message)
raw, err := (protojson.MarshalOptions{
UseProtoNames: true,
EmitDefaultValues: true,
}).Marshal(message)
if err != nil {
return nil, err
}
@@ -44,7 +47,7 @@ func setYAMLMapping(node *yaml.Node, key string, value any) error {
}
for i := 0; i < len(node.Content); i += 2 {
if node.Content[i].Value == key {
node.Content[i+1] = replacement.Content[0]
mergeYAMLNode(node.Content[i+1], replacement.Content[0])
return nil
}
}
@@ -52,6 +55,35 @@ func setYAMLMapping(node *yaml.Node, key string, value any) error {
return nil
}
// mergeYAMLNode updates values produced from the runtime configuration while
// retaining keys that are not represented by the protobuf and comments that
// were already present in the user's config file. Replacing an entire mapping
// with protojson output would discard configuration hints and extension keys.
func mergeYAMLNode(dst, src *yaml.Node) {
if dst.Kind == yaml.MappingNode && src.Kind == yaml.MappingNode {
for i := 0; i+1 < len(src.Content); i += 2 {
key := src.Content[i].Value
found := false
for j := 0; j+1 < len(dst.Content); j += 2 {
if dst.Content[j].Value == key {
mergeYAMLNode(dst.Content[j+1], src.Content[i+1])
found = true
break
}
}
if !found {
dst.Content = append(dst.Content, src.Content[i], src.Content[i+1])
}
}
return
}
// Keep comments attached to a scalar/sequence node when its value changes.
head, line, foot := dst.HeadComment, dst.LineComment, dst.FootComment
*dst = *src
dst.HeadComment, dst.LineComment, dst.FootComment = head, line, foot
}
func (d *Data) persistConfig() error {
dataConfig, adminConfig := d.runtime.Values()
return d.persistConfigValues(dataConfig, adminConfig)
@@ -90,7 +122,57 @@ func (d *Data) persistConfigValuesLocked(dataConfig *conf.Data, adminConfig *con
if err = setYAMLMapping(&document, "admin", adminValue); err != nil {
return err
}
output, err := yaml.Marshal(&document)
return writeConfigDocument(configPath, &document)
}
// persistDatabaseConfig writes only the database selected on the init page.
// Initialization must not serialize the partially populated runtime config
// back over the template: doing so removes all omitted/default settings from
// data and admin and leaves the next startup with no visible configuration.
func (d *Data) persistDatabaseConfig(database *conf.Data_Database) error {
d.configMu.Lock()
defer d.configMu.Unlock()
configPath := d.runtime.ConfigPath()
if configPath == "" {
return nil
}
raw, err := os.ReadFile(configPath)
if err != nil {
return err
}
var document yaml.Node
if err = yaml.Unmarshal(raw, &document); err != nil {
return err
}
source, err := databaseDSN(database, "")
if err != nil {
return err
}
// Do not marshal the init request through protojson here. Proto3 omits
// empty and zero values, which makes a merge retain stale values from the
// template (an empty database password is the common example). Update the
// fields owned by the init page explicitly and leave advanced database,
// Redis, Mongo, admin and server settings untouched.
databaseValue := map[string]any{
"driver": database.Driver,
"source": source,
"host": database.Host,
"port": database.Port,
"user": database.User,
"password": database.Password,
"name": database.Name,
"config": database.Config,
"path": database.Path,
}
if err = setYAMLMapping(&document, "data", map[string]any{"database": databaseValue}); err != nil {
return err
}
return writeConfigDocument(configPath, &document)
}
func writeConfigDocument(configPath string, document *yaml.Node) error {
output, err := yaml.Marshal(document)
if err != nil {
return err
}