refactor: use nginx -s reload instead of systemd
- doc: add comment
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
// Package updatepackage reads the exact repack ZIP format currently emitted by deploy.
|
||||
// Package updatepackage 负责解析 deploy 当前产出的重打包 ZIP 更新包格式,
|
||||
// 并从中提取、校验原生后端工件。包内同时支持两种来源:通过
|
||||
// artifact-selection.json 清单声明的后端原生工件(backend_native.go),以及
|
||||
// 由 Jenkins 或运维人员直接提供的 JAR 文件(direct_native.go)。包边界仅限
|
||||
// 更新包的解析、结构校验与工件落地,不涉及具体部署流程。
|
||||
package updatepackage
|
||||
|
||||
import (
|
||||
@@ -19,27 +23,46 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
manifestName = "artifact-selection.json"
|
||||
backendArtifactKind = "BACKEND"
|
||||
nativeArtifactType = "native"
|
||||
// manifestName 更新包根目录下描述工件选择结果的清单文件名。
|
||||
manifestName = "artifact-selection.json"
|
||||
// backendArtifactKind 表示清单中后端工件的类别,取值须为 "BACKEND"。
|
||||
backendArtifactKind = "BACKEND"
|
||||
// nativeArtifactType 表示原生工件类型,type 与 selectedType 均须为 "native"。
|
||||
nativeArtifactType = "native"
|
||||
// maximumManifestBytes 清单文件允许的最大未压缩字节数,用于防止恶意超大清单。
|
||||
maximumManifestBytes = 1 << 20
|
||||
)
|
||||
|
||||
// BackendNativePackage is an opened repack ZIP containing one exact native backend artifact.
|
||||
// BackendNativePackage 一个已打开的、恰好包含一个原生后端工件的重打包 ZIP 更新包。
|
||||
// 它封装了底层 ZIP 句柄与被选中工件对应的条目,并通过导出字段暴露解析出的元数据。
|
||||
// 不变量:该包在被选中后始终只对应一个明确的原生后端工件;Close 之后不得再使用。
|
||||
type BackendNativePackage struct {
|
||||
archive *zip.ReadCloser
|
||||
// archive 底层打开的 ZIP 句柄,Close 时释放。
|
||||
archive *zip.ReadCloser
|
||||
// artifactEntry 被选中原生后端工件在 ZIP 中对应的条目。
|
||||
artifactEntry *zip.File
|
||||
|
||||
PackagePath string
|
||||
// PackagePath 更新包文件的绝对路径。
|
||||
PackagePath string
|
||||
// PackageSHA256 整个更新包文件的 SHA-256 摘要(十六进制)。
|
||||
PackageSHA256 string
|
||||
CustomerCode string
|
||||
VersionID string
|
||||
ArtifactID int64
|
||||
FileName string
|
||||
Identity filestore.Identity
|
||||
// CustomerCode 从清单中解析出的客户编码。
|
||||
CustomerCode string
|
||||
// VersionID 从清单中解析出的版本标识。
|
||||
VersionID string
|
||||
// ArtifactID 被选中工件在清单中声明的 ID。
|
||||
ArtifactID int64
|
||||
// FileName 被选中工件在 ZIP 根目录下的文件名。
|
||||
FileName string
|
||||
// Identity 被选中工件的不可变身份(未压缩大小与 SHA-256)。
|
||||
Identity filestore.Identity
|
||||
}
|
||||
|
||||
// OpenBackendNative validates the ZIP structure and selects one explicitly declared native backend artifact.
|
||||
// OpenBackendNative 校验更新包文件路径、计算包摘要并解析 ZIP 结构,
|
||||
// 从 artifact-selection.json 清单中选出恰好一个显式声明的原生后端工件,
|
||||
// 返回已打开的 BackendNativePackage。
|
||||
// packagePath 必须是干净绝对路径且为直接普通文件;校验失败时返回错误。
|
||||
// 成功返回后,调用方须通过 Close 释放底层 ZIP 句柄。
|
||||
func OpenBackendNative(packagePath string) (*BackendNativePackage, error) {
|
||||
if err := validateAbsoluteRegularFile(packagePath, "update package"); err != nil {
|
||||
return nil, err
|
||||
@@ -104,7 +127,8 @@ func OpenBackendNative(packagePath string) (*BackendNativePackage, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close releases the opened ZIP file.
|
||||
// Close 释放底层 ZIP 文件句柄并清空内部条目引用。对 nil 接收者或已关闭的包,
|
||||
// 重复调用幂等返回 nil。关闭后包不再可用。
|
||||
func (p *BackendNativePackage) Close() error {
|
||||
if p == nil || p.archive == nil {
|
||||
return nil
|
||||
@@ -115,7 +139,11 @@ func (p *BackendNativePackage) Close() error {
|
||||
return archive.Close()
|
||||
}
|
||||
|
||||
// ExtractArtifact writes the selected JAR to destination and verifies size and SHA-256 before publishing it.
|
||||
// ExtractArtifact 将被选中的原生后端 JAR 写入 destination,并在发布前校验
|
||||
// 未压缩大小与 SHA-256 摘要,两者均匹配后才原子重命名到位。
|
||||
// destination 必须是干净绝对路径;其父目录会被创建且须为直接目录(非符号链接)。
|
||||
// 过程先写入临时文件、同步后重命名,再同步父目录;任何一步失败都会清理临时文件,
|
||||
// 保证 destination 不会残留半成品。
|
||||
func (p *BackendNativePackage) ExtractArtifact(destination string) error {
|
||||
if p == nil || p.archive == nil || p.artifactEntry == nil {
|
||||
return errors.New("native backend update package is not open")
|
||||
@@ -181,30 +209,55 @@ func (p *BackendNativePackage) ExtractArtifact(destination string) error {
|
||||
return syncDirectory(parent)
|
||||
}
|
||||
|
||||
// artifactSelectionManifest artifact-selection.json 清单的结构化表示。
|
||||
// 字段标签严格对应 deploy 产出的 JSON 键名,不允许未知字段。
|
||||
type artifactSelectionManifest struct {
|
||||
CustomerCode string `json:"customerCode"`
|
||||
CustomerDisplayName *string `json:"customerDisplayName"`
|
||||
VersionID string `json:"versionId"`
|
||||
Items []string `json:"items"`
|
||||
BackendArtifacts []manifestArtifact `json:"backendArtifacts"`
|
||||
FrontendArtifacts []manifestArtifact `json:"frontendArtifacts"`
|
||||
NodeSSRArtifacts []manifestArtifact `json:"nodeSsrArtifacts"`
|
||||
Remark *string `json:"remark"`
|
||||
// CustomerCode 客户编码,解析后不得为空。
|
||||
CustomerCode string `json:"customerCode"`
|
||||
// CustomerDisplayName 客户展示名,允许为空。
|
||||
CustomerDisplayName *string `json:"customerDisplayName"`
|
||||
// VersionID 版本标识,解析后不得为空。
|
||||
VersionID string `json:"versionId"`
|
||||
// Items 随包附带的脚本列表。
|
||||
Items []string `json:"items"`
|
||||
// BackendArtifacts 后端工件列表,其中须恰好一个被选为原生后端。
|
||||
BackendArtifacts []manifestArtifact `json:"backendArtifacts"`
|
||||
// FrontendArtifacts 前端工件列表,本包解析但不使用。
|
||||
FrontendArtifacts []manifestArtifact `json:"frontendArtifacts"`
|
||||
// NodeSSRArtifacts Node SSR 工件列表,本包解析但不使用。
|
||||
NodeSSRArtifacts []manifestArtifact `json:"nodeSsrArtifacts"`
|
||||
// Remark 备注信息,允许为空。
|
||||
Remark *string `json:"remark"`
|
||||
}
|
||||
|
||||
// manifestArtifact 清单中单个工件的结构化表示。
|
||||
type manifestArtifact struct {
|
||||
ID int64 `json:"id"`
|
||||
VersionCode *string `json:"versionCode"`
|
||||
ArtifactKind string `json:"artifactKind"`
|
||||
Type string `json:"type"`
|
||||
SelectedType string `json:"selectedType"`
|
||||
Platform *string `json:"platform"`
|
||||
FileName string `json:"fileName"`
|
||||
FilePath *string `json:"filePath"`
|
||||
SHA256 string `json:"sha256"`
|
||||
ImageRef *string `json:"imageRef"`
|
||||
// ID 工件唯一标识,被选中的原生后端须大于 0。
|
||||
ID int64 `json:"id"`
|
||||
// VersionCode 工件版本编码,允许为空。
|
||||
VersionCode *string `json:"versionCode"`
|
||||
// ArtifactKind 工件类别,后端工件须为 "BACKEND"。
|
||||
ArtifactKind string `json:"artifactKind"`
|
||||
// Type 工件类型,原生工件须为 "native"。
|
||||
Type string `json:"type"`
|
||||
// SelectedType 被选中的类型,原生工件须为 "native"。
|
||||
SelectedType string `json:"selectedType"`
|
||||
// Platform 目标平台,允许为空。
|
||||
Platform *string `json:"platform"`
|
||||
// FileName 工件在 ZIP 根目录下的文件名。
|
||||
FileName string `json:"fileName"`
|
||||
// FilePath 工件在归档中的路径,允许为空。
|
||||
FilePath *string `json:"filePath"`
|
||||
// SHA256 工件的十六进制 SHA-256 摘要。
|
||||
SHA256 string `json:"sha256"`
|
||||
// ImageRef 镜像引用,允许为空。
|
||||
ImageRef *string `json:"imageRef"`
|
||||
}
|
||||
|
||||
// decodeManifest 读取并解析 ZIP 中名为 artifact-selection.json 的清单条目。
|
||||
// entry 必须是普通文件且不超过 maximumManifestBytes;解析前先做精确键名校验,
|
||||
// 再禁用未知字段进行解码,最后校验 customerCode 与 versionId 均非空。
|
||||
// 任何一步失败都会返回描述性的错误。
|
||||
func decodeManifest(entry *zip.File) (artifactSelectionManifest, error) {
|
||||
if entry.FileInfo().IsDir() {
|
||||
return artifactSelectionManifest{}, errors.New("artifact-selection.json is not a file")
|
||||
@@ -244,6 +297,8 @@ func decodeManifest(entry *zip.File) (artifactSelectionManifest, error) {
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// rootManifestKeys 清单根对象允许出现的所有 JSON 键名集合,
|
||||
// 用于精确校验,拒绝任何未知键(包括大小写不匹配的键)。
|
||||
var rootManifestKeys = map[string]struct{}{
|
||||
"customerCode": {},
|
||||
"customerDisplayName": {},
|
||||
@@ -255,6 +310,7 @@ var rootManifestKeys = map[string]struct{}{
|
||||
"remark": {},
|
||||
}
|
||||
|
||||
// artifactManifestKeys 单个工件对象允许出现的所有 JSON 键名集合。
|
||||
var artifactManifestKeys = map[string]struct{}{
|
||||
"id": {},
|
||||
"versionCode": {},
|
||||
@@ -268,6 +324,9 @@ var artifactManifestKeys = map[string]struct{}{
|
||||
"imageRef": {},
|
||||
}
|
||||
|
||||
// validateExactManifestJSON 使用 json.Decoder 对清单文档做递归精确校验:
|
||||
// 根对象与各工件对象的键名必须落在预定义白名单内,且不允许重复键与未知键。
|
||||
// document 必须是单一 JSON 值,其后不得有额外内容。
|
||||
func validateExactManifestJSON(document []byte) error {
|
||||
decoder := json.NewDecoder(bytes.NewReader(document))
|
||||
decoder.UseNumber()
|
||||
@@ -277,6 +336,10 @@ func validateExactManifestJSON(document []byte) error {
|
||||
return ensureJSONEnd(decoder)
|
||||
}
|
||||
|
||||
// validateObject 校验下一个 JSON 值为对象:确认分隔符为 '{',逐键检查重复与
|
||||
// 未知字段;root 为 true 时,对三个工件列表字段递归调用 validateArtifactArray,
|
||||
// 其余字段则通过 consumeJSONValue 仅做语法与重复键校验。objectPath 用于构造
|
||||
// 错误信息中的字段路径。
|
||||
func validateObject(decoder *json.Decoder, objectPath string, allowed map[string]struct{}, root bool) error {
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
@@ -320,6 +383,9 @@ func validateObject(decoder *json.Decoder, objectPath string, allowed map[string
|
||||
return err
|
||||
}
|
||||
|
||||
// validateArtifactArray 校验下一个 JSON 值为工件数组:确认分隔符为 '[',
|
||||
// 并逐个元素调用 validateObject 按 artifactManifestKeys 白名单精确校验。
|
||||
// arrayPath 用于构造错误信息中的数组路径,形如 "backendArtifacts[0]"。
|
||||
func validateArtifactArray(decoder *json.Decoder, arrayPath string) error {
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
@@ -339,6 +405,9 @@ func validateArtifactArray(decoder *json.Decoder, arrayPath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// consumeJSONValue 校验一个非工件字段的 JSON 值:若是标量直接通过;
|
||||
// 若是数组或对象则递归检查其内部对象键不重复,以保证后续 json.Decoder 严格解码兼容。
|
||||
// valuePath 用于构造错误信息中的字段路径。
|
||||
func consumeJSONValue(decoder *json.Decoder, valuePath string) error {
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
@@ -380,6 +449,8 @@ func consumeJSONValue(decoder *json.Decoder, valuePath string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// displayJSONPath 将 JSON 字段路径转为错误信息中可读的展示形式;
|
||||
// 空路径表示清单文档本身,返回固定的文档名。
|
||||
func displayJSONPath(value string) string {
|
||||
if value == "" {
|
||||
return "artifact-selection.json"
|
||||
@@ -387,6 +458,8 @@ func displayJSONPath(value string) string {
|
||||
return value
|
||||
}
|
||||
|
||||
// ensureJSONEnd 确认 decoder 流中除已解析的 JSON 值外再无内容;
|
||||
// 若还有额外值则报多 JSON 值错误,其余解码错误原样返回。
|
||||
func ensureJSONEnd(decoder *json.Decoder) error {
|
||||
var trailing any
|
||||
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
|
||||
@@ -398,6 +471,9 @@ func ensureJSONEnd(decoder *json.Decoder) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// selectNativeBackend 从后端工件列表中选出唯一的原生后端工件:
|
||||
// 要求 ArtifactKind、Type、SelectedType 分别为 "BACKEND"、"native"、"native",
|
||||
// 且这样的工件恰好只有一个,否则返回错误。选中后校验 id、fileName、sha256 均非空。
|
||||
func selectNativeBackend(artifacts []manifestArtifact) (manifestArtifact, error) {
|
||||
var selected manifestArtifact
|
||||
count := 0
|
||||
@@ -416,6 +492,9 @@ func selectNativeBackend(artifacts []manifestArtifact) (manifestArtifact, error)
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
// validateEntries 遍历 ZIP 条目并校验安全性:条目名非空、不使用反斜杠分隔、
|
||||
// 非绝对路径、路径已清洗、非 "."、不以 "../" 开头、非符号链接且不重复,
|
||||
// 校验通过后返回以条目名为键的映射,便于后续按根文件名定位工件。
|
||||
func validateEntries(files []*zip.File) (map[string]*zip.File, error) {
|
||||
entries := make(map[string]*zip.File, len(files))
|
||||
for _, file := range files {
|
||||
@@ -434,6 +513,8 @@ func validateEntries(files []*zip.File) (map[string]*zip.File, error) {
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// validateRootFileName 校验工件文件名恰好是 ZIP 根目录下的单一文件名:
|
||||
// 非空、不含路径分隔符、已清洗、非 "." 或 ".."。name 即清单中声明的 fileName。
|
||||
func validateRootFileName(name string) error {
|
||||
if name == "" || strings.ContainsAny(name, "/\\") || path.Clean(name) != name || name == "." || name == ".." {
|
||||
return fmt.Errorf("expected one exact ZIP root file name, got %q", name)
|
||||
@@ -441,6 +522,9 @@ func validateRootFileName(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAbsoluteRegularFile 校验 filePath 为干净绝对路径且经 Lstat 确认是
|
||||
// 直接普通文件(非符号链接、非目录等)。description 用于描述被校验文件用途,
|
||||
// 以构造可读的错误信息。校验通过返回 nil。
|
||||
func validateAbsoluteRegularFile(filePath string, description string) error {
|
||||
if !filepath.IsAbs(filePath) || filepath.Clean(filePath) != filePath {
|
||||
return fmt.Errorf("%s path must be a clean absolute path", description)
|
||||
@@ -455,6 +539,8 @@ func validateAbsoluteRegularFile(filePath string, description string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// hashFile 计算指定文件的 SHA-256 摘要并返回其十六进制字符串。
|
||||
// 读取与关闭过程中任一错误都会以包裹形式返回。
|
||||
func hashFile(filePath string) (string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
@@ -469,6 +555,8 @@ func hashFile(filePath string) (string, error) {
|
||||
return hex.EncodeToString(digest.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// syncDirectory 打开 directory 并调用 Sync 将目录项刷入磁盘,
|
||||
// 用于在原子重命名后确保新文件在目录中的落盘。打开、同步或关闭失败均返回错误。
|
||||
func syncDirectory(directory string) error {
|
||||
file, err := os.Open(directory)
|
||||
if err != nil {
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOpenAndExtractNativeBackend 验证正常流程:打开合法的原生后端更新包后,
|
||||
// 元数据与身份正确,且 ExtractArtifact 提取出的内容与原始 JAR 完全一致。
|
||||
func TestOpenAndExtractNativeBackend(t *testing.T) {
|
||||
jar := []byte("native backend JAR")
|
||||
packagePath := writeBackendPackage(t, jar, nil)
|
||||
@@ -38,6 +40,8 @@ func TestOpenAndExtractNativeBackend(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpenRejectsUnknownManifestField 验证清单中出现大小写错误的未知字段时,
|
||||
// OpenBackendNative 会返回包含该字段名的错误。
|
||||
func TestOpenRejectsUnknownManifestField(t *testing.T) {
|
||||
jar := []byte("native backend JAR")
|
||||
packagePath := writeBackendPackage(t, jar, func(manifest map[string]any) {
|
||||
@@ -49,6 +53,8 @@ func TestOpenRejectsUnknownManifestField(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpenRejectsMissingDeclaredArtifact 验证清单声明的 fileName 在 ZIP 中缺失时,
|
||||
// OpenBackendNative 会返回包含该文件名的错误。
|
||||
func TestOpenRejectsMissingDeclaredArtifact(t *testing.T) {
|
||||
jar := []byte("native backend JAR")
|
||||
packagePath := writePackageEntries(t, map[string][]byte{
|
||||
@@ -63,6 +69,8 @@ func TestOpenRejectsMissingDeclaredArtifact(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtractRejectsArtifactDigestMismatch 验证清单中的 sha256 与实际工件不符时,
|
||||
// ExtractArtifact 返回摘要不匹配错误,且不会在目标路径留下任何已发布文件。
|
||||
func TestExtractRejectsArtifactDigestMismatch(t *testing.T) {
|
||||
jar := []byte("native backend JAR")
|
||||
packagePath := writeBackendPackage(t, jar, func(manifest map[string]any) {
|
||||
@@ -83,6 +91,8 @@ func TestExtractRejectsArtifactDigestMismatch(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpenRejectsUnsafeAndDuplicateEntries 验证 OpenBackendNative 会拒绝包含
|
||||
// 不安全路径条目(如 "../" 前缀)的更新包,以及包含重复 ZIP 条目的更新包。
|
||||
func TestOpenRejectsUnsafeAndDuplicateEntries(t *testing.T) {
|
||||
jar := []byte("native backend JAR")
|
||||
manifest := manifestJSON(t, jar, nil)
|
||||
@@ -126,6 +136,8 @@ func TestOpenRejectsUnsafeAndDuplicateEntries(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// writeBackendPackage 构造一个包含清单与 backend.jar 的合法更新包,
|
||||
// 返回其临时文件路径。modify 可选,用于在写入前改写清单内容。
|
||||
func writeBackendPackage(t *testing.T, jar []byte, modify func(map[string]any)) string {
|
||||
t.Helper()
|
||||
return writePackageEntries(t, map[string][]byte{
|
||||
@@ -134,6 +146,8 @@ func writeBackendPackage(t *testing.T, jar []byte, modify func(map[string]any))
|
||||
})
|
||||
}
|
||||
|
||||
// manifestJSON 构造一份以 jar 摘要为 sha256 的完整清单 JSON,返回序列化字节。
|
||||
// modify 可选,用于在序列化前改写清单内容。
|
||||
func manifestJSON(t *testing.T, jar []byte, modify func(map[string]any)) []byte {
|
||||
t.Helper()
|
||||
digest := sha256.Sum256(jar)
|
||||
@@ -168,6 +182,8 @@ func manifestJSON(t *testing.T, jar []byte, modify func(map[string]any)) []byte
|
||||
return content
|
||||
}
|
||||
|
||||
// writePackageEntries 将给定名字到内容的映射写入一个临时 ZIP 文件,
|
||||
// 返回该文件路径。映射遍历顺序不影响结果。
|
||||
func writePackageEntries(t *testing.T, entries map[string][]byte) string {
|
||||
t.Helper()
|
||||
packagePath := filepath.Join(t.TempDir(), "package.zip")
|
||||
|
||||
@@ -14,17 +14,22 @@ import (
|
||||
"yms-daemon/internal/filestore"
|
||||
)
|
||||
|
||||
// DirectNativeJAR is one JAR supplied directly by Jenkins or an operator.
|
||||
// FileName is treated as opaque text; no version is parsed from it.
|
||||
// DirectNativeJAR 表示由 Jenkins 或运维人员直接提供的一个原生后端 JAR 文件。
|
||||
// FileName 被当作不透明文本处理,不会从中解析任何版本信息。
|
||||
type DirectNativeJAR struct {
|
||||
Path string
|
||||
SHA256 string
|
||||
// Path JAR 文件的绝对路径。
|
||||
Path string
|
||||
// SHA256 JAR 文件的十六进制 SHA-256 摘要。
|
||||
SHA256 string
|
||||
// FileName JAR 文件的基础文件名(不含目录)。
|
||||
FileName string
|
||||
// Identity JAR 文件的不可变身份(大小与 SHA-256),用于事务校验。
|
||||
Identity filestore.Identity
|
||||
}
|
||||
|
||||
// OpenDirectNativeJAR validates the direct file, verifies every ZIP entry and
|
||||
// records the immutable identity used by the transaction.
|
||||
// OpenDirectNativeJAR 校验直接提供的 JAR 文件:确认其为干净绝对路径下的直接
|
||||
// 普通文件、文件名以 ".jar" 结尾,并打开验证 ZIP 内所有条目可完整读取,
|
||||
// 最后记录事务所需的不可变身份。任一校验失败返回错误。
|
||||
func OpenDirectNativeJAR(jarPath string) (DirectNativeJAR, error) {
|
||||
if err := validateAbsoluteRegularFile(jarPath, "native backend JAR"); err != nil {
|
||||
return DirectNativeJAR{}, err
|
||||
@@ -48,8 +53,10 @@ func OpenDirectNativeJAR(jarPath string) (DirectNativeJAR, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CopyArtifact copies the exact JAR into transaction storage and rejects a
|
||||
// source file that changes after OpenDirectNativeJAR returns.
|
||||
// CopyArtifact 将 JAR 原样复制到事务存储的 destination,并拒绝在
|
||||
// OpenDirectNativeJAR 返回之后内容发生变化的源文件。destination 必须是干净
|
||||
// 绝对路径;复制过程先写临时文件,边复制边计算摘要,比对大小与 SHA-256 一致后
|
||||
// 对临时副本再次校验 ZIP,最后原子重命名并同步父目录。任一步失败均清理临时文件。
|
||||
func (j DirectNativeJAR) CopyArtifact(destination string) error {
|
||||
if !filepath.IsAbs(destination) || filepath.Clean(destination) != destination {
|
||||
return errors.New("native backend JAR destination must be a clean absolute path")
|
||||
@@ -111,6 +118,8 @@ func (j DirectNativeJAR) CopyArtifact(destination string) error {
|
||||
return syncDirectory(parent)
|
||||
}
|
||||
|
||||
// verifyJARArchive 打开 jarPath 对应的 ZIP 并逐条目完整读取(丢弃内容),
|
||||
// 确认归档结构有效且每个非目录条目可解压。空归档或任一读取失败均返回错误。
|
||||
func verifyJARArchive(jarPath string) error {
|
||||
archive, err := zip.OpenReader(jarPath)
|
||||
if err != nil {
|
||||
@@ -137,6 +146,8 @@ func verifyJARArchive(jarPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// identifyFile 计算指定文件的大小与 SHA-256 摘要,返回不可变身份 Identity。
|
||||
// 读取或关闭失败时返回错误。
|
||||
func identifyFile(filePath string) (filestore.Identity, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestOpenAndCopyDirectNativeJAR 验证正常流程:打开直接提供的 JAR 后元数据正确,
|
||||
// 且 CopyArtifact 复制出的内容与源文件完全一致。
|
||||
func TestOpenAndCopyDirectNativeJAR(t *testing.T) {
|
||||
jarPath := writeDirectNativeJAR(t, "glory-soft-yms.jar", []byte("backend classes"))
|
||||
jar, err := OpenDirectNativeJAR(jarPath)
|
||||
@@ -33,6 +35,8 @@ func TestOpenAndCopyDirectNativeJAR(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpenDirectNativeJARRejectsInvalidArchive 验证内容并非合法 ZIP 的 JAR 文件
|
||||
// 会被 OpenDirectNativeJAR 拒绝。
|
||||
func TestOpenDirectNativeJARRejectsInvalidArchive(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "glory-soft-yms.jar")
|
||||
if err := os.WriteFile(path, []byte("not a JAR"), 0o600); err != nil {
|
||||
@@ -43,6 +47,8 @@ func TestOpenDirectNativeJARRejectsInvalidArchive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCopyDirectNativeJARRejectsChangedSource 验证源 JAR 在 OpenDirectNativeJAR
|
||||
// 之后被替换时,CopyArtifact 会返回包含 "changed" 的错误。
|
||||
func TestCopyDirectNativeJARRejectsChangedSource(t *testing.T) {
|
||||
jarPath := writeDirectNativeJAR(t, "glory-soft-yms.jar", []byte("first"))
|
||||
jar, err := OpenDirectNativeJAR(jarPath)
|
||||
@@ -63,6 +69,8 @@ func TestCopyDirectNativeJARRejectsChangedSource(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// writeDirectNativeJAR 在临时目录创建名为 fileName 的 JAR,内含一个
|
||||
// BOOT-INF/classes/application.properties 条目,内容为 content,返回文件路径。
|
||||
func writeDirectNativeJAR(t *testing.T, fileName string, content []byte) string {
|
||||
t.Helper()
|
||||
jarPath := filepath.Join(t.TempDir(), fileName)
|
||||
|
||||
Reference in New Issue
Block a user