mirror of
https://git.evilfox.cc/test2/wg.git
synced 2026-08-01 04:13:24 +00:00
Add Go WireGuard panel with PostgreSQL 17 and Dokploy compose.
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
package mysqlimport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
var (
|
||||
reInsert = regexp.MustCompile(`(?is)INSERT INTO\s+\x60?(\w+)\x60?\s+VALUES\s*(.+?);`)
|
||||
reCreate = regexp.MustCompile(`(?is)CREATE TABLE\s+\x60?(\w+)\x60?`)
|
||||
)
|
||||
|
||||
// ImportFile converts and loads a MySQL dump into PostgreSQL once.
|
||||
func ImportFile(ctx context.Context, pool *pgxpool.Pool, dumpPath string) error {
|
||||
var done string
|
||||
err := pool.QueryRow(ctx, `SELECT value FROM app_meta WHERE key = 'mysql_import_done'`).Scan(&done)
|
||||
if err == nil && done == "1" {
|
||||
log.Println("mysql import already done, skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
raw, err := os.ReadFile(dumpPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read dump: %w", err)
|
||||
}
|
||||
if !utf8.Valid(raw) {
|
||||
// try windows-1251/latin1 fallback: replace invalid
|
||||
raw = []byte(strings.ToValidUTF8(string(raw), "?"))
|
||||
}
|
||||
text := string(raw)
|
||||
|
||||
tablesOrder := []string{
|
||||
"users",
|
||||
"site_status",
|
||||
"servers",
|
||||
"servers_simple",
|
||||
"countries",
|
||||
"wg_configs",
|
||||
"wg_config_settings",
|
||||
"demo_keys",
|
||||
"awg_configs",
|
||||
"amnezia_configs",
|
||||
"vless_configs",
|
||||
"admin_uploads",
|
||||
"user_servers",
|
||||
"license_keys",
|
||||
"rate_limits",
|
||||
}
|
||||
|
||||
tx, err := pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
// Temporarily disable FK checks via deferred constraints where possible
|
||||
if _, err := tx.Exec(ctx, `SET session_replication_role = replica`); err != nil {
|
||||
log.Printf("warn: cannot set session_replication_role: %v", err)
|
||||
}
|
||||
|
||||
inserts := map[string][]string{}
|
||||
for _, m := range reInsert.FindAllStringSubmatch(text, -1) {
|
||||
table := strings.ToLower(m[1])
|
||||
valuesPart := strings.TrimSpace(m[2])
|
||||
rows, err := splitValueTuples(valuesPart)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse inserts for %s: %w", table, err)
|
||||
}
|
||||
inserts[table] = append(inserts[table], rows...)
|
||||
}
|
||||
|
||||
for _, table := range tablesOrder {
|
||||
rows := inserts[table]
|
||||
if len(rows) == 0 {
|
||||
continue
|
||||
}
|
||||
log.Printf("importing %s (%d rows)", table, len(rows))
|
||||
if err := importTable(ctx, tx, table, rows); err != nil {
|
||||
return fmt.Errorf("import %s: %w", table, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Fix sequences
|
||||
for _, table := range tablesOrder {
|
||||
_, _ = tx.Exec(ctx, fmt.Sprintf(`
|
||||
SELECT setval(pg_get_serial_sequence('%s','id'), COALESCE((SELECT MAX(id) FROM %s), 1), true)
|
||||
`, table, table))
|
||||
}
|
||||
|
||||
_, err = tx.Exec(ctx, `
|
||||
INSERT INTO app_meta (key, value, updated_at)
|
||||
VALUES ('mysql_import_done', '1', NOW())
|
||||
ON CONFLICT (key) DO UPDATE SET value = '1', updated_at = NOW()`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(ctx, `SET session_replication_role = DEFAULT`); err != nil {
|
||||
log.Printf("warn: restore session_replication_role: %v", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("mysql dump imported successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
func importTable(ctx context.Context, tx pgx.Tx, table string, rows []string) error {
|
||||
cols, err := tableColumns(table)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, row := range rows {
|
||||
vals, err := parseRowValues(row)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(vals) != len(cols) {
|
||||
// try truncate/pad carefully
|
||||
if len(vals) < len(cols) {
|
||||
return fmt.Errorf("column count mismatch for %s: got %d want %d value=%s", table, len(vals), len(cols), truncate(row, 120))
|
||||
}
|
||||
vals = vals[:len(cols)]
|
||||
}
|
||||
converted := make([]any, len(vals))
|
||||
placeholders := make([]string, len(vals))
|
||||
for i, v := range vals {
|
||||
converted[i] = convertValue(table, cols[i], v)
|
||||
placeholders[i] = fmt.Sprintf("$%d", i+1)
|
||||
}
|
||||
sql := fmt.Sprintf(
|
||||
`INSERT INTO %s (%s) VALUES (%s) ON CONFLICT DO NOTHING`,
|
||||
table,
|
||||
strings.Join(quoteIdent(cols), ", "),
|
||||
strings.Join(placeholders, ", "),
|
||||
)
|
||||
if _, err := tx.Exec(ctx, sql, converted...); err != nil {
|
||||
return fmt.Errorf("insert failed (%s): %w; sample=%s", table, err, truncate(row, 160))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tableColumns(table string) ([]string, error) {
|
||||
m := map[string][]string{
|
||||
"admin_uploads": {"id", "original_name", "stored_name", "file_path", "file_url", "file_size", "uploaded_at"},
|
||||
"amnezia_configs": {"id", "config_name", "original_filename", "file_path", "file_url", "config_content", "host", "port", "username", "password", "description", "created_at"},
|
||||
"awg_configs": {"id", "config_name", "config_content", "qr_code_url", "created_at"},
|
||||
"countries": {"id", "code", "name_ru", "name_en", "flag", "sort_order"},
|
||||
"demo_keys": {"id", "config_id", "token", "expires_at", "created_at"},
|
||||
"license_keys": {"id", "key_value", "server_id", "protocol", "client_id", "email", "used_by", "created_at", "expires_at"},
|
||||
"rate_limits": {"id", "user_id", "action", "created_at"},
|
||||
"servers": {"id", "name", "ip", "port", "panel_username", "panel_password", "status", "created_at", "updated_at", "inbound_id"},
|
||||
"servers_simple": {"id", "name", "host", "port", "country", "status", "created_at"},
|
||||
"site_status": {"id", "is_online", "message", "updated_at"},
|
||||
"user_servers": {"id", "user_id", "server_id", "inbound_id", "protocol", "client_id", "email", "created_at"},
|
||||
"users": {"id", "username", "email", "password_hash", "role", "is_approved", "created_at", "updated_at"},
|
||||
"vless_configs": {"id", "vless_uri", "subscription_url", "created_at"},
|
||||
"wg_config_settings": {"setting_key", "setting_value", "updated_at"},
|
||||
"wg_configs": {"id", "token", "user_id", "config_content", "created_at", "original_filename", "is_demo"},
|
||||
}
|
||||
cols, ok := m[table]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown table %s", table)
|
||||
}
|
||||
return cols, nil
|
||||
}
|
||||
|
||||
func convertValue(table, col, raw string) any {
|
||||
v := strings.TrimSpace(raw)
|
||||
if strings.EqualFold(v, "NULL") {
|
||||
return nil
|
||||
}
|
||||
if len(v) >= 2 && v[0] == '\'' && v[len(v)-1] == '\'' {
|
||||
v = unquoteMySQLString(v)
|
||||
}
|
||||
|
||||
// MySQL dump has an empty country code for "Other"
|
||||
if table == "countries" && col == "code" && strings.TrimSpace(v) == "" {
|
||||
return "_"
|
||||
}
|
||||
|
||||
boolCols := map[string]bool{
|
||||
"is_approved": true,
|
||||
"is_online": true,
|
||||
"is_demo": true,
|
||||
}
|
||||
if boolCols[col] {
|
||||
if v == "1" || strings.EqualFold(v, "true") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
intCols := map[string]bool{
|
||||
"id": true, "user_id": true, "server_id": true, "config_id": true,
|
||||
"port": true, "file_size": true, "inbound_id": true, "sort_order": true,
|
||||
"used_by": true,
|
||||
}
|
||||
if intCols[col] {
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
_ = table
|
||||
return v
|
||||
}
|
||||
|
||||
func unquoteMySQLString(s string) string {
|
||||
s = s[1 : len(s)-1]
|
||||
replacer := strings.NewReplacer(
|
||||
`\\`, `\`,
|
||||
`\'`, `'`,
|
||||
`\"`, `"`,
|
||||
`\n`, "\n",
|
||||
`\r`, "\r",
|
||||
`\t`, "\t",
|
||||
`\0`, "\x00",
|
||||
`\Z`, "\x1a",
|
||||
)
|
||||
return replacer.Replace(s)
|
||||
}
|
||||
|
||||
func splitValueTuples(s string) ([]string, error) {
|
||||
var rows []string
|
||||
depth := 0
|
||||
inStr := false
|
||||
esc := false
|
||||
start := -1
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if inStr {
|
||||
if esc {
|
||||
esc = false
|
||||
continue
|
||||
}
|
||||
if c == '\\' {
|
||||
esc = true
|
||||
continue
|
||||
}
|
||||
if c == '\'' {
|
||||
// mysql '' escape
|
||||
if i+1 < len(s) && s[i+1] == '\'' {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
inStr = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if c == '\'' {
|
||||
inStr = true
|
||||
continue
|
||||
}
|
||||
if c == '(' {
|
||||
if depth == 0 {
|
||||
start = i + 1
|
||||
}
|
||||
depth++
|
||||
continue
|
||||
}
|
||||
if c == ')' {
|
||||
depth--
|
||||
if depth == 0 && start >= 0 {
|
||||
rows = append(rows, s[start:i])
|
||||
start = -1
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if depth != 0 || inStr {
|
||||
return nil, fmt.Errorf("unbalanced values clause")
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func parseRowValues(row string) ([]string, error) {
|
||||
var vals []string
|
||||
inStr := false
|
||||
esc := false
|
||||
start := 0
|
||||
for i := 0; i < len(row); i++ {
|
||||
c := row[i]
|
||||
if inStr {
|
||||
if esc {
|
||||
esc = false
|
||||
continue
|
||||
}
|
||||
if c == '\\' {
|
||||
esc = true
|
||||
continue
|
||||
}
|
||||
if c == '\'' {
|
||||
if i+1 < len(row) && row[i+1] == '\'' {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
inStr = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if c == '\'' {
|
||||
inStr = true
|
||||
continue
|
||||
}
|
||||
if c == ',' {
|
||||
vals = append(vals, strings.TrimSpace(row[start:i]))
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
vals = append(vals, strings.TrimSpace(row[start:]))
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
func quoteIdent(cols []string) []string {
|
||||
out := make([]string, len(cols))
|
||||
for i, c := range cols {
|
||||
out[i] = `"` + c + `"`
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n] + "..."
|
||||
}
|
||||
|
||||
// Touch for unused regex keep
|
||||
var _ = reCreate
|
||||
Reference in New Issue
Block a user