fix(task-archive): 遇到符号链接跳过而非整包失败

归档格式端到端只支持普通文件与目录(解包端对其它类型直接报错),原打包器遇到
任何 symlink 即整包失败(task archive refuses symlink),导致工作目录里一条软链
就卡死整个任务归档。改为跳过该符号链接并记日志:lstat 不跟随、不越出目录树、
不写 symlink 条目;链接指向树内时目标文件仍会被单独遍历归档。安全防护不变。

新增 TestTaskArchivePackageSkipsSymlink 覆盖。
This commit is contained in:
Autumn-27 committed 2026-09-19 11:08:15 -04:00
1 parent 33c26dc72a
commit 37950f4a6f
2 files changed
+39 -1

No files matched your search

+6 -1
View File
@@ -9,6 +9,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"log"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -577,7 +578,11 @@ func writeTaskArchivePackage(path, payloadDir string, snapshot *pgdb.TaskArchive
return err return err
} }
if info.Mode()&os.ModeSymlink != 0 { 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, "") header, err := tar.FileInfoHeader(info, "")
if err != nil { if err != nil {
+33
View File
@@ -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) { func TestTaskArchiveStageJournalRollsBackInterruptedMoves(t *testing.T) {
dataDir := t.TempDir() dataDir := t.TempDir()
taskFile := filepath.Join(dataDir, "tasks", "42", "evidence.txt") taskFile := filepath.Join(dataDir, "tasks", "42", "evidence.txt")