3f603aa293
需求:整库逻辑备份做定时,但要在「系统设置 → 存储与备份」里设置定时时间与储存天数。 后端 - migration 000045 backup_schedule(单行:enabled/schedule_time/retention_days/retention_months/ last_applied_*),store + 领域校验(HH:MM、1~365 天、0~24 月,非法值 400 且不落库不下发) - internal/backuphost:通过 ssh 在目标主机上读回/下发计划并立即备份(未接线时 Available=false, 页面降级提示;主机侧错误带输出回传)。最近一次只认有 MANIFEST.json 的完整目录,跳 *.incomplete - 接口 GET/PUT /api/v1/backup-schedule、GET …/host、POST …/apply、POST …/run; 写操作经 classifyRole 限管理员;run 为异步 202 + 在途单飞(http 写超时 30s,备份约 35s) - openapi.yaml 补齐 5 条路由(契约测试要求每条都写 400/500) 主机侧 - scripts/install_backup_schedule.sh(幂等):写 /etc/knowledge-governance/backup-schedule.env 与 /etc/cron.d/kb-logical-backup,参数校验,--show/--disable,最后一行 SCHEDULE_APPLIED 供平台核对 - logical-backup.sh 从该文件读保留策略;失败或被中断把目录改名 *.incomplete,不留'像成功'的半成品 - install --show 只回显计划行(原先把 SHELL=/PATH= 当计划显示) 前端 - web/src/backupSchedule.ts(严格解析:缺计划字段即报错,不把默认值伪装成设置) - BackupScheduleSection:容器 + 纯视图(管理员表单/只读、已一致/待下发、在途轮询) 运行前提(已落实):运行镜像加 openssh-client,且容器 uid 1000 在 /etc/passwd 有名字 (否则 ssh 报 No user exists for uid 1000);凭据只读挂载 secrets/backup-host(git 忽略) 实测:主机 cron 每天 03:30 / 保留 7 天 / 每月 3 份;立即备份一次 34 秒 / 742.7 MiB / 失败 0; Go 与前端测试全绿(170 vitest)。
73 lines
2.8 KiB
Go
73 lines
2.8 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"knowledge-governance/internal/governance"
|
|
)
|
|
|
|
// LoadBackupSchedule reads the single active backup schedule row. A missing row
|
|
// (fresh database before the seed, or a rolled-back migration) reads back as the
|
|
// defaults instead of an error, so the settings page never hard-fails.
|
|
func (s *Store) LoadBackupSchedule(ctx context.Context) (governance.BackupSchedule, error) {
|
|
schedule := governance.BackupSchedule{
|
|
ID: "active",
|
|
Enabled: false,
|
|
ScheduleTime: governance.BackupScheduleDefaultTime,
|
|
RetentionDays: 7,
|
|
RetentionMonths: 3,
|
|
}
|
|
err := s.db.QueryRow(ctx, `
|
|
SELECT enabled,schedule_time,retention_days,retention_months,last_applied_at,last_applied_summary,updated_by,updated_at
|
|
FROM backup_schedule WHERE id='active'`).
|
|
Scan(&schedule.Enabled, &schedule.ScheduleTime, &schedule.RetentionDays, &schedule.RetentionMonths,
|
|
&schedule.LastAppliedAt, &schedule.LastAppliedSummary, &schedule.UpdatedBy, &schedule.UpdatedAt)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return schedule, nil
|
|
}
|
|
if err != nil {
|
|
return governance.BackupSchedule{}, errors.New("load backup schedule")
|
|
}
|
|
return schedule, nil
|
|
}
|
|
|
|
// SaveBackupSchedule upserts the schedule row. Validation belongs to the caller
|
|
// (governance.BackupSchedule.Validate); this only persists what was accepted.
|
|
func (s *Store) SaveBackupSchedule(ctx context.Context, schedule governance.BackupSchedule, updatedBy string, now time.Time) error {
|
|
if now.IsZero() {
|
|
return errors.New("backup schedule save requires a timestamp")
|
|
}
|
|
if _, err := s.db.Exec(ctx, `
|
|
INSERT INTO backup_schedule(id,enabled,schedule_time,retention_days,retention_months,updated_by,updated_at)
|
|
VALUES('active',$1,$2,$3,$4,$5,$6)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
enabled=EXCLUDED.enabled,
|
|
schedule_time=EXCLUDED.schedule_time,
|
|
retention_days=EXCLUDED.retention_days,
|
|
retention_months=EXCLUDED.retention_months,
|
|
updated_by=EXCLUDED.updated_by,
|
|
updated_at=EXCLUDED.updated_at`,
|
|
schedule.Enabled, schedule.ScheduleTime, schedule.RetentionDays, schedule.RetentionMonths, updatedBy, now); err != nil {
|
|
return errors.New("save backup schedule")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MarkBackupScheduleApplied records the last successful push to the data-plane
|
|
// host (cron file + next run), which the page shows as "当前主机上的计划".
|
|
func (s *Store) MarkBackupScheduleApplied(ctx context.Context, summary string, appliedAt time.Time) error {
|
|
if appliedAt.IsZero() {
|
|
return errors.New("backup schedule apply requires a timestamp")
|
|
}
|
|
if _, err := s.db.Exec(ctx, `
|
|
UPDATE backup_schedule SET last_applied_at=$1, last_applied_summary=$2 WHERE id='active'`,
|
|
appliedAt, summary); err != nil {
|
|
return errors.New("mark backup schedule applied")
|
|
}
|
|
return nil
|
|
}
|