mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 21:13:00 +08:00
196 lines
7.0 KiB
Go
196 lines
7.0 KiB
Go
package update
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/matthewlu070111/smart-srun/core/internal/domain"
|
|
)
|
|
|
|
// Inventory comes from the package database, not the router model or hostname.
|
|
type Inventory struct {
|
|
PackageManager string `json:"package_manager"`
|
|
Architecture string `json:"architecture"`
|
|
FirmwareFamily string `json:"firmware_family"`
|
|
DisplayVersion string `json:"display_version"`
|
|
Packages map[string]string `json:"packages"`
|
|
}
|
|
|
|
func (i Inventory) Mode() (string, error) {
|
|
_, core := i.Packages["smart-srun"]
|
|
_, luci := i.Packages["luci-app-smart-srun"]
|
|
_, bundle := i.Packages["luci-app-smart-srun-bundle"]
|
|
if (bundle && (core || luci)) || (!bundle && !core) {
|
|
return "", domain.Errorf(domain.CodePackageIncompatible, "安装包型不完整或互相冲突,请先修复包数据库")
|
|
}
|
|
if bundle {
|
|
return "bundle", nil
|
|
}
|
|
if luci {
|
|
if i.Packages["smart-srun"] != i.Packages["luci-app-smart-srun"] {
|
|
return "", domain.Errorf(domain.CodeRecoveryRequired, "核心与界面版本不一致,请先修复上次安装")
|
|
}
|
|
return "split", nil
|
|
}
|
|
return "core", nil
|
|
}
|
|
|
|
type Plan struct {
|
|
ID string `json:"plan_id"`
|
|
Release string `json:"release"`
|
|
SourceCommit string `json:"source_commit"`
|
|
InstallMode string `json:"install_mode"`
|
|
Inventory Inventory `json:"inventory"`
|
|
Assets []Asset `json:"assets"`
|
|
DownloadBytes int64 `json:"download_bytes"`
|
|
InstalledBytes int64 `json:"installed_bytes"`
|
|
}
|
|
|
|
func (i Inventory) Validate() error {
|
|
current, err := ParseVersion(i.DisplayVersion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := i.Mode(); err != nil {
|
|
return err
|
|
}
|
|
if (i.PackageManager != "opkg" && i.PackageManager != "apk") ||
|
|
!namePattern.MatchString(i.Architecture) || i.Architecture == "all" || i.Architecture == "noarch" || !familyPattern.MatchString(i.FirmwareFamily) {
|
|
return domain.Errorf(domain.CodePackageIncompatible, "无法确定本设备的包管理器、架构或固件系列")
|
|
}
|
|
for name, native := range i.Packages {
|
|
if name != "smart-srun" && name != "luci-app-smart-srun" && name != "luci-app-smart-srun-bundle" {
|
|
return domain.Errorf(domain.CodePackageIncompatible, "更新清单包含无关的已安装包")
|
|
}
|
|
prefix := current.NativeBase(i.PackageManager) + "-r"
|
|
if !strings.HasPrefix(native, prefix) || !positiveNumber(strings.TrimPrefix(native, prefix)) {
|
|
return domain.Errorf(domain.CodePackageIncompatible, "运行版本与已安装包版本不一致,请先重启或修复服务")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// BuildPlan never changes package format, architecture, firmware family or
|
|
// bundle/split ownership as a side effect of an update. Stable installations
|
|
// stay on stable unless the caller explicitly selects the RC channel.
|
|
func BuildPlan(manifest Manifest, inventory Inventory, channel string) (Plan, error) {
|
|
if err := manifest.Validate(); err != nil {
|
|
return Plan{}, err
|
|
}
|
|
if err := inventory.Validate(); err != nil {
|
|
return Plan{}, err
|
|
}
|
|
current, err := ParseVersion(inventory.DisplayVersion)
|
|
if err != nil {
|
|
return Plan{}, err
|
|
}
|
|
target, _ := ParseVersion(manifest.Release) // already validated above
|
|
if target.Major != current.Major {
|
|
return Plan{}, domain.Errorf(domain.CodePackageIncompatible, "跨主版本更新需要单独确认配置兼容性")
|
|
}
|
|
if channel == "" {
|
|
channel = current.Channel()
|
|
}
|
|
if (channel != "stable" && channel != "rc") || (channel == "stable" && target.RC != 0) {
|
|
return Plan{}, domain.Errorf(domain.CodePackageIncompatible, "该版本不属于当前更新通道")
|
|
}
|
|
if target.Compare(current) <= 0 {
|
|
return Plan{}, domain.Errorf(domain.CodeNotFound, "没有更新的兼容版本")
|
|
}
|
|
mode, err := inventory.Mode()
|
|
if err != nil {
|
|
return Plan{}, err
|
|
}
|
|
kinds := []string{"core"}
|
|
if mode == "bundle" {
|
|
kinds = []string{"bundle"}
|
|
} else if mode == "split" {
|
|
kinds = append(kinds, "luci")
|
|
}
|
|
inventory.Packages = maps.Clone(inventory.Packages)
|
|
plan := Plan{Release: manifest.Release, SourceCommit: manifest.SourceCommit, InstallMode: mode, Inventory: inventory}
|
|
for _, kind := range kinds {
|
|
matches := compatibleAssets(manifest.Assets, inventory, kind)
|
|
if len(matches) != 1 {
|
|
return Plan{}, domain.Errorf(domain.CodePackageIncompatible, "未找到唯一且已完成基本验证的兼容安装包")
|
|
}
|
|
asset := matches[0]
|
|
asset.FirmwareCompat = slices.Clone(asset.FirmwareCompat)
|
|
plan.Assets = append(plan.Assets, asset)
|
|
plan.DownloadBytes += asset.Bytes
|
|
plan.InstalledBytes += asset.InstalledBytes
|
|
}
|
|
if len(plan.Assets) == 2 && plan.Assets[0].PackageVersion != plan.Assets[1].PackageVersion {
|
|
return Plan{}, domain.Errorf(domain.CodePackageIncompatible, "核心与界面安装包版本不匹配")
|
|
}
|
|
// LuCI + core together count against the same device payload limit.
|
|
// The message reads the constant rather than repeating it: the previous
|
|
// literal said 10 MiB and would have kept saying so after D80 raised it.
|
|
if plan.InstalledBytes > MaxPayloadBytes {
|
|
return Plan{}, domain.Errorf(domain.CodePackageIncompatible,
|
|
"安装后的总载荷超过 %d MiB 限制", MaxPayloadBytes>>20)
|
|
}
|
|
data, err := json.Marshal(plan)
|
|
if err != nil {
|
|
return Plan{}, domain.Errorf(domain.CodeInternal, "无法记录更新计划").Wrap(err)
|
|
}
|
|
digest := sha256.Sum256(data)
|
|
plan.ID = hex.EncodeToString(digest[:])
|
|
return plan, nil
|
|
}
|
|
|
|
// RecoveryAssets selects the exact currently installed native versions. A
|
|
// nearby release or a different package revision is not an adequate backup.
|
|
func RecoveryAssets(manifest Manifest, inventory Inventory) ([]Asset, error) {
|
|
if err := manifest.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
if manifest.Release != inventory.DisplayVersion {
|
|
return nil, invalidManifest()
|
|
}
|
|
mode, err := inventory.Mode()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
kinds := []string{"core"}
|
|
if mode == "bundle" {
|
|
kinds = []string{"bundle"}
|
|
} else if mode == "split" {
|
|
kinds = append(kinds, "luci")
|
|
}
|
|
var assets []Asset
|
|
for _, kind := range kinds {
|
|
matches := compatibleAssets(manifest.Assets, inventory, kind)
|
|
if len(matches) != 1 || matches[0].PackageVersion != inventory.Packages[matches[0].PackageName()] {
|
|
return nil, domain.Errorf(domain.CodePackageIncompatible, "找不到当前精确版本的恢复安装包")
|
|
}
|
|
asset := matches[0]
|
|
asset.FirmwareCompat = slices.Clone(asset.FirmwareCompat)
|
|
assets = append(assets, asset)
|
|
}
|
|
return assets, nil
|
|
}
|
|
|
|
func compatibleAssets(assets []Asset, inventory Inventory, kind string) []Asset {
|
|
var found []Asset
|
|
for _, asset := range assets {
|
|
arch := inventory.Architecture
|
|
if kind == "luci" {
|
|
arch = "all"
|
|
if inventory.PackageManager == "apk" {
|
|
arch = "noarch"
|
|
}
|
|
}
|
|
if asset.Kind == kind && asset.PackageManager == inventory.PackageManager &&
|
|
asset.OpenWrtArch == arch && slices.Contains(asset.FirmwareCompat, inventory.FirmwareFamily) &&
|
|
asset.Validation.Build && (kind == "luci" || asset.Validation.ELF) {
|
|
found = append(found, asset)
|
|
}
|
|
}
|
|
return found
|
|
}
|