pack

package
v0.35.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 6, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package pack 提供跨宿主迁移用的资产包容器:zip 容器 + 一份清单(manifest)。

容器格式

一个迁移包是标准 zip 文件,包内固定含一份清单 manifest.json:

manifest.json   清单(格式版本、创建时间、各文件行数、各文件 SHA-256 摘要)
<其它条目>     数据文件,一行一条 JSON(JSONL)

选 zip 而不是自定义二进制格式,是因为迁移包要能被人在没装本程序的机器上打开看看里面是什么。 archive/zip 是标准库,但「容器 + 清单 + 每文件摘要 + 格式版本 + 体检」这一整套若每个要导出 资产的仓各写一遍,就会出现格式漂移;本包把这套约定固化下来。

写

w := pack.NewWriter(f, pack.CurrentFormatVersion)
n, err := w.AddJSONL("users.jsonl", rows)
m, err := w.Close() // 清单落盘并返回

清单里的 created_at 取自 Writer 的时刻来源,缺省 time.Now;需要确定性导出 (单测、逐字节可复现的包)时注入固定时钟:

w := pack.NewWriter(f, pack.CurrentFormatVersion, pack.WithClock(func() time.Time {
	return fixed
}))

读

arc, err := pack.Open(r, size)
err = arc.ReadJSONL("users.jsonl", func(row json.RawMessage) error { ... })

体检

m, problems, err := pack.Inspect(r, size)

Inspect 一次说清所有问题(缺哪几个文件、哪个摘要不符、哪个条数对不上、 哪个条数没核对上),而不是遇到第一个错误就停;err 只在两类「不可导」原因时返回,且可用 errors.Is 区分:

ErrCorrupt           包损坏:zip 读不开、清单缺失/损坏、清单 JSON 解析失败
ErrUnsupportedVersion 版本不认识:清单的 format_version 与调用方期望不符

条数核对覆盖两种键口径:先按文件名精确匹配清单 counts,再按「文件名去扩展名」 的基名宽松匹配(覆盖「键=类别名/基名」的生产者口径)。两种都匹配不上时,Inspect 会明确报出「条数未核对」而不是静默跳过这条检查——体检报「通过」意味着每条 检查都真的跑过了。

rows 支持范围

AddJSONL 接受「任意可 JSON 编码的行集合」,逐行编码后流式写入,不把整包读进内存:

[]any              元素任意可 JSON 编码(map/string/int/结构体指针……)
[]map[string]any   常见结构
[]struct{...}      结构体切片
[]T                任意元素类型的切片/数组

约束:rows 必须是切片或数组(元素零值也接受);不接受 nil、单个值、map、字节切片 ([]byte 与「行集合」语义冲突,直接报错而不是写出一行 base64)。

摘要

清单里的摘要是数据文件内容的 SHA-256 十六进制值,与 github.com/oekit/pkg/encoding 的 Sha256Hex 同算法同结果(流式场景用增量 hasher 累积,等价关系由契约测试锁定)。

文件名安全

写入与读取都做文件名净化:含路径分隔符、含 ".."、绝对路径(含 Windows 盘符)、 控制字符的条目名一律拒绝,读取时记入问题清单——防 zip slip 路径穿越。

Index

Constants

View Source
const CurrentFormatVersion = 1

CurrentFormatVersion 是当前写出的清单格式版本。

Variables

View Source
var (
	// ErrCorrupt 表示迁移包损坏:zip 读不开、清单缺失/损坏、清单 JSON 解析失败。
	// 用 errors.Is 判定,报错信息可直接展示给用户。
	ErrCorrupt = errors.New("pack: 迁移包损坏")
	// ErrUnsupportedVersion 表示清单的格式版本不被调用方识别。
	// 报错信息里带实际版本号与期望版本号。
	ErrUnsupportedVersion = errors.New("pack: 格式版本不兼容")
	// ErrFileNotFound 表示包内不存在指定文件。
	ErrFileNotFound = errors.New("pack: 包内不存在该文件")
)

Functions

func DigestHex

func DigestHex(data []byte) string

DigestHex 返回内容的 SHA-256 十六进制摘要。

与 github.com/oekit/pkg/encoding 的 Sha256Hex 同型;包内流式写入/读取时使用增量 hasher 累积,摘要值与该函数逐字节一致(契约测试 TestDigestMatchesEncoding 锁定)。

Types

type Archive

type Archive struct {
	// contains filtered or unexported fields
}

Archive 是已打开的迁移包。

func Open

func Open(r io.ReaderAt, size int64) (*Archive, error)

Open 打开迁移包并读取清单。

清单格式版本必须与 CurrentFormatVersion 一致,否则返回包装 ErrUnsupportedVersion 的错误; zip 读不开或清单缺失/损坏时返回包装 ErrCorrupt 的错误。

