safing-portmaster/service/broadcasts/install_info.go

175 lines
4 KiB
Go

package broadcasts
import (
"errors"
"fmt"
"strconv"
"sync"
"time"
semver "github.com/hashicorp/go-version"
"github.com/safing/portbase/database"
"github.com/safing/portbase/database/query"
"github.com/safing/portbase/database/record"
"github.com/safing/portbase/info"
"github.com/safing/portbase/log"
)
const installInfoDBKey = "core:status/install-info"
// InstallInfo holds generic info about the install.
type InstallInfo struct {
record.Base
sync.Mutex
Version string
NumericVersion int64
Time time.Time
NumericDate int64
DaysSinceInstall int64
UnixTimestamp int64
}
// GetInstallInfo returns the install info from the database.
func GetInstallInfo() (*InstallInfo, error) {
r, err := db.Get(installInfoDBKey)
if err != nil {
return nil, err
}
// Unwrap.
if r.IsWrapped() {
// Only allocate a new struct, if we need it.
newRecord := &InstallInfo{}
err = record.Unwrap(r, newRecord)
if err != nil {
return nil, err
}
return newRecord, nil
}
// or adjust type
newRecord, ok := r.(*InstallInfo)
if !ok {
return nil, fmt.Errorf("record not of type *InstallInfo, but %T", r)
}
return newRecord, nil
}
func ensureInstallInfo() {
// Get current install info from database.
installInfo, err := GetInstallInfo()
if err != nil {
installInfo = &InstallInfo{}
if !errors.Is(err, database.ErrNotFound) {
log.Warningf("updates: failed to load install info: %s", err)
}
}
// Fill in missing data and save.
installInfo.checkAll()
if err := installInfo.save(); err != nil {
log.Warningf("updates: failed to save install info: %s", err)
}
}
func (ii *InstallInfo) save() error {
if !ii.KeyIsSet() {
ii.SetKey(installInfoDBKey)
}
return db.Put(ii)
}
func (ii *InstallInfo) checkAll() {
ii.checkVersion()
ii.checkInstallDate()
}
func (ii *InstallInfo) checkVersion() {
// Check if everything is present.
if ii.Version != "" && ii.NumericVersion > 0 {
return
}
// Update version information.
versionInfo := info.GetInfo()
ii.Version = versionInfo.Version
// Update numeric version.
if versionInfo.Version != "" {
numericVersion, err := MakeNumericVersion(versionInfo.Version)
if err != nil {
log.Warningf("updates: failed to make numeric version: %s", err)
} else {
ii.NumericVersion = numericVersion
}
}
}
// MakeNumericVersion makes a numeric version with the first three version
// segment always using three digits.
func MakeNumericVersion(version string) (numericVersion int64, err error) {
// Parse version string.
ver, err := semver.NewVersion(version)
if err != nil {
return 0, fmt.Errorf("failed to parse core version: %w", err)
}
// Transform version for numeric representation.
segments := ver.Segments()
for i := 0; i < 3 && i < len(segments); i++ {
segmentNumber := int64(segments[i])
if segmentNumber > 999 {
segmentNumber = 999
}
switch i {
case 0:
numericVersion += segmentNumber * 1000000
case 1:
numericVersion += segmentNumber * 1000
case 2:
numericVersion += segmentNumber
}
}
return numericVersion, nil
}
func (ii *InstallInfo) checkInstallDate() {
// Check if everything is present.
if ii.UnixTimestamp > 0 &&
ii.NumericDate > 0 &&
ii.DaysSinceInstall > 0 &&
!ii.Time.IsZero() {
return
}
// Find oldest created database entry and use it as install time.
oldest := time.Now().Unix()
it, err := db.Query(query.New("core"))
if err != nil {
log.Warningf("updates: failed to create iterator for searching DB for install time: %s", err)
return
}
defer it.Cancel()
for r := range it.Next {
if oldest > r.Meta().Created {
oldest = r.Meta().Created
}
}
// Set data.
ii.UnixTimestamp = oldest
ii.Time = time.Unix(oldest, 0)
ii.DaysSinceInstall = int64(time.Since(ii.Time).Hours()) / 24
// Transform date for numeric representation.
numericDate, err := strconv.ParseInt(ii.Time.Format("20060102"), 10, 64)
if err != nil {
log.Warningf("updates: failed to make numeric date from %s: %s", ii.Time, err)
} else {
ii.NumericDate = numericDate
}
}