feat(backend): establish gin sqlite service skeleton

This commit is contained in:
QiuSW
2026-07-25 22:29:56 +08:00
parent 84f2da3cf7
commit ab21219a07
32 changed files with 1854 additions and 42 deletions
+127
View File
@@ -0,0 +1,127 @@
package config
import (
"errors"
"net"
"path/filepath"
"strconv"
"strings"
"time"
)
const (
HTTPAddressEnvironment = "CMROUBAO_HTTP_ADDR"
DatabasePathEnvironment = "CMROUBAO_DATABASE_PATH"
defaultHTTPAddress = "127.0.0.1:8080"
defaultDatabasePath = "var/cmroubao.db"
)
type LookupEnvironment func(string) (string, bool)
type Config struct {
HTTPAddress string
DatabasePath string
ReadHeaderTimeout time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
ShutdownTimeout time.Duration
MaxHeaderBytes int
}
func Load(lookup LookupEnvironment) (Config, error) {
httpAddress, err := environmentValue(
lookup,
HTTPAddressEnvironment,
defaultHTTPAddress,
)
if err != nil {
return Config{}, err
}
if err := validateHTTPAddress(httpAddress); err != nil {
return Config{}, err
}
databasePath, err := environmentValue(
lookup,
DatabasePathEnvironment,
defaultDatabasePath,
)
databasePath, err = validatedDatabasePath(databasePath, err)
if err != nil {
return Config{}, err
}
return Config{
HTTPAddress: httpAddress,
DatabasePath: filepath.Clean(databasePath),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
ShutdownTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}, nil
}
func LoadDatabasePath(lookup LookupEnvironment) (string, error) {
databasePath, err := environmentValue(
lookup,
DatabasePathEnvironment,
defaultDatabasePath,
)
return validatedDatabasePath(databasePath, err)
}
func environmentValue(
lookup LookupEnvironment,
name string,
defaultValue string,
) (string, error) {
value, exists := lookup(name)
if !exists {
return defaultValue, nil
}
value = strings.TrimSpace(value)
if value == "" {
return "", errors.New(name + " must not be blank")
}
return value, nil
}
func validateHTTPAddress(address string) error {
host, portValue, err := net.SplitHostPort(address)
if err != nil || strings.TrimSpace(host) == "" {
return errors.New(HTTPAddressEnvironment + " must include a host and port")
}
port, err := strconv.Atoi(portValue)
if err != nil || port < 1 || port > 65535 {
return errors.New(HTTPAddressEnvironment + " port must be between 1 and 65535")
}
return nil
}
func validatedDatabasePath(path string, previousError error) (string, error) {
if previousError != nil {
return "", previousError
}
if strings.ContainsRune(path, '\x00') {
return "", errors.New(
DatabasePathEnvironment + " contains an invalid character",
)
}
lowerPath := strings.ToLower(path)
cleanPath := filepath.Clean(path)
extension := strings.ToLower(filepath.Ext(cleanPath))
if cleanPath == "." ||
cleanPath == string(filepath.Separator) ||
lowerPath == ":memory:" ||
strings.HasPrefix(lowerPath, "file:") ||
(extension != ".db" && extension != ".sqlite" && extension != ".sqlite3") {
return "", errors.New(
DatabasePathEnvironment + " must be a SQLite file path",
)
}
return cleanPath, nil
}
+133
View File
@@ -0,0 +1,133 @@
package config
import (
"path/filepath"
"testing"
"time"
)
func TestLoadUsesSafeDefaults(t *testing.T) {
cfg, err := Load(emptyEnvironment)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.HTTPAddress != "127.0.0.1:8080" {
t.Fatalf("HTTPAddress = %q", cfg.HTTPAddress)
}
if cfg.DatabasePath != filepath.FromSlash("var/cmroubao.db") {
t.Fatalf("DatabasePath = %q", cfg.DatabasePath)
}
if cfg.ReadHeaderTimeout <= 0 ||
cfg.ReadTimeout <= 0 ||
cfg.WriteTimeout <= 0 ||
cfg.IdleTimeout <= 0 ||
cfg.ShutdownTimeout <= 0 ||
cfg.MaxHeaderBytes <= 0 {
t.Fatal("server safety limits must all be positive")
}
if cfg.ShutdownTimeout > 30*time.Second {
t.Fatalf("ShutdownTimeout = %s", cfg.ShutdownTimeout)
}
}
func TestLoadAcceptsExplicitConfiguration(t *testing.T) {
values := map[string]string{
HTTPAddressEnvironment: "192.0.2.10:9090",
DatabasePathEnvironment: "tmp/test.db",
}
cfg, err := Load(mapEnvironment(values))
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if cfg.HTTPAddress != values[HTTPAddressEnvironment] {
t.Fatalf("HTTPAddress = %q", cfg.HTTPAddress)
}
if cfg.DatabasePath != filepath.Clean(values[DatabasePathEnvironment]) {
t.Fatalf("DatabasePath = %q", cfg.DatabasePath)
}
}
func TestLoadRejectsUnsafeOrInvalidValues(t *testing.T) {
tests := []struct {
name string
values map[string]string
}{
{
name: "blank explicit address",
values: map[string]string{
HTTPAddressEnvironment: " ",
},
},
{
name: "address without host",
values: map[string]string{
HTTPAddressEnvironment: ":8080",
},
},
{
name: "invalid port",
values: map[string]string{
HTTPAddressEnvironment: "127.0.0.1:70000",
},
},
{
name: "memory database",
values: map[string]string{
DatabasePathEnvironment: ":memory:",
},
},
{
name: "database DSN",
values: map[string]string{
DatabasePathEnvironment: "file:test.db?mode=memory",
},
},
{
name: "database directory",
values: map[string]string{
DatabasePathEnvironment: "./",
},
},
{
name: "non SQLite extension",
values: map[string]string{
DatabasePathEnvironment: "var/database.txt",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if _, err := Load(mapEnvironment(test.values)); err == nil {
t.Fatal("Load() error = nil")
}
})
}
}
func TestLoadDatabasePathIgnoresHTTPConfiguration(t *testing.T) {
path, err := LoadDatabasePath(mapEnvironment(map[string]string{
HTTPAddressEnvironment: "invalid",
DatabasePathEnvironment: "tmp/migration.sqlite",
}))
if err != nil {
t.Fatalf("LoadDatabasePath() error = %v", err)
}
if path != filepath.Clean("tmp/migration.sqlite") {
t.Fatalf("path = %q", path)
}
}
func emptyEnvironment(string) (string, bool) {
return "", false
}
func mapEnvironment(values map[string]string) LookupEnvironment {
return func(name string) (string, bool) {
value, exists := values[name]
return value, exists
}
}