diff --git a/server/task_archive_package.go b/server/task_archive_package.go index a07631f..bfbee15 100644 --- a/server/task_archive_package.go +++ b/server/task_archive_package.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "io/fs" + "log" "os" "path/filepath" "strconv" @@ -577,7 +578,11 @@ func writeTaskArchivePackage(path, payloadDir string, snapshot *pgdb.TaskArchive return err } if info.Mode()&os.ModeSymlink != 0 { - return fmt.Errorf("task archive refuses symlink: %s", current) + // 归档格式端到端只支持普通文件与目录(解包端对其它类型直接报错), + // 无法还原符号链接。跳过而非整包失败:不读取链接目标(lstat,不越出目录树), + // 也不写入 symlink 条目;链接指向树内时目标文件本身仍会被单独遍历归档。 + log.Printf("[task-archive] 跳过符号链接(归档不支持,不影响其它文件):%s", current) + return nil } header, err := tar.FileInfoHeader(info, "") if err != nil { diff --git a/server/task_archive_package_test.go b/server/task_archive_package_test.go index 9846673..c149e86 100644 --- a/server/task_archive_package_test.go +++ b/server/task_archive_package_test.go @@ -94,6 +94,39 @@ func TestTaskArchivePackageFilesRoundTrip(t *testing.T) { } } +func TestTaskArchivePackageSkipsSymlink(t *testing.T) { + dataDir := t.TempDir() + payload := filepath.Join(dataDir, "payload") + if err := os.MkdirAll(payload, archiveDirMode); err != nil { + t.Fatal(err) + } + regular := filepath.Join(payload, "keep.txt") + if err := os.WriteFile(regular, []byte("keep me"), archiveFileMode); err != nil { + t.Fatal(err) + } + // 工作目录里出现的符号链接应被跳过,而不是让整个归档失败。 + if err := os.Symlink(regular, filepath.Join(payload, "link.txt")); err != nil { + t.Skipf("symlink unsupported on this platform: %v", err) + } + + archivePath := filepath.Join(dataDir, "archive.tar.zst") + snapshot := &pgdb.TaskArchiveSnapshot{FormatVersion: pgdb.TaskArchiveFormatVersion, TaskID: 42} + _, _, checksum, err := writeTaskArchivePackage(archivePath, payload, snapshot) + if err != nil { + t.Fatalf("archive should skip symlink, not fail: %v", err) + } + extracted := filepath.Join(dataDir, "restore") + if err := extractTaskArchivePackage(archivePath, checksum, extracted); err != nil { + t.Fatal(err) + } + if got, err := os.ReadFile(filepath.Join(extracted, "keep.txt")); err != nil || string(got) != "keep me" { + t.Fatalf("regular file not archived: got=%q err=%v", got, err) + } + if _, err := os.Lstat(filepath.Join(extracted, "link.txt")); !os.IsNotExist(err) { + t.Fatalf("symlink should have been skipped, but link.txt exists: %v", err) + } +} + func TestTaskArchiveStageJournalRollsBackInterruptedMoves(t *testing.T) { dataDir := t.TempDir() taskFile := filepath.Join(dataDir, "tasks", "42", "evidence.txt")