github.com/safing/portbase@v0.19.5/updater/resource.go (about)

     1  package updater
     2  
     3  import (
     4  	"errors"
     5  	"io/fs"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  	"sync"
    11  
    12  	semver "github.com/hashicorp/go-version"
    13  
    14  	"github.com/safing/jess/filesig"
    15  	"github.com/safing/portbase/log"
    16  	"github.com/safing/portbase/utils"
    17  )
    18  
    19  var devVersion *semver.Version
    20  
    21  func init() {
    22  	var err error
    23  	devVersion, err = semver.NewVersion("0")
    24  	if err != nil {
    25  		panic(err)
    26  	}
    27  }
    28  
    29  // Resource represents a resource (via an identifier) and multiple file versions.
    30  type Resource struct {
    31  	sync.Mutex
    32  	registry *ResourceRegistry
    33  	notifier *notifier
    34  
    35  	// Identifier is the unique identifier for that resource.
    36  	// It forms a file path using a forward-slash as the
    37  	// path separator.
    38  	Identifier string
    39  
    40  	// Versions holds all available resource versions.
    41  	Versions []*ResourceVersion
    42  
    43  	// ActiveVersion is the last version of the resource
    44  	// that someone requested using GetFile().
    45  	ActiveVersion *ResourceVersion
    46  
    47  	// SelectedVersion is newest, selectable version of
    48  	// that resource that is available. A version
    49  	// is selectable if it's not blacklisted by the user.
    50  	// Note that it's not guaranteed that the selected version
    51  	// is available locally. In that case, GetFile will attempt
    52  	// to download the latest version from the updates servers
    53  	// specified in the resource registry.
    54  	SelectedVersion *ResourceVersion
    55  
    56  	// VerificationOptions holds the verification options for this resource.
    57  	VerificationOptions *VerificationOptions
    58  
    59  	// Index holds a reference to the index this resource was last defined in.
    60  	// Will be nil if resource was only found on disk.
    61  	Index *Index
    62  }
    63  
    64  // ResourceVersion represents a single version of a resource.
    65  type ResourceVersion struct {
    66  	resource *Resource
    67  
    68  	// VersionNumber is the string representation of the resource
    69  	// version.
    70  	VersionNumber string
    71  	semVer        *semver.Version
    72  
    73  	// Available indicates if this version is available locally.
    74  	Available bool
    75  
    76  	// SigAvailable indicates if the signature of this version is available locally.
    77  	SigAvailable bool
    78  
    79  	// CurrentRelease indicates that this is the current release that should be
    80  	// selected, if possible.
    81  	CurrentRelease bool
    82  
    83  	// PreRelease indicates that this version is pre-release.
    84  	PreRelease bool
    85  
    86  	// Blacklisted may be set to true if this version should
    87  	// be skipped and not used. This is useful if the version
    88  	// is known to be broken.
    89  	Blacklisted bool
    90  }
    91  
    92  func (rv *ResourceVersion) String() string {
    93  	return rv.VersionNumber
    94  }
    95  
    96  // SemVer returns the semantic version of the resource.
    97  func (rv *ResourceVersion) SemVer() *semver.Version {
    98  	return rv.semVer
    99  }
   100  
   101  // EqualsVersion normalizes the given version and checks equality with semver.
   102  func (rv *ResourceVersion) EqualsVersion(version string) bool {
   103  	cmpSemVer, err := semver.NewVersion(version)
   104  	if err != nil {
   105  		return false
   106  	}
   107  
   108  	return rv.semVer.Equal(cmpSemVer)
   109  }
   110  
   111  // isSelectable returns true if the version represented by rv is selectable.
   112  // A version is selectable if it's not blacklisted and either already locally
   113  // available or ready to be downloaded.
   114  func (rv *ResourceVersion) isSelectable() bool {
   115  	switch {
   116  	case rv.Blacklisted:
   117  		// Should not be used.
   118  		return false
   119  	case rv.Available:
   120  		// Is available locally, use!
   121  		return true
   122  	case !rv.resource.registry.Online:
   123  		// Cannot download, because registry is set to offline.
   124  		return false
   125  	case rv.resource.Index == nil:
   126  		// Cannot download, because resource is not part of an index.
   127  		return false
   128  	case !rv.resource.Index.AutoDownload:
   129  		// Cannot download, because index may not automatically download.
   130  		return false
   131  	default:
   132  		// Is not available locally, but we are allowed to download it on request!
   133  		return true
   134  	}
   135  }
   136  
   137  // isBetaVersionNumber checks if rv is marked as a beta version by checking
   138  // the version string. It does not honor the BetaRelease field of rv!
   139  func (rv *ResourceVersion) isBetaVersionNumber() bool { //nolint:unused
   140  	// "b" suffix check if for backwards compatibility
   141  	// new versions should use the pre-release suffix as
   142  	// declared by https://semver.org
   143  	// i.e. 1.2.3-beta
   144  	switch rv.semVer.Prerelease() {
   145  	case "b", "beta":
   146  		return true
   147  	default:
   148  		return false
   149  	}
   150  }
   151  
   152  // Export makes a copy of the resource with only the exposed information.
   153  // Attributes are copied and safe to access.
   154  // Any ResourceVersion must not be modified.
   155  func (res *Resource) Export() *Resource {
   156  	res.Lock()
   157  	defer res.Unlock()
   158  
   159  	// Copy attibutes.
   160  	export := &Resource{
   161  		Identifier:      res.Identifier,
   162  		Versions:        make([]*ResourceVersion, len(res.Versions)),
   163  		ActiveVersion:   res.ActiveVersion,
   164  		SelectedVersion: res.SelectedVersion,
   165  	}
   166  	// Copy Versions slice.
   167  	copy(export.Versions, res.Versions)
   168  
   169  	return export
   170  }
   171  
   172  // Len is the number of elements in the collection.
   173  // It implements sort.Interface for ResourceVersion.
   174  func (res *Resource) Len() int {
   175  	return len(res.Versions)
   176  }
   177  
   178  // Less reports whether the element with index i should
   179  // sort before the element with index j.
   180  // It implements sort.Interface for ResourceVersions.
   181  func (res *Resource) Less(i, j int) bool {
   182  	return res.Versions[i].semVer.GreaterThan(res.Versions[j].semVer)
   183  }
   184  
   185  // Swap swaps the elements with indexes i and j.
   186  // It implements sort.Interface for ResourceVersions.
   187  func (res *Resource) Swap(i, j int) {
   188  	res.Versions[i], res.Versions[j] = res.Versions[j], res.Versions[i]
   189  }
   190  
   191  // available returns whether any version of the resource is available.
   192  func (res *Resource) available() bool {
   193  	for _, rv := range res.Versions {
   194  		if rv.Available {
   195  			return true
   196  		}
   197  	}
   198  	return false
   199  }
   200  
   201  // inUse returns true if the resource is currently in use.
   202  func (res *Resource) inUse() bool {
   203  	return res.ActiveVersion != nil
   204  }
   205  
   206  // AnyVersionAvailable returns true if any version of
   207  // res is locally available.
   208  func (res *Resource) AnyVersionAvailable() bool {
   209  	res.Lock()
   210  	defer res.Unlock()
   211  
   212  	return res.available()
   213  }
   214  
   215  func (reg *ResourceRegistry) newResource(identifier string) *Resource {
   216  	return &Resource{
   217  		registry:            reg,
   218  		Identifier:          identifier,
   219  		Versions:            make([]*ResourceVersion, 0, 1),
   220  		VerificationOptions: reg.GetVerificationOptions(identifier),
   221  	}
   222  }
   223  
   224  // AddVersion adds a resource version to a resource.
   225  func (res *Resource) AddVersion(version string, available, currentRelease, preRelease bool) error {
   226  	res.Lock()
   227  	defer res.Unlock()
   228  
   229  	// reset current release flags
   230  	if currentRelease {
   231  		for _, rv := range res.Versions {
   232  			rv.CurrentRelease = false
   233  		}
   234  	}
   235  
   236  	var rv *ResourceVersion
   237  	// check for existing version
   238  	for _, possibleMatch := range res.Versions {
   239  		if possibleMatch.VersionNumber == version {
   240  			rv = possibleMatch
   241  			break
   242  		}
   243  	}
   244  
   245  	// create new version if none found
   246  	if rv == nil {
   247  		// parse to semver
   248  		sv, err := semver.NewVersion(version)
   249  		if err != nil {
   250  			return err
   251  		}
   252  
   253  		rv = &ResourceVersion{
   254  			resource:      res,
   255  			VersionNumber: sv.String(), // Use normalized version.
   256  			semVer:        sv,
   257  		}
   258  		res.Versions = append(res.Versions, rv)
   259  	}
   260  
   261  	// set flags
   262  	if available {
   263  		rv.Available = true
   264  
   265  		// If available and signatures are enabled for this resource, check if the
   266  		// signature is available.
   267  		if res.VerificationOptions != nil && utils.PathExists(rv.storageSigPath()) {
   268  			rv.SigAvailable = true
   269  		}
   270  	}
   271  	if currentRelease {
   272  		rv.CurrentRelease = true
   273  	}
   274  	if preRelease || rv.semVer.Prerelease() != "" {
   275  		rv.PreRelease = true
   276  	}
   277  
   278  	return nil
   279  }
   280  
   281  // GetFile returns the selected version as a *File.
   282  func (res *Resource) GetFile() *File {
   283  	res.Lock()
   284  	defer res.Unlock()
   285  
   286  	// check for notifier
   287  	if res.notifier == nil {
   288  		// create new notifier
   289  		res.notifier = newNotifier()
   290  	}
   291  
   292  	// check if version is selected
   293  	if res.SelectedVersion == nil {
   294  		res.selectVersion()
   295  	}
   296  
   297  	// create file
   298  	return &File{
   299  		resource:      res,
   300  		version:       res.SelectedVersion,
   301  		notifier:      res.notifier,
   302  		versionedPath: res.SelectedVersion.versionedPath(),
   303  		storagePath:   res.SelectedVersion.storagePath(),
   304  	}
   305  }
   306  
   307  //nolint:gocognit // function already kept as simple as possible
   308  func (res *Resource) selectVersion() {
   309  	sort.Sort(res)
   310  
   311  	// export after we finish
   312  	var fallback bool
   313  	defer func() {
   314  		if fallback {
   315  			log.Tracef("updater: selected version %s (as fallback) for resource %s", res.SelectedVersion, res.Identifier)
   316  		} else {
   317  			log.Debugf("updater: selected version %s for resource %s", res.SelectedVersion, res.Identifier)
   318  		}
   319  
   320  		if res.inUse() &&
   321  			res.SelectedVersion != res.ActiveVersion && // new selected version does not match previously selected version
   322  			res.notifier != nil {
   323  
   324  			res.notifier.markAsUpgradeable()
   325  			res.notifier = nil
   326  
   327  			log.Debugf("updater: active version of %s is %s, update available", res.Identifier, res.ActiveVersion.VersionNumber)
   328  		}
   329  	}()
   330  
   331  	if len(res.Versions) == 0 {
   332  		// TODO: find better way to deal with an empty version slice (which should not happen)
   333  		res.SelectedVersion = nil
   334  		return
   335  	}
   336  
   337  	// Target selection
   338  
   339  	// 1) Dev release if dev mode is active and ignore blacklisting
   340  	if res.registry.DevMode {
   341  		// Get last version, as this will be v0.0.0, if available.
   342  		rv := res.Versions[len(res.Versions)-1]
   343  		// Check if it's v0.0.0.
   344  		if rv.semVer.Equal(devVersion) && rv.Available {
   345  			res.SelectedVersion = rv
   346  			return
   347  		}
   348  	}
   349  
   350  	// 2) Find the current release. This may be also be a pre-release.
   351  	for _, rv := range res.Versions {
   352  		if rv.CurrentRelease {
   353  			if rv.isSelectable() {
   354  				res.SelectedVersion = rv
   355  				return
   356  			}
   357  			// There can only be once current release,
   358  			// so we can abort after finding one.
   359  			break
   360  		}
   361  	}
   362  
   363  	// 3) If UsePreReleases is set, find any newest version.
   364  	if res.registry.UsePreReleases {
   365  		for _, rv := range res.Versions {
   366  			if rv.isSelectable() {
   367  				res.SelectedVersion = rv
   368  				return
   369  			}
   370  		}
   371  	}
   372  
   373  	// 4) Find the newest stable version.
   374  	for _, rv := range res.Versions {
   375  		if !rv.PreRelease && rv.isSelectable() {
   376  			res.SelectedVersion = rv
   377  			return
   378  		}
   379  	}
   380  
   381  	// 5) Default to newest.
   382  	res.SelectedVersion = res.Versions[0]
   383  	fallback = true
   384  }
   385  
   386  // Blacklist blacklists the specified version and selects a new version.
   387  func (res *Resource) Blacklist(version string) error {
   388  	res.Lock()
   389  	defer res.Unlock()
   390  
   391  	// count available and valid versions
   392  	valid := 0
   393  	for _, rv := range res.Versions {
   394  		if rv.semVer.Equal(devVersion) {
   395  			continue // ignore dev versions
   396  		}
   397  		if !rv.Blacklisted {
   398  			valid++
   399  		}
   400  	}
   401  	if valid <= 1 {
   402  		return errors.New("cannot blacklist last version") // last one, cannot blacklist!
   403  	}
   404  
   405  	// find version and blacklist
   406  	for _, rv := range res.Versions {
   407  		if rv.VersionNumber == version {
   408  			// blacklist and update
   409  			rv.Blacklisted = true
   410  			res.selectVersion()
   411  			return nil
   412  		}
   413  	}
   414  
   415  	return errors.New("could not find version")
   416  }
   417  
   418  // Purge deletes old updates, retaining a certain amount, specified by
   419  // the keep parameter. Purge will always keep at least 2 versions so
   420  // specifying a smaller keep value will have no effect.
   421  func (res *Resource) Purge(keepExtra int) { //nolint:gocognit
   422  	res.Lock()
   423  	defer res.Unlock()
   424  
   425  	// If there is any blacklisted version within the resource, pause purging.
   426  	// In this case we may need extra available versions beyond what would be
   427  	// available after purging.
   428  	for _, rv := range res.Versions {
   429  		if rv.Blacklisted {
   430  			log.Debugf(
   431  				"%s: pausing purging of resource %s, as it contains blacklisted items",
   432  				res.registry.Name,
   433  				rv.resource.Identifier,
   434  			)
   435  			return
   436  		}
   437  	}
   438  
   439  	// Safeguard the amount of extra version to keep.
   440  	if keepExtra < 2 {
   441  		keepExtra = 2
   442  	}
   443  
   444  	// Search for purge boundary.
   445  	var purgeBoundary int
   446  	var skippedActiveVersion bool
   447  	var skippedSelectedVersion bool
   448  	var skippedStableVersion bool
   449  boundarySearch:
   450  	for i, rv := range res.Versions {
   451  		// Check if required versions are already skipped.
   452  		switch {
   453  		case !skippedActiveVersion && res.ActiveVersion != nil:
   454  			// Skip versions until the active version, if it's set.
   455  		case !skippedSelectedVersion && res.SelectedVersion != nil:
   456  			// Skip versions until the selected version, if it's set.
   457  		case !skippedStableVersion:
   458  			// Skip versions until the stable version.
   459  		default:
   460  			// All required version skipped, set purge boundary.
   461  			purgeBoundary = i + keepExtra
   462  			break boundarySearch
   463  		}
   464  
   465  		// Check if current instance is a required version.
   466  		if rv == res.ActiveVersion {
   467  			skippedActiveVersion = true
   468  		}
   469  		if rv == res.SelectedVersion {
   470  			skippedSelectedVersion = true
   471  		}
   472  		if !rv.PreRelease {
   473  			skippedStableVersion = true
   474  		}
   475  	}
   476  
   477  	// Check if there is anything to purge at all.
   478  	if purgeBoundary <= keepExtra || purgeBoundary >= len(res.Versions) {
   479  		return
   480  	}
   481  
   482  	// Purge everything beyond the purge boundary.
   483  	for _, rv := range res.Versions[purgeBoundary:] {
   484  		// Only remove if resource file is actually available.
   485  		if !rv.Available {
   486  			continue
   487  		}
   488  
   489  		// Remove resource file.
   490  		storagePath := rv.storagePath()
   491  		err := os.Remove(storagePath)
   492  		if err != nil {
   493  			if !errors.Is(err, fs.ErrNotExist) {
   494  				log.Warningf("%s: failed to purge resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err)
   495  			}
   496  		} else {
   497  			log.Tracef("%s: purged resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber)
   498  		}
   499  
   500  		// Remove resource signature file.
   501  		err = os.Remove(rv.storageSigPath())
   502  		if err != nil {
   503  			if !errors.Is(err, fs.ErrNotExist) {
   504  				log.Warningf("%s: failed to purge resource signature %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err)
   505  			}
   506  		} else {
   507  			log.Tracef("%s: purged resource signature %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber)
   508  		}
   509  
   510  		// Remove unpacked version of resource.
   511  		ext := filepath.Ext(storagePath)
   512  		if ext == "" {
   513  			// Nothing to do if file does not have an extension.
   514  			continue
   515  		}
   516  		unpackedPath := strings.TrimSuffix(storagePath, ext)
   517  
   518  		// Remove if it exists, or an error occurs on access.
   519  		_, err = os.Stat(unpackedPath)
   520  		if err == nil || !errors.Is(err, fs.ErrNotExist) {
   521  			err = os.Remove(unpackedPath)
   522  			if err != nil {
   523  				log.Warningf("%s: failed to purge unpacked resource %s v%s: %s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber, err)
   524  			} else {
   525  				log.Tracef("%s: purged unpacked resource %s v%s", res.registry.Name, rv.resource.Identifier, rv.VersionNumber)
   526  			}
   527  		}
   528  	}
   529  
   530  	// remove entries of deleted files
   531  	res.Versions = res.Versions[purgeBoundary:]
   532  }
   533  
   534  // SigningMetadata returns the metadata to be included in signatures.
   535  func (rv *ResourceVersion) SigningMetadata() map[string]string {
   536  	return map[string]string{
   537  		"id":      rv.resource.Identifier,
   538  		"version": rv.VersionNumber,
   539  	}
   540  }
   541  
   542  // GetFile returns the version as a *File.
   543  // It locks the resource for doing so.
   544  func (rv *ResourceVersion) GetFile() *File {
   545  	rv.resource.Lock()
   546  	defer rv.resource.Unlock()
   547  
   548  	// check for notifier
   549  	if rv.resource.notifier == nil {
   550  		// create new notifier
   551  		rv.resource.notifier = newNotifier()
   552  	}
   553  
   554  	// create file
   555  	return &File{
   556  		resource:      rv.resource,
   557  		version:       rv,
   558  		notifier:      rv.resource.notifier,
   559  		versionedPath: rv.versionedPath(),
   560  		storagePath:   rv.storagePath(),
   561  	}
   562  }
   563  
   564  // versionedPath returns the versioned identifier.
   565  func (rv *ResourceVersion) versionedPath() string {
   566  	return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber)
   567  }
   568  
   569  // versionedSigPath returns the versioned identifier of the file signature.
   570  func (rv *ResourceVersion) versionedSigPath() string {
   571  	return GetVersionedPath(rv.resource.Identifier, rv.VersionNumber) + filesig.Extension
   572  }
   573  
   574  // storagePath returns the absolute storage path.
   575  func (rv *ResourceVersion) storagePath() string {
   576  	return filepath.Join(rv.resource.registry.storageDir.Path, filepath.FromSlash(rv.versionedPath()))
   577  }
   578  
   579  // storageSigPath returns the absolute storage path of the file signature.
   580  func (rv *ResourceVersion) storageSigPath() string {
   581  	return rv.storagePath() + filesig.Extension
   582  }