mirror of https://github.com/jetkvm/kvm.git
155 lines
5.5 KiB
Go
155 lines
5.5 KiB
Go
package ota
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
)
|
|
|
|
// to make the field names consistent with the RPCState struct
|
|
var componentFieldMap = map[string]string{
|
|
"app": "App",
|
|
"system": "System",
|
|
}
|
|
|
|
// RPCState represents the current OTA state for the RPC API
|
|
type RPCState struct {
|
|
Updating bool `json:"updating"`
|
|
Error string `json:"error,omitempty"`
|
|
MetadataFetchedAt *time.Time `json:"metadataFetchedAt,omitempty"`
|
|
AppUpdatePending bool `json:"appUpdatePending"`
|
|
SystemUpdatePending bool `json:"systemUpdatePending"`
|
|
AppDownloadProgress *float32 `json:"appDownloadProgress,omitempty"` //TODO: implement for progress bar
|
|
AppDownloadFinishedAt *time.Time `json:"appDownloadFinishedAt,omitempty"`
|
|
SystemDownloadProgress *float32 `json:"systemDownloadProgress,omitempty"` //TODO: implement for progress bar
|
|
SystemDownloadFinishedAt *time.Time `json:"systemDownloadFinishedAt,omitempty"`
|
|
AppVerificationProgress *float32 `json:"appVerificationProgress,omitempty"`
|
|
AppVerifiedAt *time.Time `json:"appVerifiedAt,omitempty"`
|
|
SystemVerificationProgress *float32 `json:"systemVerificationProgress,omitempty"`
|
|
SystemVerifiedAt *time.Time `json:"systemVerifiedAt,omitempty"`
|
|
AppUpdateProgress *float32 `json:"appUpdateProgress,omitempty"` //TODO: implement for progress bar
|
|
AppUpdatedAt *time.Time `json:"appUpdatedAt,omitempty"`
|
|
SystemUpdateProgress *float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement
|
|
SystemUpdatedAt *time.Time `json:"systemUpdatedAt,omitempty"`
|
|
}
|
|
|
|
// applyComponentStatusToRPCState uses reflection to map componentUpdateStatus fields to RPCState
|
|
func applyComponentStatusToRPCState(component string, status componentUpdateStatus, rpcState *RPCState) {
|
|
prefix := componentFieldMap[component]
|
|
if prefix == "" {
|
|
return
|
|
}
|
|
|
|
rpcVal := reflect.ValueOf(rpcState).Elem()
|
|
|
|
// it's really inefficient, but hey we do not need to use this often
|
|
// componentUpdateStatus is for internal use only, and all fields are unexported
|
|
for i := 0; i < rpcVal.NumField(); i++ {
|
|
rpcFieldName, hasPrefix := strings.CutPrefix(rpcVal.Type().Field(i).Name, prefix)
|
|
if !hasPrefix {
|
|
continue
|
|
}
|
|
|
|
switch rpcFieldName {
|
|
case "DownloadProgress":
|
|
rpcVal.Field(i).Set(reflect.ValueOf(&status.downloadProgress))
|
|
case "DownloadFinishedAt":
|
|
rpcVal.Field(i).Set(reflect.ValueOf(&status.downloadFinishedAt))
|
|
case "VerificationProgress":
|
|
rpcVal.Field(i).Set(reflect.ValueOf(&status.verificationProgress))
|
|
case "VerifiedAt":
|
|
rpcVal.Field(i).Set(reflect.ValueOf(&status.verifiedAt))
|
|
case "UpdateProgress":
|
|
rpcVal.Field(i).Set(reflect.ValueOf(&status.updateProgress))
|
|
case "UpdatedAt":
|
|
rpcVal.Field(i).Set(reflect.ValueOf(&status.updatedAt))
|
|
case "UpdatePending":
|
|
rpcVal.Field(i).SetBool(status.pending)
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// ToRPCState converts the State to the RPCState
|
|
func (s *State) ToRPCState() *RPCState {
|
|
r := &RPCState{
|
|
Updating: s.updating,
|
|
Error: s.error,
|
|
MetadataFetchedAt: &s.metadataFetchedAt,
|
|
}
|
|
|
|
for component, status := range s.componentUpdateStatuses {
|
|
applyComponentStatusToRPCState(component, status, r)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
func remoteMetadataToComponentStatus(
|
|
remoteMetadata *UpdateMetadata,
|
|
component string,
|
|
componentStatus *componentUpdateStatus,
|
|
params UpdateParams,
|
|
) error {
|
|
prefix := componentFieldMap[component]
|
|
if prefix == "" {
|
|
return fmt.Errorf("unknown component: %s", component)
|
|
}
|
|
|
|
remoteMetadataVal := reflect.ValueOf(remoteMetadata).Elem()
|
|
for i := 0; i < remoteMetadataVal.NumField(); i++ {
|
|
fieldName, hasPrefix := strings.CutPrefix(remoteMetadataVal.Type().Field(i).Name, prefix)
|
|
if !hasPrefix {
|
|
continue
|
|
}
|
|
|
|
switch fieldName {
|
|
case "URL":
|
|
componentStatus.url = remoteMetadataVal.Field(i).String()
|
|
case "Hash":
|
|
componentStatus.hash = remoteMetadataVal.Field(i).String()
|
|
case "Version":
|
|
componentStatus.version = remoteMetadataVal.Field(i).String()
|
|
default:
|
|
// fmt.Printf("unknown field %s", fieldName)
|
|
continue
|
|
}
|
|
}
|
|
|
|
localVersion, err := semver.NewVersion(componentStatus.localVersion)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing local version: %w", err)
|
|
}
|
|
|
|
remoteVersion, err := semver.NewVersion(componentStatus.version)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing remote version: %w", err)
|
|
}
|
|
componentStatus.available = remoteVersion.GreaterThan(localVersion)
|
|
componentStatus.availableReason = fmt.Sprintf("remote version %s is greater than local version %s", remoteVersion.String(), localVersion.String())
|
|
|
|
// Handle pre-release updates
|
|
if remoteVersion.Prerelease() != "" && params.IncludePreRelease && componentStatus.available {
|
|
componentStatus.availableReason += " (pre-release)"
|
|
}
|
|
|
|
// If a custom version is specified, use it to determine if the update is available
|
|
constraint, componentExists := params.Components[component]
|
|
// we don't need to check again if it's already available
|
|
if componentExists && constraint != "" {
|
|
componentStatus.available = componentStatus.version != componentStatus.localVersion
|
|
if componentStatus.available {
|
|
componentStatus.availableReason = fmt.Sprintf("custom version %s is not equal to local version %s", constraint, componentStatus.localVersion)
|
|
}
|
|
} else if !componentExists {
|
|
componentStatus.available = false
|
|
componentStatus.availableReason = "component not specified in update parameters"
|
|
}
|
|
|
|
return nil
|
|
}
|