func OpenVersion

func OpenVersion(r io.ReaderAt, size int64, expected int) (*Archive, error)

OpenVersion 与 Open 相同,但显式指定调用方期望的清单格式版本。

func (*Archive) Manifest

func (a *Archive) Manifest() *Manifest

Manifest 返回包内清单。

func (*Archive) Names

func (a *Archive) Names() []string

Names 返回包内全部条目名(含清单),按字典序排列。

func (*Archive) ReadJSONL

func (a *Archive) ReadJSONL(name string, fn func(json.RawMessage) error) error

ReadJSONL 按名读回 JSONL 行,逐行回调。

回调收到的是该行的原始 JSON(json.RawMessage),调用方自行解码; 回调返回错误会中断读取并原样带回(errors.Is 仍可判定回调自己的哨兵错误)。 空行会被跳过。

func (*Archive) ReadJSONLInto

func (a *Archive) ReadJSONLInto(name string, dst any) error

ReadJSONLInto 按名读回 JSONL 行并解码进 dst 指向的切片(追加语义)。

dst 必须是 *[]T 形式(指向切片的非空指针),每行按元素类型解码后追加。

func (*Archive) Verify

func (a *Archive) Verify() []string

Verify 对已打开的包做体检,返回问题清单(与 Inspect 同一套规则)。

与 Inspect 不同的是:这里包已经打开成功,因此不会出现「包损坏 / 版本不兼容」两类错误, 只返回软问题清单(缺文件、摘要不符、条数不符、条数未核对、多余文件、非法文件名)。

type Manifest

type Manifest struct {
	// FormatVersion 是清单格式版本,写入方与读取方必须一致(当前为 CurrentFormatVersion)。
	FormatVersion int `json:"format_version"`
	// CreatedAt 是包的创建时间(UTC),取自 Writer 的时刻来源
	// (缺省 time.Now,可用 WithClock 注入)。
	CreatedAt time.Time `json:"created_at"`
	// Counts 记录每个数据文件的行数(文件名为键)。
	Counts map[string]int `json:"counts"`
	// Files 记录每个数据文件的 SHA-256 十六进制摘要(文件名为键)。
	Files map[string]string `json:"files"`
}

Manifest 描述迁移包的结构与内容。

func Inspect

func Inspect(r io.ReaderAt, size int64) (*Manifest, []string, error)

Inspect 对迁移包做体检。

一次说清所有问题而不是遇到第一个错误就停:problems 里会同时列出缺失文件、摘要不符、 条数不符、条数未核对、多余文件、非法文件名等;err 只在「包损坏」「版本不兼容」两类不可导原因时返回。

func InspectVersion

func InspectVersion(r io.ReaderAt, size int64, expected int) (*Manifest, []string, error)

InspectVersion 与 Inspect 相同,但显式指定调用方期望的清单格式版本。

版本一致时 err 为 nil(问题都落在 problems 里);版本不一致时 err 包装 ErrUnsupportedVersion, 此时清单仍会返回(便于把实际版本号展示给用户)。

type Option added in v0.17.0

type Option func(*Writer)

Option Writer 可选项。

func WithClock added in v0.17.0

func WithClock(now func() time.Time) Option

WithClock 注入清单 created_at 的时刻来源(缺省 time.Now)。

清单里的 created_at 落在包里(下游拿它判断「何时导出」「两个包哪个新」), 写死时刻会让导出逻辑没法做确定性单测、同一份数据反复导出不可复现—— 注入固定时钟即可得到逐字节可复现的清单。

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

Writer 逐个写入迁移包的数据文件。

Writer 不是并发安全的;一个包由一个 Writer 顺序写出。

func NewWriter

func NewWriter(w io.Writer, formatVersion int, opts ...Option) *Writer

NewWriter 创建迁移包写入器,底层数据写入 w。

formatVersion 是清单格式版本,应传 CurrentFormatVersion;版本会被写进清单, 读取方据此判断能否识别该包。

可选项 WithClock 注入清单 created_at 的时刻来源(缺省 time.Now)。

func (*Writer) AddJSONL

func (w *Writer) AddJSONL(name string, rows any) (int, error)

AddJSONL 把 rows 逐行编码成 JSONL 写入包内名为 name 的条目,返回写入行数。

行是逐条编码、逐条写出的(不把整个行集合攒进内存),适合大结果集导出。 写入成功后该文件的 SHA-256 摘要与行数会记入清单(Close 时落盘)。

name 必须是安全的普通文件名:含路径分隔符、含 ".."、绝对路径的会被拒绝。

func (*Writer) Close

func (w *Writer) Close() (*Manifest, error)

Close 写入清单并收尾 zip,返回最终清单。

清单以固定文件名 manifest.json 放在包内(自身不计入清单的文件列表)。 Close 只能调用一次,重复调用返回错误。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL