Help us shape the future of Go! Take the 2026 Go Developer Survey

pack

package
v0.27.0 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 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