In-place upgrade tool for Stalwart Mail Server (0.15.5 -> latest) with checkpointed rollback and post-migration validation. Design stage; see ARCHITECTURE.md.
107 lines
3.2 KiB
Go
107 lines
3.2 KiB
Go
package preflight
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// knownBackends is the set of store/blob/FTS backend identifiers Stalwart's
|
|
// documentation names: RocksDB, SQLite, FoundationDB, PostgreSQL, MySQL,
|
|
// S3-compatible storage, and Elasticsearch.
|
|
var knownBackends = map[string]bool{
|
|
"rocksdb": true, "sqlite": true, "foundationdb": true,
|
|
"postgresql": true, "mysql": true, "s3": true, "elasticsearch": true,
|
|
}
|
|
|
|
// BackendMatch is one "type = <backend>" assignment found in a config
|
|
// file, tagged with where it was found (the enclosing TOML section, or the
|
|
// dotted JSON key path).
|
|
type BackendMatch struct {
|
|
Path string
|
|
Backend string
|
|
}
|
|
|
|
// DetectStoreBackends scans a Stalwart config file for store/blob/FTS
|
|
// backend declarations. It deliberately does not assume one fixed schema
|
|
// path: the exact TOML (pre-0.16) or JSON (0.16+) layout has already
|
|
// changed once between those versions and may change again, so this
|
|
// searches structurally for `type = "<known backend>"` assignments wherever
|
|
// they appear and reports every match with its location. Treat the result
|
|
// as "here's what to confirm before an unattended run", not ground truth.
|
|
func DetectStoreBackends(configPath string) ([]BackendMatch, error) {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("preflight: read config %s: %w", configPath, err)
|
|
}
|
|
if json.Valid(data) {
|
|
return scanJSONBackends(data)
|
|
}
|
|
return scanTOMLBackends(data)
|
|
}
|
|
|
|
var (
|
|
tomlSectionRe = regexp.MustCompile(`^\[(.+)\]$`)
|
|
tomlKVRe = regexp.MustCompile(`^([A-Za-z0-9_.-]+)\s*=\s*"([^"]*)"$`)
|
|
)
|
|
|
|
func scanTOMLBackends(data []byte) ([]BackendMatch, error) {
|
|
var matches []BackendMatch
|
|
section := ""
|
|
scanner := bufio.NewScanner(bytes.NewReader(data))
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if line == "" || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
if m := tomlSectionRe.FindStringSubmatch(line); m != nil {
|
|
section = m[1]
|
|
continue
|
|
}
|
|
if m := tomlKVRe.FindStringSubmatch(line); m != nil {
|
|
key, value := m[1], strings.ToLower(m[2])
|
|
if key == "type" && knownBackends[value] {
|
|
matches = append(matches, BackendMatch{Path: section, Backend: value})
|
|
}
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return nil, fmt.Errorf("preflight: scan toml config: %w", err)
|
|
}
|
|
return matches, nil
|
|
}
|
|
|
|
func scanJSONBackends(data []byte) ([]BackendMatch, error) {
|
|
var root any
|
|
if err := json.Unmarshal(data, &root); err != nil {
|
|
return nil, fmt.Errorf("preflight: parse json config: %w", err)
|
|
}
|
|
var matches []BackendMatch
|
|
walkJSONForBackends(root, "", &matches)
|
|
return matches, nil
|
|
}
|
|
|
|
func walkJSONForBackends(node any, path string, matches *[]BackendMatch) {
|
|
switch v := node.(type) {
|
|
case map[string]any:
|
|
if t, ok := v["type"].(string); ok && knownBackends[strings.ToLower(t)] {
|
|
*matches = append(*matches, BackendMatch{Path: path, Backend: strings.ToLower(t)})
|
|
}
|
|
for k, child := range v {
|
|
childPath := k
|
|
if path != "" {
|
|
childPath = path + "." + k
|
|
}
|
|
walkJSONForBackends(child, childPath, matches)
|
|
}
|
|
case []any:
|
|
for i, child := range v {
|
|
walkJSONForBackends(child, fmt.Sprintf("%s[%d]", path, i), matches)
|
|
}
|
|
}
|
|
}
|