Skip to content

Commit bd56822

Browse files
author
Developer
committed
feat: add optional mysqlbinlog_path field on PITR create page
Add a non-required mysqlbinlog path input field to the PITR wizard. If provided, the path is used directly; if left empty, the system auto-discovers mysqlbinlog from PATH, MySQL basedir, or common installation paths.
1 parent 2612004 commit bd56822

6 files changed

Lines changed: 61 additions & 32 deletions

File tree

internal/server/pitr/handler.go

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,12 @@ func NewHandler(
5555
// ---------- request / response types ----------
5656

5757
type startRequest struct {
58-
AgentID string `json:"agent_id"`
59-
TargetTable string `json:"target_table"`
60-
RecoveryTime string `json:"recovery_time"`
61-
Mode string `json:"mode"` // "preview" or "execute"
62-
MySQLDSN string `json:"mysql_dsn"`
58+
AgentID string `json:"agent_id"`
59+
TargetTable string `json:"target_table"`
60+
RecoveryTime string `json:"recovery_time"`
61+
Mode string `json:"mode"` // "preview" or "execute"
62+
MySQLDSN string `json:"mysql_dsn"`
63+
MySQLBinlogPath string `json:"mysqlbinlog_path,omitempty"`
6364
}
6465

6566
type startResponse struct {
@@ -252,13 +253,14 @@ func (h *Handler) Start(w http.ResponseWriter, r *http.Request) {
252253
}
253254

254255
op := &Operation{
255-
OrgID: agt.OrgID,
256-
AgentID: req.AgentID,
257-
TargetTable: req.TargetTable,
258-
RecoveryTime: recoveryTime,
259-
Mode: req.Mode,
260-
DSN: req.MySQLDSN,
261-
State: StatePreflight,
256+
OrgID: agt.OrgID,
257+
AgentID: req.AgentID,
258+
TargetTable: req.TargetTable,
259+
RecoveryTime: recoveryTime,
260+
Mode: req.Mode,
261+
DSN: req.MySQLDSN,
262+
MySQLBinlogPath: req.MySQLBinlogPath,
263+
State: StatePreflight,
262264
}
263265

264266
if err := h.opStore.Create(op); err != nil {
@@ -497,9 +499,19 @@ func (h *Handler) Progress(w http.ResponseWriter, r *http.Request) {
497499
})
498500
}
499501

500-
// findMySQLBinlog locates the mysqlbinlog binary by checking PATH first, then
501-
// querying MySQL's basedir, and finally checking common installation paths.
502-
func findMySQLBinlog(ctx context.Context, conn *connector.MySQLConnector) (string, error) {
502+
// findMySQLBinlog locates the mysqlbinlog binary. If mysqlbinlogPath is
503+
// non-empty it is validated and returned directly. Otherwise the function
504+
// checks PATH, MySQL basedir, and common installation paths.
505+
func findMySQLBinlog(mysqlbinlogPath string, ctx context.Context, conn *connector.MySQLConnector) (string, error) {
506+
// 0. Explicit path from user config — use it directly.
507+
if mysqlbinlogPath != "" {
508+
if fi, err := os.Stat(mysqlbinlogPath); err == nil && fi.Mode().IsRegular() {
509+
abs, _ := filepath.Abs(mysqlbinlogPath)
510+
return abs, nil
511+
}
512+
return "", fmt.Errorf("specified mysqlbinlog path %q does not exist", mysqlbinlogPath)
513+
}
514+
503515
// 1. Try PATH first.
504516
if path, err := exec.LookPath("mysqlbinlog"); err == nil {
505517
return path, nil
@@ -533,7 +545,7 @@ func findMySQLBinlog(ctx context.Context, conn *connector.MySQLConnector) (strin
533545
}
534546
}
535547

536-
return "", fmt.Errorf("mysqlbinlog not found in $PATH, MySQL basedir, or common installation paths; install mysql-client or add mysqlbinlog to your PATH")
548+
return "", fmt.Errorf("mysqlbinlog not found; install mysql-client or specify the path in the mysqlbinlog_path field")
537549
}
538550

539551
// parseBinlogRemote uses mysqlbinlog --read-from-remote-server to parse binlog
@@ -797,7 +809,7 @@ func (h *Handler) runOperation(op *Operation, operator string) {
797809
if len(paths) > 0 {
798810
if _, statErr := os.Stat(paths[0]); os.IsNotExist(statErr) {
799811
log.Printf("pitr: binlog files not accessible locally, trying mysqlbinlog remote for op %s", op.ID)
800-
mysqlbinlogPath, findErr := findMySQLBinlog(ctx, conn)
812+
mysqlbinlogPath, findErr := findMySQLBinlog(op.MySQLBinlogPath, ctx, conn)
801813
if findErr != nil {
802814
h.failOperation(op, "parse binlogs (remote): %v", findErr)
803815
return

internal/server/pitr/store.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,22 @@ import (
1111
// Operation represents a point-in-time recovery operation and its current
1212
// state within the workflow state machine.
1313
type Operation struct {
14-
ID string `json:"id"`
15-
OrgID string `json:"orgId"`
16-
AgentID string `json:"agentId"`
17-
TargetTable string `json:"targetTable"`
18-
RecoveryTime time.Time `json:"recoveryTime"`
19-
Mode string `json:"mode"` // "preview" or "execute"
20-
State OperationState `json:"state"`
21-
DSN string `json:"-"` // MySQL DSN, never exposed to frontend
22-
PreflightRes *PreflightResult `json:"preflightResult,omitempty"`
23-
ParseRes *ParseSummary `json:"parseResult,omitempty"`
24-
ExecRes *ExecSummary `json:"execResult,omitempty"`
25-
Progress *ProgressInfo `json:"progress,omitempty"`
26-
Error string `json:"error,omitempty"`
27-
CreatedAt time.Time `json:"createdAt"`
28-
UpdatedAt time.Time `json:"updatedAt"`
14+
ID string `json:"id"`
15+
OrgID string `json:"orgId"`
16+
AgentID string `json:"agentId"`
17+
TargetTable string `json:"targetTable"`
18+
RecoveryTime time.Time `json:"recoveryTime"`
19+
Mode string `json:"mode"` // "preview" or "execute"
20+
State OperationState `json:"state"`
21+
DSN string `json:"-"` // MySQL DSN, never exposed to frontend
22+
MySQLBinlogPath string `json:"-"` // Optional explicit mysqlbinlog path
23+
PreflightRes *PreflightResult `json:"preflightResult,omitempty"`
24+
ParseRes *ParseSummary `json:"parseResult,omitempty"`
25+
ExecRes *ExecSummary `json:"execResult,omitempty"`
26+
Progress *ProgressInfo `json:"progress,omitempty"`
27+
Error string `json:"error,omitempty"`
28+
CreatedAt time.Time `json:"createdAt"`
29+
UpdatedAt time.Time `json:"updatedAt"`
2930
}
3031

3132
// PreflightResult contains the output of the preflight check phase.

web/src/api/pitr.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export async function startPITR(data: {
77
recovery_time: string;
88
mode: 'preview' | 'execute';
99
mysql_dsn: string;
10+
mysqlbinlog_path?: string;
1011
}): Promise<{ operationId: string; status: string }> {
1112
const response = await apiClient.post<{ operationId: string; status: string }>('/pitr/start', data);
1213
return response.data;

web/src/locales/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@
167167
"mysqlDsn": "MySQL DSN",
168168
"mysqlDsnHelp": "Format: user:password@tcp(host:port)/database",
169169
"mysqlDsnPlaceholder": "user:password@tcp(127.0.0.1:3306)/database",
170+
"mysqlbinlogPath": "mysqlbinlog path (optional)",
171+
"mysqlbinlogPathHelp": "Leave empty to auto-discover from system PATH",
172+
"mysqlbinlogPathPlaceholder": "C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqlbinlog.exe",
170173
"targetTablePlaceholder": "e.g. mydb.orders",
171174
"calculating": "Calculating...",
172175
"updatedAt": "Updated At",

web/src/locales/zh.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,9 @@
167167
"mysqlDsn": "MySQL 连接串",
168168
"mysqlDsnHelp": "格式:user:password@tcp(host:port)/database",
169169
"mysqlDsnPlaceholder": "user:password@tcp(127.0.0.1:3306)/database",
170+
"mysqlbinlogPath": "mysqlbinlog 路径(可选)",
171+
"mysqlbinlogPathHelp": "留空则自动查找系统的 mysqlbinlog",
172+
"mysqlbinlogPathPlaceholder": "C:\\Program Files\\MySQL\\MySQL Server 8.0\\bin\\mysqlbinlog.exe",
170173
"targetTablePlaceholder": "例如 mydb.orders",
171174
"calculating": "计算中...",
172175
"updatedAt": "更新时间",

web/src/pages/pitr/PITRWizardPage.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export default function PITRWizardPage() {
5151
const [targetTable, setTargetTable] = useState('');
5252
const [recoveryTime, setRecoveryTime] = useState('');
5353
const [mysqlDsn, setMysqlDsn] = useState('');
54+
const [mysqlBinlogPath, setMysqlBinlogPath] = useState('');
5455
const [operationId, setOperationId] = useState<string | null>(null);
5556

5657
// Fetch agents list
@@ -99,6 +100,7 @@ export default function PITRWizardPage() {
99100
recovery_time: dayjs(recoveryTime).toISOString(),
100101
mode: 'execute',
101102
mysql_dsn: mysqlDsn,
103+
mysqlbinlog_path: mysqlBinlogPath || undefined,
102104
}),
103105
onSuccess: (data) => {
104106
setOperationId(data.operationId);
@@ -256,6 +258,13 @@ export default function PITRWizardPage() {
256258
onChange={(e) => setMysqlDsn(e.target.value)}
257259
/>
258260
</Form.Item>
261+
<Form.Item label={t('pitr.mysqlbinlogPath')} help={t('pitr.mysqlbinlogPathHelp')}>
262+
<Input
263+
placeholder={t('pitr.mysqlbinlogPathPlaceholder')}
264+
value={mysqlBinlogPath}
265+
onChange={(e) => setMysqlBinlogPath(e.target.value)}
266+
/>
267+
</Form.Item>
259268
</Form>
260269
);
261270

0 commit comments

Comments
 (0)