fix(pages): 收紧部署包与 Agent 同步边界

完成 V2 Phase 0 安全与一致性前置:统一真实归档限额、流式拉取、候选裁剪、保留上传删除语义及 Pages 路由引用锁。
This commit is contained in:
deqiying
2026-07-19 16:42:45 +08:00
parent f386674464
commit 4e8ec23264
47 changed files with 4005 additions and 759 deletions
+49 -172
View File
@@ -6,7 +6,6 @@ package pagesarchive
import (
"archive/tar"
"archive/zip"
"bytes"
"compress/bzip2"
"compress/gzip"
"fmt"
@@ -19,8 +18,8 @@ import (
type archiveFile interface {
Name() string
Mode() os.FileMode
IsDir() bool
IsSymlink() bool
Size() uint64
Open() (io.ReadCloser, error)
}
@@ -29,12 +28,10 @@ type zipArchiveFile struct {
file *zip.File
}
func (z zipArchiveFile) Name() string { return z.file.Name }
func (z zipArchiveFile) IsDir() bool { return z.file.FileInfo().IsDir() }
func (z zipArchiveFile) IsSymlink() bool {
return z.file.Mode()&os.ModeSymlink != 0
}
func (z zipArchiveFile) Size() uint64 { return z.file.UncompressedSize64 }
func (z zipArchiveFile) Name() string { return z.file.Name }
func (z zipArchiveFile) Mode() os.FileMode { return z.file.Mode() }
func (z zipArchiveFile) IsDir() bool { return z.file.FileInfo().IsDir() }
func (z zipArchiveFile) Size() uint64 { return z.file.UncompressedSize64 }
func (z zipArchiveFile) Open() (io.ReadCloser, error) {
return z.file.Open()
}
@@ -43,65 +40,27 @@ type sevenZipArchiveFile struct {
file *sevenzip.File
}
func (z sevenZipArchiveFile) Name() string { return z.file.Name }
func (z sevenZipArchiveFile) IsDir() bool { return z.file.FileInfo().IsDir() }
func (z sevenZipArchiveFile) IsSymlink() bool {
return z.file.Mode()&os.ModeSymlink != 0
}
func (z sevenZipArchiveFile) Size() uint64 { return z.file.UncompressedSize }
func (z sevenZipArchiveFile) Name() string { return z.file.Name }
func (z sevenZipArchiveFile) Mode() os.FileMode { return z.file.Mode() }
func (z sevenZipArchiveFile) IsDir() bool { return z.file.FileInfo().IsDir() }
func (z sevenZipArchiveFile) Size() uint64 { return z.file.UncompressedSize }
func (z sevenZipArchiveFile) Open() (io.ReadCloser, error) {
return z.file.Open()
}
// listEntriesAt lists archive members from a random-access source.
// When materializeBodies is true, tar-family streams buffer regular-file bodies so Entry.Open works.
// When false (inspect path), tar bodies are discarded after reading headers; zip/7z only use central directory metadata.
func listEntriesAt(ra io.ReaderAt, size int64, format Format, materializeBodies bool) ([]Entry, error) {
// listRandomAccessEntriesAt lists zip/7z members without reading their bodies.
// Tar-family archives use the sequential streaming paths in inspect.go/extract.go.
func listRandomAccessEntriesAt(ra io.ReaderAt, size int64, format Format) ([]Entry, error) {
if size < 0 {
return nil, fmt.Errorf("invalid pages package size")
}
switch format {
case FormatZip:
return listZipEntriesAt(ra, size)
case FormatTar:
return listTarFamily(io.NewSectionReader(ra, 0, size), FormatTar, materializeBodies)
case FormatTarGz:
return listTarFamily(io.NewSectionReader(ra, 0, size), FormatTarGz, materializeBodies)
case FormatTarXz:
return listTarFamily(io.NewSectionReader(ra, 0, size), FormatTarXz, materializeBodies)
case FormatTarBz2:
return listTarFamily(io.NewSectionReader(ra, 0, size), FormatTarBz2, materializeBodies)
case FormatSevenZip:
return listSevenZipEntriesAt(ra, size)
default:
return nil, fmt.Errorf("unsupported pages package format: %s", format)
}
}
func listTarFamily(r io.Reader, format Format, materializeBodies bool) ([]Entry, error) {
switch format {
case FormatTar:
if materializeBodies {
return listTarEntries(r, true)
}
return listTarEntries(r, false)
case FormatTarGz:
gzReader, err := gzip.NewReader(r)
if err != nil {
return nil, fmt.Errorf("open gzip pages package: %w", err)
}
defer func() { _ = gzReader.Close() }()
return listTarEntries(gzReader, materializeBodies)
case FormatTarXz:
xzReader, err := xz.NewReader(r)
if err != nil {
return nil, fmt.Errorf("open xz pages package: %w", err)
}
return listTarEntries(xzReader, materializeBodies)
case FormatTarBz2:
return listTarEntries(bzip2.NewReader(r), materializeBodies)
default:
return nil, fmt.Errorf("unsupported tar family format: %s", format)
return nil, fmt.Errorf("unsupported random-access pages package format: %s", format)
}
}
@@ -109,10 +68,15 @@ func entriesFromArchiveFiles(files []archiveFile) []Entry {
entries := make([]Entry, 0, len(files))
for _, item := range files {
file := item
mode := file.Mode()
isDir := file.IsDir()
isSymlink := mode&os.ModeSymlink != 0
isSpecial := !isDir && !isSymlink && !mode.IsRegular()
entries = append(entries, Entry{
Name: file.Name(),
IsDir: file.IsDir(),
IsSymlink: file.IsSymlink(),
IsDir: isDir,
IsSymlink: isSymlink,
IsSpecial: isSpecial,
Size: file.Size(),
Open: file.Open,
})
@@ -144,132 +108,45 @@ func listSevenZipEntriesAt(ra io.ReaderAt, size int64) ([]Entry, error) {
return entriesFromArchiveFiles(files), nil
}
func listTarEntries(r io.Reader, materializeBodies bool) ([]Entry, error) {
tarReader := tar.NewReader(r)
type materialised struct {
header *tar.Header
body []byte
}
items := make([]materialised, 0)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
func openTarFamilyReader(r io.Reader, format Format) (*tar.Reader, func() error, error) {
switch format {
case FormatTar:
return tar.NewReader(r), func() error { return nil }, nil
case FormatTarGz:
gzReader, err := gzip.NewReader(r)
if err != nil {
return nil, fmt.Errorf("read tar pages package: %w", err)
return nil, nil, fmt.Errorf("open gzip pages package: %w", err)
}
item, skip, err := readTarHeader(tarReader, header, materializeBodies)
return tar.NewReader(gzReader), gzReader.Close, nil
case FormatTarXz:
xzReader, err := xz.NewReader(r)
if err != nil {
return nil, err
return nil, nil, fmt.Errorf("open xz pages package: %w", err)
}
if skip {
continue
}
items = append(items, item)
}
entries := make([]Entry, 0, len(items))
for _, item := range items {
entries = append(entries, tarEntryFromHeader(item.header, item.body, materializeBodies))
}
return entries, nil
}
func readTarHeader(tarReader *tar.Reader, header *tar.Header, materializeBodies bool) (item struct {
header *tar.Header
body []byte
}, skip bool, err error) {
switch header.Typeflag {
case tar.TypeDir, tar.TypeSymlink, tar.TypeLink:
return struct {
header *tar.Header
body []byte
}{header: header}, false, nil
case tar.TypeReg, tar.TypeRegA: //nolint:staticcheck // TypeRegA still appears in older archives
if !materializeBodies {
if err := discardTarBody(tarReader, header); err != nil {
return item, false, err
}
return struct {
header *tar.Header
body []byte
}{header: header}, false, nil
}
body, readErr := readTarBody(tarReader, header)
if readErr != nil {
return item, false, readErr
}
return struct {
header *tar.Header
body []byte
}{header: header, body: body}, false, nil
return tar.NewReader(xzReader), func() error { return nil }, nil
case FormatTarBz2:
return tar.NewReader(bzip2.NewReader(r)), func() error { return nil }, nil
default:
if header.Size > 0 {
if _, copyErr := io.CopyN(io.Discard, tarReader, header.Size); copyErr != nil {
return item, false, fmt.Errorf("skip tar entry %s: %w", header.Name, copyErr)
}
}
return item, true, nil
return nil, nil, fmt.Errorf("unsupported tar family format: %s", format)
}
}
func discardTarBody(tarReader *tar.Reader, header *tar.Header) error {
if header.Size <= 0 {
_, err := io.Copy(io.Discard, tarReader)
if err != nil {
return fmt.Errorf("discard tar entry %s: %w", header.Name, err)
}
return nil
}
if _, err := io.CopyN(io.Discard, tarReader, header.Size); err != nil {
return fmt.Errorf("discard tar entry %s: %w", header.Name, err)
}
return nil
}
func readTarBody(tarReader *tar.Reader, header *tar.Header) ([]byte, error) {
func entryFromTarHeader(header *tar.Header) Entry {
entry := Entry{Name: header.Name}
if header.Size > 0 {
body := make([]byte, header.Size)
if _, err := io.ReadFull(tarReader, body); err != nil {
return nil, fmt.Errorf("read tar entry %s: %w", header.Name, err)
}
return body, nil
entry.Size = uint64(header.Size) //nolint:gosec // archive/tar rejects negative sizes
}
body, err := io.ReadAll(tarReader)
if err != nil {
return nil, fmt.Errorf("read tar entry %s: %w", header.Name, err)
}
return body, nil
}
func tarEntryFromHeader(header *tar.Header, body []byte, materializeBodies bool) Entry {
size := header.Size
if materializeBodies && int64(len(body)) > size {
size = int64(len(body))
}
entry := Entry{
Name: header.Name,
IsDir: header.Typeflag == tar.TypeDir,
IsSymlink: header.Typeflag == tar.TypeSymlink || header.Typeflag == tar.TypeLink,
Size: uint64(size), //nolint:gosec // non-negative sizes
}
if entry.IsDir || entry.IsSymlink {
entry.Open = func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(nil)), nil
}
return entry
}
if materializeBodies {
bodyCopy := body
entry.Open = func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(bodyCopy)), nil
}
return entry
}
// Inspect path: body not retained; Open is unavailable.
entry.Open = func() (io.ReadCloser, error) {
return nil, fmt.Errorf("tar entry body not materialized: %s", header.Name)
switch header.Typeflag {
case tar.TypeReg, tar.TypeRegA: //nolint:staticcheck // TypeRegA appears in older archives
// Regular file.
case tar.TypeDir:
entry.IsDir = true
case tar.TypeSymlink:
entry.IsSymlink = true
case tar.TypeLink:
entry.IsHardlink = true
default:
entry.IsSpecial = true
}
return entry
}