139 lines
3.3 KiB
Go
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)}
|
|
}
|