+2
-1
@@ -19,6 +19,7 @@ steps:
|
||||
commands:
|
||||
- mkdir -p dist
|
||||
- go build -trimpath -ldflags="-s -w" -o dist/syncdb ./cmd/syncdb
|
||||
- GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o dist/syncdb.exe ./cmd/syncdb
|
||||
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
@@ -30,4 +31,4 @@ steps:
|
||||
from_secret: registry_password
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
- tag
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: test build docker
|
||||
.PHONY: test build build-windows docker
|
||||
|
||||
test:
|
||||
go test ./...
|
||||
@@ -7,4 +7,7 @@ build:
|
||||
go build -trimpath -ldflags="-s -w" -o dist/syncdb ./cmd/syncdb
|
||||
|
||||
docker:
|
||||
docker build -t syncdb:dev .
|
||||
docker build -t syncdb:dev .
|
||||
|
||||
build-windows:
|
||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o dist/syncdb.exe ./cmd/syncdb
|
||||
|
||||
@@ -6,8 +6,48 @@ syncdb 使用 Go 提供 REST API、同步引擎和 Web 服务,前端构建产
|
||||
|
||||
详细设计见 [docs/design.md](docs/design.md),交互原型见 [docs/prototype.html](docs/prototype.html)。
|
||||
|
||||
```bash
|
||||
syncdb --config config.yaml
|
||||
## Windows 启动
|
||||
|
||||
开发阶段可直接双击项目根目录的 `build-and-run.cmd`,或在 PowerShell 中运行:
|
||||
|
||||
```powershell
|
||||
.\build-and-run.cmd
|
||||
```
|
||||
|
||||
Drone CI 负责前端构建、Go 测试、跨平台编译、制品打包和 Docker 镜像发布。
|
||||
脚本需要 Go 1.23 或更高版本,自动切换到项目目录,将包含前端资源的程序编译到 `dist/syncdb.exe`,编译成功后立即启动并自动打开浏览器。编译或启动失败会保留窗口显示错误;不会在编译失败时启动旧程序。重新打包前请先停止已运行的实例,脚本不会自动终止其他进程。环境变量 `SYNCDB_ADDR`、`SYNCDB_OPEN_BROWSER` 会原样继承。
|
||||
|
||||
双击 `syncdb.exe` 即可启动服务,并自动使用默认浏览器打开 `http://127.0.0.1:8080/`。保留控制台窗口以维持服务运行;按 `Ctrl+C` 可停止服务。
|
||||
|
||||
在项目根目录使用 PowerShell 构建:
|
||||
|
||||
```powershell
|
||||
go build -trimpath -ldflags="-s -w" -o dist/syncdb.exe ./cmd/syncdb
|
||||
```
|
||||
|
||||
生成后双击 `dist/syncdb.exe`。前端已嵌入程序,不需要单独启动前端服务。
|
||||
|
||||
可通过环境变量 `SYNCDB_ADDR` 修改监听地址(默认 `:8080`);浏览器会打开实际监听端口。设置 `SYNCDB_OPEN_BROWSER=false` 可禁用自动打开浏览器。自动打开仅在 Windows 下生效,端口监听失败时不会打开浏览器;浏览器启动失败不影响服务,可手动访问控制台打印的地址。
|
||||
|
||||
Linux/macOS 可运行 `./syncdb`,然后手动打开浏览器。Linux/macOS 构建环境也可使用 `make build-windows` 生成 Windows amd64 可执行文件。
|
||||
|
||||
## Web 控制台
|
||||
|
||||
浏览器控制台直接调用同源 API,无需 Node.js、前端构建或外部 CDN。支持中文、英文、日文切换及窄屏布局。
|
||||
|
||||
- **同步概览**:当前数据源、任务、执行中记录和导出数量,以及最近执行记录。
|
||||
- **数据源**:添加 MySQL / MongoDB 元信息,搜索列表并调用连接测试接口。
|
||||
- **同步任务**:选择源与目标、同步模式、冲突策略和表 / 集合;查看详情并确认登记运行。
|
||||
- **执行记录**:查看状态、行数、起止时间及包含错误信息的原始记录详情。
|
||||
- **SQL 导出**:选择 MySQL 数据源登记导出;只有后端返回文件后才显示下载入口。
|
||||
- **Binlog / SQL 审计**:展示当前接口状态和审计记录;尚未配置的功能明确提示不可用。
|
||||
- **复制账号权限**:查看和复制后端提供的 SQL 模板,不在页面执行 SQL。局域网 HTTP 环境若禁止剪贴板访问,可手动选择复制。
|
||||
|
||||
列表支持搜索和手动刷新,表单提交期间防止重复点击,失败时保留输入并提示重试。
|
||||
|
||||
### 当前开发限制
|
||||
|
||||
配置和记录仅保存在内存中,重启后清空。数据库适配器、同步执行器、SQL 导出引擎与 Binlog 读取器尚未实现:连接测试返回未配置,登记运行仅创建 `RUNNING` 记录,导出仅创建 `PENDING` 记录,不会实际连接数据库、同步数据或生成文件。界面不会将这些操作显示为实际执行成功,也不提供后端尚不支持的编辑、删除或取消操作。
|
||||
|
||||
默认监听 `:8080`,允许局域网访问;开发服务未提供身份认证,请仅在可信网络使用,不要向公网暴露。
|
||||
|
||||
Drone CI 负责 Go 检查、Linux / Windows 编译和 Docker 镜像发布,静态页面通过 `go:embed` 随程序发布。
|
||||
|
||||
+25
-2
@@ -7,10 +7,13 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
@@ -89,14 +92,34 @@ func main() {
|
||||
mux.HandleFunc("GET /api/replications/accounts", s.accountSQL)
|
||||
mux.Handle("/", http.FileServer(http.FS(mustSub(webFiles, "web"))))
|
||||
httpServer := &http.Server{Addr: env("SYNCDB_ADDR", ":8080"), Handler: logging(mux), ReadHeaderTimeout: 10 * time.Second}
|
||||
listener, err := net.Listen("tcp", httpServer.Addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
host, port, err := net.SplitHostPort(listener.Addr().String())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if address := net.ParseIP(host); address != nil && address.IsUnspecified() {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
browserURL := "http://" + net.JoinHostPort(host, port) + "/"
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer signal.Stop(stop)
|
||||
go func() {
|
||||
log.Printf("syncdb listening on %s", httpServer.Addr)
|
||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("syncdb listening on %s", browserURL)
|
||||
if err := httpServer.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
if runtime.GOOS == "windows" && env("SYNCDB_OPEN_BROWSER", "true") != "false" {
|
||||
go func() {
|
||||
if err := exec.Command("rundll32.exe", "url.dll,FileProtocolHandler", browserURL).Run(); err != nil {
|
||||
log.Printf("could not open browser: %v; open %s manually", err, browserURL)
|
||||
}
|
||||
}()
|
||||
}
|
||||
<-stop
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -1 +1,38 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>syncdb</title><style>body{font:16px system-ui;margin:0;background:#f5f7fb;color:#172033}main{max-width:900px;margin:80px auto;padding:32px;background:#fff;border-radius:16px;box-shadow:0 12px 34px #263b6812}h1{color:#3867ff}code{background:#edf1ff;padding:3px 6px;border-radius:4px}</style></head><body><main><h1>syncdb</h1><p>MySQL / MongoDB 数据同步服务</p><p>Go Web 服务已启动,前端资源已嵌入单个二进制文件。</p><p>健康检查:<a href="/api/health"><code>/api/health</code></a></p></main></body></html>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>syncdb</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
<script src="/app.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<aside class="sidebar">
|
||||
<a class="brand" href="#overview"><span class="brand-icon">⇄</span>syncdb<span class="dev">DEV</span></a>
|
||||
<div class="sidebar-caption" data-i18n="workspace"></div>
|
||||
<nav id="navigation"></nav>
|
||||
<div class="sidebar-footer"><span class="status-dot"></span><span data-i18n="singleBinary"></span></div>
|
||||
</aside>
|
||||
<main>
|
||||
<header class="topbar">
|
||||
<div><div class="eyebrow" data-i18n="workspace"></div><h1 id="page-title"></h1></div>
|
||||
<div class="toolbar">
|
||||
<span id="health" class="badge"></span>
|
||||
<select id="language" aria-label="Language"><option value="zh">简体中文</option><option value="en">English</option><option value="ja">日本語</option></select>
|
||||
<button id="refresh" type="button" data-i18n="refresh"></button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="notice" data-i18n="devNotice"></div>
|
||||
<div id="load-error" class="notice error" role="alert" hidden></div>
|
||||
<div class="section-toolbar"><p id="subtitle"></p><div id="page-actions"></div></div>
|
||||
<label class="search"><span data-i18n="search"></span><input id="search" type="search" autocomplete="off"></label>
|
||||
<div id="content" aria-live="polite"></div>
|
||||
<footer><span>syncdb</span><span id="updated"></span></footer>
|
||||
</main>
|
||||
</div>
|
||||
<dialog id="dialog" aria-labelledby="dialog-title"><form id="editor"><header><h2 id="dialog-title"></h2><button type="button" id="close-dialog" data-i18n="close"></button></header><div id="fields" class="form-grid"></div><p id="form-error" class="form-error" role="alert"></p><div class="dialog-actions"><button type="button" id="cancel-dialog" data-i18n="cancel"></button><button type="submit" class="primary" id="save" data-i18n="create"></button></div></form></dialog>
|
||||
<div id="toast" role="status" hidden></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user