Files
code-utils/bootstrap.go
2026-08-11 19:07:05 +08:00

139 lines
3.3 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
const (
BootstrapReady = "ready"
BootstrapSetup = "setup_required"
BootstrapRecovery = "recovery_required"
)
type BootstrapConfig struct {
DatabasePath string `json:"databasePath"`
Initialized bool `json:"initialized"`
}
type BootstrapStatus struct {
State string `json:"state"`
DatabasePath string `json:"databasePath"`
DefaultPath string `json:"defaultPath"`
ErrorCode string `json:"errorCode,omitempty"`
ErrorDetail string `json:"errorDetail,omitempty"`
}
type CodedError struct{ Code, Detail string }
func (e *CodedError) Error() string {
if e.Detail == "" {
return e.Code
}
return e.Code + ": " + e.Detail
}
func coded(code string, err error) error {
if err == nil {
return &CodedError{Code: code}
}
return &CodedError{Code: code, Detail: err.Error()}
}
func bootstrapPath() (string, error) {
d, e := os.UserConfigDir()
if e != nil {
return "", e
}
return filepath.Join(d, "CodeCount", "bootstrap.json"), nil
}
func readBootstrap(path string) (BootstrapConfig, error) {
b, e := os.ReadFile(path)
if e != nil {
return BootstrapConfig{}, e
}
var c BootstrapConfig
if e = json.Unmarshal(b, &c); e != nil {
return c, e
}
if !c.Initialized || strings.TrimSpace(c.DatabasePath) == "" {
return c, errors.New("invalid bootstrap configuration")
}
return c, nil
}
func writeBootstrap(path string, c BootstrapConfig) error {
if e := os.MkdirAll(filepath.Dir(path), 0755); e != nil {
return e
}
b, e := json.MarshalIndent(c, "", " ")
if e != nil {
return e
}
tmp := path + ".tmp"
if e = os.WriteFile(tmp, b, 0600); e != nil {
return e
}
if e = os.Rename(tmp, path); e != nil {
_ = os.Remove(tmp)
return e
}
return nil
}
func createValidatedStore(target string) (*Store, error) {
target = filepath.Clean(strings.TrimSpace(target))
if target == "" {
return nil, coded("DB_PATH_REQUIRED", nil)
}
dir := filepath.Dir(target)
if e := os.MkdirAll(dir, 0755); e != nil {
return nil, coded("DB_DIRECTORY_UNWRITABLE", e)
}
probe := filepath.Join(dir, ".code-count-write-test")
if e := os.WriteFile(probe, []byte("ok"), 0600); e != nil {
return nil, coded("DB_DIRECTORY_UNWRITABLE", e)
}
_ = os.Remove(probe)
if _, e := os.Stat(target); e == nil {
return OpenStore(target)
} else if !os.IsNotExist(e) {
return nil, coded("DB_FILE_UNREADABLE", e)
}
tmp := target + ".initializing"
_ = os.Remove(tmp)
_ = os.Remove(tmp + "-wal")
_ = os.Remove(tmp + "-shm")
s, e := OpenStore(tmp)
if e != nil {
return nil, coded("DB_INITIALIZE_FAILED", e)
}
var result string
e = s.db.QueryRow(`PRAGMA integrity_check`).Scan(&result)
_ = s.db.Close()
if e != nil || result != "ok" {
_ = os.Remove(tmp)
return nil, coded("DB_INTEGRITY_FAILED", e)
}
if e = os.Rename(tmp, target); e != nil {
_ = os.Remove(tmp)
return nil, coded("DB_INITIALIZE_FAILED", e)
}
s, e = OpenStore(target)
if e != nil {
return nil, coded("DB_OPEN_FAILED", e)
}
return s, nil
}
func bootstrapFailure(defaultPath, path string, e error) BootstrapStatus {
code := "DB_OPEN_FAILED"
var ce *CodedError
if errors.As(e, &ce) {
code = ce.Code
}
return BootstrapStatus{State: BootstrapRecovery, DatabasePath: path, DefaultPath: defaultPath, ErrorCode: code, ErrorDetail: fmt.Sprint(e)}
}