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 }