mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-09-06 07:56:41 +08:00
feat(restore): 恢复到指定目录(文件类型本机恢复 + 确认弹窗输入) (#86)
* feat(restore): 支持恢复到指定目录(文件类型本机恢复)
恢复此前只能覆盖原始源路径。新增「恢复到指定目录」:把文件备份还原到任意目录,
用于测试恢复、迁移、并排恢复而不覆盖现网数据。
- backup.TaskSpec +RestoreTargetPath;FileRunner.Restore 非空时把归档解压到该目录。
- model.RestoreRecord +TargetPath(持久化/审计)。
- RestoreService.Start 增加 targetPath 参数与校验:仅文件类型、需绝对路径、
远程节点暂不支持(清晰报错);executeLocally 透传到 spec。
- 恢复触发端点接受可选请求体 {targetPath}(无 body 时恢复到原始路径)。
- 测试:恢复到指定目录后文件落在该目录;相对路径被拒。
* feat(restore): 恢复确认弹窗支持指定恢复目录
文件类型 + 本机恢复时,恢复确认弹窗新增「恢复到指定目录」输入(可选、绝对路径、
留空=原位置),并实时反映在「恢复目标」摘要中;经 startRestoreFromBackup 透传 targetPath。
This commit is contained in:
@@ -269,6 +269,61 @@ func TestRestoreServiceStart_RejectsCorruptedBackup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRestoreServiceStart_RestoresToAlternatePath 验证恢复到指定目录:归档落在指定目录,
|
||||
// 且相对路径被拒绝。
|
||||
func TestRestoreServiceStart_RestoresToAlternatePath(t *testing.T) {
|
||||
h := newRestoreTestHarness(t, false)
|
||||
ctx := context.Background()
|
||||
|
||||
backupDetail, err := h.execution.RunTaskByIDSync(ctx, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("RunTaskByIDSync: %v", err)
|
||||
}
|
||||
if backupDetail.Status != "success" {
|
||||
t.Fatalf("expected backup success, got %s", backupDetail.Status)
|
||||
}
|
||||
|
||||
altDir := filepath.Join(t.TempDir(), "restore-here")
|
||||
|
||||
// 相对路径应被拒绝(且不创建恢复记录)。
|
||||
if _, relErr := h.service.StartSelective(ctx, backupDetail.ID, nil, "relative/path", "tester"); relErr == nil {
|
||||
t.Fatal("relative target path should be rejected")
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
h.service.async = func(job func()) {
|
||||
go func() {
|
||||
job()
|
||||
close(done)
|
||||
}()
|
||||
}
|
||||
detail, err := h.service.StartSelective(ctx, backupDetail.ID, nil, altDir, "tester")
|
||||
if err != nil {
|
||||
t.Fatalf("StartSelective(altDir): %v", err)
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(15 * time.Second):
|
||||
t.Fatal("restore did not complete in time")
|
||||
}
|
||||
|
||||
final, err := h.service.Get(ctx, detail.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("Get: %v", err)
|
||||
}
|
||||
if final.Status != model.RestoreRecordStatusSuccess {
|
||||
t.Fatalf("expected success, got %s (err=%s)", final.Status, final.ErrorMessage)
|
||||
}
|
||||
// 源目录 basename 为 "source",归档解压到 altDir/source/index.html。
|
||||
got, err := os.ReadFile(filepath.Join(altDir, "source", "index.html"))
|
||||
if err != nil {
|
||||
t.Fatalf("read restored file at alternate path: %v", err)
|
||||
}
|
||||
if string(got) != "hello-restore" {
|
||||
t.Fatalf("unexpected restored content at alt path: %q", string(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreServiceStart_RemoteNodeEnqueuesCommand(t *testing.T) {
|
||||
h := newRestoreTestHarness(t, true)
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in New Issue
Block a user