Files
portmaster/base/updater/resource.go
2024-09-05 10:25:57 +03:00

583 lines
17 KiB
Go

package updater
// import (
// "errors"
// "io/fs"
// "os"
// "path/filepath"
// "sort"
// "strings"
// "sync"
// semver "github.com/hashicorp/go-version"
// "github.com/safing/jess/filesig"
// "github.com/safing/portmaster/base/log"
// "github.com/safing/portmaster/base/utils"
// )
// var devVersion *semver.Version
// func init() {
// var err error
// devVersion, err = semver.NewVersion("0")
// if err != nil {
// panic(err)
// }
// }
// Resource represents a resource (via an identifier) and multiple file versions.
// type Resource struct {
// sync.Mutex
// registry *ResourceRegistry
// notifier *notifier
// // Identifier is the unique identifier for that resource.
// // It forms a file path using a forward-slash as the
// // path separator.
// Identifier string
// // Versions holds all available resource versions.
// Versions []*ResourceVersion
// // ActiveVersion is the last version of the resource
// // that someone requested using GetFile().
// ActiveVersion *ResourceVersion
// // SelectedVersion is newest, selectable version of
// // that resource that is available. A version
// // is selectable if it's not blacklisted by the user.
// // Note that it's not guaranteed that the selected version
// // is available locally. In that case, GetFile will attempt
// // to download the latest version from the updates servers
// // specified in the resource registry.
// SelectedVersion *ResourceVersion
// // VerificationOptions holds the verification options for this resource.
// VerificationOptions *VerificationOptions
// // Index holds a reference to the index this resource was last defined in.
// // Will be nil if resource was only found on disk.
// Index *Index
// }
// ResourceVersion represents a single version of a resource.
// type ResourceVersion struct {
// resource *Resource
// // VersionNumber is the string representation of the resource
// // version.
// VersionNumber string
// semVer *semver.Version
// // Available indicates if this version is available locally.
// Available bool
// // SigAvailable indicates if the signature of this version is available locally.
// SigAvailable bool
// // CurrentRelease indicates that this is the current release that should be
// // selected, if possible.
// CurrentRelease bool
// // PreRelease indicates that this version is pre-release.
// PreRelease bool
// // Blacklisted may be set to true if this version should
// // be skipped and not used. This is useful if the version
// // is known to be broken.
// Blacklisted bool
// }
// func (rv *ResourceVersion) String() string {
// return rv.VersionNumber
// }
// // SemVer returns the semantic version of the resource.
// func (rv *ResourceVersion) SemVer() *semver.Version {
// return rv.semVer
// }
// EqualsVersion normalizes the given version and checks equality with semver.
// func (rv *ResourceVersion) EqualsVersion(version string) bool {
// cmpSemVer, err := semver.NewVersion(version)
// if err != nil {
// return false
// }
// return rv.semVer.Equal(cmpSemVer)
// }
// // isSelectable returns true if the version represented by rv is selectable.
// // A version is selectable if it's not blacklisted and either already locally
// // available or ready to be downloaded.
// func (rv *ResourceVersion) isSelectable() bool {
// switch {
// case rv.Blacklisted:
// // Should not be used.
// return false
// case rv.Available:
// // Is available locally, use!
// return true
// case !rv.resource.registry.Online:
// // Cannot download, because registry is set to offline.
// return false
// case rv.resource.Index == nil:
// // Cannot download, because resource is not part of an index.
// return false
// case !rv.resource.Index.AutoDownload:
// // Cannot download, because index may not automatically download.
// return false
// default:
// // Is not available locally, but we are allowed to download it on request!
// return true
// }
// }
// isBetaVersionNumber checks if rv is marked as a beta version by checking
// the version string. It does not honor the BetaRelease field of rv!
// func (rv *ResourceVersion) isBetaVersionNumber() bool { //nolint:unused
// // "b" suffix check if for backwards compatibility
// // new versions should use the pre-release suffix as
// // declared by https://semver.org
// // i.e. 1.2.3-beta
// switch rv.semVer.Prerelease() {
// case "b", "beta":
// return true
// default:
// return false
// }
// }
// Export makes a copy of the resource with only the exposed information.
// Attributes are copied and safe to access.
// Any ResourceVersion must not be modified.
// func (res *Resource) Export() *Resource {
// res.Lock()
// defer res.Unlock()
// // Copy attibutes.
// export := &Resource{
// Identifier: res.Identifier,
// Versions: make([]*ResourceVersion, len(res.Versions)),
// ActiveVersion: res.ActiveVersion,
// SelectedVersion: res.SelectedVersion,
// }
// // Copy Versions slice.
// copy(export.Versions, res.Versions)
// return export
// }
// // Len is the number of elements in the collection.
// // It implements sort.Interface for ResourceVersion.
// func (res *Resource) Len() int {
// return len(res.Versions)
// }
// // Less reports whether the element with index i should
// // sort before the element with index j.
// // It implements sort.Interface for ResourceVersions.
// func (res *Resource) Less(i, j int) bool {
// return res.Versions[i].semVer.GreaterThan(res.Versions[j].semVer)
// }
// // Swap swaps the elements with indexes i and j.
// // It implements sort.Interface for ResourceVersions.
// func (res *Resource) Swap(i, j int) {
// res.Versions[i], res.Versions[j] = res.Versions[j], res.Versions[i]
// }
// // available returns whether any version of the resource is available.
// func (res *Resource) available() bool {
// for _, rv := range res.Versions {
// if rv.Available {
// return true
// }
// }
// return false
// }
// // inUse returns true if the resource is currently in use.
// func (res *Resource) inUse() bool {
// return res.ActiveVersion != nil
// }
// // AnyVersionAvailable returns true if any version of
// // res is locally available.
// func (res *Resource) AnyVersionAvailable() bool {
// res.Lock()
// defer res.Unlock()
// return res.available()
// }
// func (reg *ResourceRegistry) newResource(identifier string) *Resource {
// return &Resource{
// registry: reg,
// Identifier: identifier,
// Versions: make([]*ResourceVersion, 0, 1),
// VerificationOptions: reg.GetVerificationOptions(identifier),
// }
// }
// // AddVersion adds a resource version to a resource.
// func (res *Resource) AddVersion(version string, available, currentRelease, preRelease bool) error {
// res.Lock()
// defer res.Unlock()
// // reset current release flags
// if currentRelease {
// for _, rv := range res.Versions {
// rv.CurrentRelease = false
// }
// }
// var rv *ResourceVersion
// // check for existing version
// for _, possibleMatch := range res.Versions {
// if possibleMatch.VersionNumber == version {
// rv = possibleMatch
// break
// }
// }
// // create new version if none found
// if rv == nil {
// // parse to semver
// sv, err := semver.NewVersion(version)
// if err != nil {
// return err
// }
// rv = &ResourceVersion{
// resource: res,
// VersionNumber: sv.String(), // Use normalized version.
// semVer: sv,
// }
// res.Versions = append(res.Versions, rv)
// }
// // set flags
// if available {
// rv.Available = true
// // If available and signatures are enabled for this resource, check if the
// // signature is available.
// if res.VerificationOptions != nil && utils.PathExists(rv.storageSigPath()) {
// rv.SigAvailable = true
// }
// }
// if currentRelease {
// rv.CurrentRelease = true
// }
// if preRelease || rv.semVer.Prerelease() != "" {
// rv.PreRelease = true
// }
// return nil
// }
// // GetFile returns the selected version as a *File.
// func (res *Resource) GetFile() *File {
// res.Lock()
// defer res.Unlock()
// // check for notifier
// if res.notifier == nil {
// // create new notifier
// res.notifier = newNotifier()
// }
// // check if version is selected
// if res.SelectedVersion == nil {
// res.selectVersion()
// }
// // create file
// return &File{
// resource: res,
// version: res.SelectedVersion,
// notifier: res.notifier,
// versionedPath: res.SelectedVersion.versionedPath(),
// storagePath: res.SelectedVersion.storagePath(),
// }
// }
// //nolint:gocognit // function already kept as simple as possible
// func (res *Resource) selectVersion() {
// sort.Sort(res)
// // export after we finish
// var fallback bool
// defer func() {
// if fallback {
// log.Tracef("updater: selected version %s (as fallback) for resource %s", res.SelectedVersion, res.Identifier)
// } else {
// log.Debugf("updater: selected version %s for resource %s", res.SelectedVersion, res.Identifier)
// }
// if res.inUse() &&
// res.SelectedVersion != res.ActiveVersion && // new selected version does not match previously selected version
// res.notifier != nil {
// res.notifier.markAsUpgradeable()
// res.notifier = nil
// log.Debugf("updater: active version of %s is %s, update available", res.Identifier, res.ActiveVersion.VersionNumber)
// }
// }()
// if len(res.Versions) == 0 {
// // TODO: find better way to deal with an empty version slice (which should not happen)
// res.SelectedVersion = nil
// return
// }
// // Target selection
// // 1) Dev release if dev mode is active and ignore blacklisting
// if res.registry.DevMode {
// // Get last version, as this will be v0.0.0, if available.
// rv := res.Versions[len(res.Versions)-1]
// // Check if it's v0.0.0.
// if rv.semVer.Equal(devVersion) && rv.Available {
// res.SelectedVersion = rv
// return
// }
// }
// // 2) Find the current release. This may be also be a pre-release.
// for _, rv := range res.Versions {
// if rv.CurrentRelease {
// if rv.isSelectable() {
// res.SelectedVersion = rv
// return
// }
// // There can only be once current release,
// // so we can abort after finding one.
// break
// }
// }
// // 3) If UsePreReleases is set, find any newest version.
// if res.registry.UsePreReleases {
// for _, rv := range res.Versions {
// if rv.isSelectable() {
// res.SelectedVersion = rv
// return
// }
// }
// }
// // 4) Find the newest stable version.
// for _, rv := range res.Versions {
// if !rv.PreRelease && rv.isSelectable() {
// res.SelectedVersion = rv
// return
// }
// }
// // 5) Default to newest.
// res.SelectedVersion = res.Versions[0]
// fallback = true
// }
// // Blacklist blacklists the specified version and selects a new version.
// func (res *Resource) Blacklist(version string) error {
// res.Lock()
// defer res.Unlock()
// // count available and valid versions
// valid := 0
// for _, rv := range res.Versions {
// if rv.semVer.Equal(devVersion) {
// continue // ignore dev versions
// }
// if !rv.Blacklisted {
// valid++
// }
// }
// if valid <= 1 {
// return errors.New("cannot blacklist last version") // last one, cannot blacklist!
// }
// // find version and blacklist
// for _, rv := range res.Versions {
// if rv.VersionNumber == version {
// // blacklist and update
// rv.Blacklisted = true
// res.selectVersion()
// return nil
// }
// }
// return errors.New("could not find version")
// }
// // Purge deletes old updates, retaining a certain amount, specified by
// // the keep parameter. Purge will always keep at least 2 versions so
// // specifying a smaller keep value will have no effect.
// func (res *Resource) Purge(keepExtra int) { //nolint:gocognit
// res.Lock()
// defer res.Unlock()
// // If there is any blacklisted version within the resource, pause purging.
// // In this case we may need extra available versions beyond what would be
// // available after purging.
// for _, rv := range res.Versions {
// if rv.Blacklisted {
// log.Debugf(
// "%s: pausing purging of resource %s, as it contains blacklisted items",
// res.registry.Name,
// rv.resource.Identifier,
// )
// return
// }
// }
// // Safeguard the amount of extra version to keep.
// if keepExtra < 2 {
// keepExtra = 2
// }
// // Search for purge boundary.
// var purgeBoundary int
// var skippedActiveVersion bool
// var skippedSelectedVersion bool
// var skippedStableVersion bool
// boundarySearch:
// for i, rv := range res.Versions {
// // Check if required versions are already skipped.
// switch {
// case !skippedActiveVersion && res.ActiveVersion != nil:
// // Skip versions until the active version, if it's set.
// case !skippedSelectedVersion && res.SelectedVersion != nil:
// // Skip versions until the selected version, if it's set.
// case !skippedStableVersion:
// // Skip versions until the stable version.
// default:
// // All required version skipped, set purge boundary.
// purgeBoundary = i + keepExtra
// break boundarySearch
// }
// // Check if current instance is a required version.
// if rv == res.ActiveVersion {
// skippedActiveVersion = true
// }
// if rv == res.SelectedVersion {
// skippedSelectedVersion = true
// }
// if !rv.PreRelease {
// skippedStableVersion = true
// }
// }
// // Check if there is anything to purge at all.
// if purgeBoundary <= keepExtra || purgeBoundary >= len(res.Versions) {
// return
// }
// // Purge everything beyond the purge boundary.
// for _, rv := range res.Versions[purgeBoundary:] {
// // Only remove if resource file is actually available.
// if !rv.Available {
// continue
// }
// // Remove resource file.
// storagePath := rv.storagePath()
// err := os.Remove(storagePath)
// if err != nil {
// if !errors.Is(err, fs.ErrNotExist) {
// log.Warningf("%s: failed to purge resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err)
// }
// } else {
// log.Tracef("%s: purged resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber)
// }
// // Remove resource signature file.
// err = os.Remove(rv.storageSigPath())
// if err != nil {
// if !errors.Is(err, fs.ErrNotExist) {
// log.Warningf("%s: failed to purge resource signature %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err)
// }
// } else {
// log.Tracef("%s: purged resource signature %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber)
// }
// // Remove unpacked version of resource.
// ext := filepath.Ext(storagePath)
// if ext == "" {
// // Nothing to do if file does not have an extension.
// continue
// }
// unpackedPath := strings.TrimSuffix(storagePath, ext)
// // Remove if it exists, or an error occurs on access.
// _, err = os.Stat(unpackedPath)
// if err == nil || !errors.Is(err, fs.ErrNotExist) {
// err = os.Remove(unpackedPath)
// if err != nil {
// log.Warningf("%s: failed to purge unpacked resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err)
// } else {
// log.Tracef("%s: purged unpacked resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber)
// }
// }
// }
// // remove entries of deleted files
// res.Versions = res.Versions[purgeBoundary:]
// }
// // SigningMetadata returns the metadata to be included in signatures.
// func (rv *ResourceVersion) SigningMetadata() map[string]string {
// return map[string]string{
// "id": rv.resource.Identifier,
// "version": rv.VersionNumber,
// }
// }
// // GetFile returns the version as a *File.
// // It locks the resource for doing so.
// func (rv *ResourceVersion) GetFile() *File {
// rv.resource.Lock()
// defer rv.resource.Unlock()
// // check for notifier
// if rv.resource.notifier == nil {
// // create new notifier
// rv.resource.notifier = newNotifier()
// }
// // create file
// return &File{
// resource: rv.resource,
// version: rv,
// notifier: rv.resource.notifier,
// versionedPath: rv.versionedPath(),
// storagePath: rv.storagePath(),
// }
// }
// // versionedPath returns the versioned identifier.
// func (rv *ResourceVersion) versionedPath() string {
// return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber)
// }
// // versionedSigPath returns the versioned identifier of the file signature.
// func (rv *ResourceVersion) versionedSigPath() string {
// return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) + filesig.Extension
// }
// // storagePath returns the absolute storage path.
// func (rv *ResourceVersion) storagePath() string {
// return filepath.Join(rv.resource.registry.storageDir.Path, filepath.FromSlash(rv.versionedPath()))
// }
// // storageSigPath returns the absolute storage path of the file signature.
// func (rv *ResourceVersion) storageSigPath() string {
// return rv.storagePath() + filesig.Extension
// }