Files
Bridge-and-Join-s/internal/release/bundle.go
zuevav 17d8ec3988 Обновления из файла для закрытого контура: пакет с портала → проверка подписи → ядро, потом модули
- release: пакет обновления (ExtractBundle/ReadStagedManifest/WriteBundle), Artifact.MinCore,
  план блокирует модуль, которому нужно ядро новее, чем будет после установки
- bj-release: модули получают min_core = версия релиза
- настройки → Обновления: «Обновить из файла» (zip до 2 ГБ), фон как у сетевого обновления,
  причина блокировки в таблице компонентов

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
2026-09-18 11:27:23 +03:00

143 lines
5.0 KiB
Go

package release
// bundle.go — пакет обновления одним файлом для закрытого контура.
//
// Сервер, у которого нет выхода к серверу обновлений, получает обновление файлом: zip с
// подписанным манифестом и файлами компонентов, скачанный с портала клиента. Файл
// распаковывается в каталог подготовки и дальше идёт тем же путём, что и обновление по
// сети: проверка подписи и хешей, план по версиям, установка ядра, потом модулей.
import (
"archive/zip"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
)
// BundleManifestName — имя манифеста внутри пакета.
const BundleManifestName = "manifest.json"
// maxBundleFile — предел одного файла в пакете (защита от «zip-бомбы»).
const maxBundleFile = 2 << 30
// ExtractBundle — распаковать пакет в каталог подготовки. Принимаются только обычные файлы
// без каталогов в имени; манифест обязателен. Возвращает имена распакованных файлов.
func ExtractBundle(zipPath, dest string) ([]string, error) {
zr, err := zip.OpenReader(zipPath)
if err != nil {
return nil, fmt.Errorf("файл не является пакетом обновления (zip): %v", err)
}
defer zr.Close()
if err := os.MkdirAll(dest, 0o750); err != nil {
return nil, err
}
var names []string
seenManifest := false
for _, f := range zr.File {
if f.FileInfo().IsDir() {
continue
}
// Внутри может быть один каталог верхнего уровня (bj-update-…/) — берём только имя файла.
name := path.Base(strings.ReplaceAll(f.Name, "\\", "/"))
if name == "." || name == "/" || strings.HasPrefix(name, ".") || strings.ContainsAny(name, "/\\") {
continue
}
if f.UncompressedSize64 > maxBundleFile {
return nil, fmt.Errorf("файл %s в пакете слишком большой", name)
}
rc, err := f.Open()
if err != nil {
return nil, err
}
dst := filepath.Join(dest, name)
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o640)
if err != nil {
rc.Close()
return nil, err
}
_, err = io.Copy(out, io.LimitReader(rc, maxBundleFile+1))
rc.Close()
out.Close()
if err != nil {
return nil, err
}
if name == BundleManifestName {
seenManifest = true
}
names = append(names, name)
}
if !seenManifest {
return nil, errors.New("в пакете нет manifest.json — это не пакет обновления bj")
}
return names, nil
}
// ReadStagedManifest — подписанный манифест из каталога подготовки с проверкой подписи.
func ReadStagedManifest(dir string, pub []byte) (*SignedManifest, *Manifest, error) {
b, err := os.ReadFile(filepath.Join(dir, BundleManifestName))
if err != nil {
return nil, nil, err
}
var sm SignedManifest
if err := json.Unmarshal(b, &sm); err != nil {
return nil, nil, fmt.Errorf("манифест не разобран: %v", err)
}
m, err := Verify(&sm, pub)
if err != nil {
return nil, nil, fmt.Errorf("подпись манифеста не подтверждена: %v", err)
}
return &sm, m, nil
}
// WriteBundle — собрать пакет обновления: подписанный манифест и выбранные файлы релиза из
// каталога dir. include — какие файлы (Artifact.File) положить; пусто — все.
func WriteBundle(w io.Writer, dir string, include func(a Artifact) bool) error {
b, err := os.ReadFile(filepath.Join(dir, BundleManifestName))
if err != nil {
return err
}
var sm SignedManifest
if err := json.Unmarshal(b, &sm); err != nil {
return err
}
var m Manifest
if raw, err := base64.StdEncoding.DecodeString(sm.Payload); err == nil {
_ = json.Unmarshal(raw, &m)
}
zw := zip.NewWriter(w)
defer zw.Close()
fw, err := zw.CreateHeader(&zip.FileHeader{Name: BundleManifestName, Method: zip.Deflate})
if err != nil {
return err
}
if _, err := fw.Write(b); err != nil {
return err
}
for _, a := range m.Artifacts {
if include != nil && !include(a) {
continue
}
src, err := os.Open(filepath.Join(dir, a.File))
if err != nil {
return fmt.Errorf("файл %s релиза не найден: %v", a.File, err)
}
fw, err := zw.CreateHeader(&zip.FileHeader{Name: a.File, Method: zip.Store}) // бинари уже сжаты плохо — не тратим время
if err != nil {
src.Close()
return err
}
_, err = io.Copy(fw, src)
src.Close()
if err != nil {
return err
}
}
return nil
}