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 }