Add HTTP /v1 MVC, TCP send path, groups with server-side member-avatar collage, object-storage upload, and friends/favorites around the architecture-v3 layout. Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
2.9 KiB
Go
130 lines
2.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
mysqldriver "github.com/go-sql-driver/mysql"
|
|
"go.uber.org/zap"
|
|
|
|
"spark-server/internal/bootstrap"
|
|
"spark-server/internal/infrastructure/mysql"
|
|
redisx "spark-server/internal/infrastructure/redis"
|
|
)
|
|
|
|
func main() {
|
|
cfgPath := flag.String("config", "configs/config.yaml", "config path")
|
|
dir := flag.String("dir", "migrations", "SQL migrations directory")
|
|
flag.Parse()
|
|
if err := run(*cfgPath, *dir); err != nil {
|
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run(cfgPath, dir string) error {
|
|
cfg, err := bootstrap.Load(cfgPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log := zap.NewNop()
|
|
db, err := mysql.Open(log, &mysql.Options{
|
|
DSN: cfg.MySQL.DSN,
|
|
SlowThreshold: cfg.MySQL.SlowThreshold,
|
|
MaxOpenConns: cfg.MySQL.MaxOpenConns,
|
|
MaxIdleConns: cfg.MySQL.MaxIdleConns,
|
|
ConnMaxLifetime: cfg.MySQL.ConnMaxLifetime,
|
|
ConnMaxIdleTime: cfg.MySQL.ConnMaxIdleTime,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("mysql: %w", err)
|
|
}
|
|
defer db.Close()
|
|
if !db.Ready() {
|
|
return fmt.Errorf("mysql: dsn is empty")
|
|
}
|
|
rdb, err := redisx.Open(&redisx.Options{
|
|
Addr: cfg.Redis.Addr,
|
|
Username: cfg.Redis.Username,
|
|
Password: cfg.Redis.Password,
|
|
DB: cfg.Redis.DB,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("redis: %w", err)
|
|
}
|
|
defer rdb.Close()
|
|
if !rdb.Ready() {
|
|
return fmt.Errorf("redis: addr is empty")
|
|
}
|
|
pingCtx, pingCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer pingCancel()
|
|
if err := rdb.Ping(pingCtx); err != nil {
|
|
return fmt.Errorf("redis ping: %w", err)
|
|
}
|
|
files, err := filepath.Glob(filepath.Join(dir, "*.sql"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
gdb := db.Gorm()
|
|
ctx := context.Background()
|
|
for _, path := range files {
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i, stmt := range splitSQL(string(raw)) {
|
|
if err := gdb.WithContext(ctx).Exec(stmt).Error; err != nil {
|
|
if ignorableDDL(err) {
|
|
continue
|
|
}
|
|
return fmt.Errorf("%s statement %d: %w", filepath.Base(path), i+1, err)
|
|
}
|
|
}
|
|
fmt.Printf("ok %s\n", filepath.Base(path))
|
|
}
|
|
fmt.Println("mysql ok, redis ok")
|
|
return nil
|
|
}
|
|
|
|
func splitSQL(raw string) []string {
|
|
var b strings.Builder
|
|
for _, line := range strings.Split(raw, "\n") {
|
|
trim := strings.TrimSpace(line)
|
|
if trim == "" || strings.HasPrefix(trim, "--") {
|
|
continue
|
|
}
|
|
b.WriteString(line)
|
|
b.WriteByte('\n')
|
|
}
|
|
parts := strings.Split(b.String(), ";")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
s := strings.TrimSpace(p)
|
|
if s != "" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func ignorableDDL(err error) bool {
|
|
var me *mysqldriver.MySQLError
|
|
if !errors.As(err, &me) {
|
|
return false
|
|
}
|
|
switch me.Number {
|
|
case 1050, // table already exists
|
|
1060, // duplicate column
|
|
1061: // duplicate key name
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|