feat: update button (download, verify, stage)
This commit is contained in:
1 parent
834493a4f5
commit
70460e4170
41 files changed
+5345
-97
No files matched your search
@@ -4,17 +4,29 @@ import "RenewCMS/internal/domain/update"
|
||||
|
||||
type GetUpdateStatusUseCase struct {
|
||||
store update.StatusStore
|
||||
jobs update.JobStore
|
||||
helper update.HelperDetector
|
||||
currentVersion string
|
||||
}
|
||||
|
||||
func NewGetUpdateStatusUseCase(store update.StatusStore, currentVersion string) *GetUpdateStatusUseCase {
|
||||
return &GetUpdateStatusUseCase{store: store, currentVersion: currentVersion}
|
||||
func NewGetUpdateStatusUseCase(store update.StatusStore, jobs update.JobStore, helper update.HelperDetector, currentVersion string) *GetUpdateStatusUseCase {
|
||||
return &GetUpdateStatusUseCase{store: store, jobs: jobs, helper: helper, currentVersion: currentVersion}
|
||||
}
|
||||
|
||||
func (g *GetUpdateStatusUseCase) GetStatus() update.UpdateStatus {
|
||||
result, found := g.store.Load()
|
||||
if !found {
|
||||
return update.NewStatus(g.currentVersion, nil)
|
||||
func (g *GetUpdateStatusUseCase) GetStatus(isOwner bool) update.UpdateStatus {
|
||||
return buildUpdateStatus(g.store, g.jobs, g.currentVersion, isOwner && g.helper.Installed())
|
||||
}
|
||||
|
||||
func buildUpdateStatus(store update.StatusStore, jobs update.JobStore, currentVersion string, canApply bool) update.UpdateStatus {
|
||||
var status update.UpdateStatus
|
||||
if result, found := store.Load(); found {
|
||||
status = update.NewStatus(currentVersion, &result)
|
||||
} else {
|
||||
status = update.NewStatus(currentVersion, nil)
|
||||
}
|
||||
return update.NewStatus(g.currentVersion, &result)
|
||||
if job, found := jobs.Load(); found {
|
||||
status = status.WithJob(job)
|
||||
}
|
||||
status.CanApply = canApply
|
||||
return status
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package useCases
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/update"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StartUpdateDownloadUseCase struct {
|
||||
store update.StatusStore
|
||||
jobs update.JobStore
|
||||
helper update.HelperDetector
|
||||
fetcher update.AssetFetcher
|
||||
verifier update.SignatureVerifier
|
||||
runner update.StagedBinaryRunner
|
||||
backup update.DatabaseBackup
|
||||
staging update.StagingArea
|
||||
logger update.Logger
|
||||
currentVersion string
|
||||
platform update.Platform
|
||||
feedHost string
|
||||
now func() time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type StartUpdateDownloadDependencies struct {
|
||||
Store update.StatusStore
|
||||
Jobs update.JobStore
|
||||
Helper update.HelperDetector
|
||||
Fetcher update.AssetFetcher
|
||||
Verifier update.SignatureVerifier
|
||||
Runner update.StagedBinaryRunner
|
||||
Backup update.DatabaseBackup
|
||||
Staging update.StagingArea
|
||||
Logger update.Logger
|
||||
CurrentVersion string
|
||||
Platform update.Platform
|
||||
FeedURL string
|
||||
Now func() time.Time
|
||||
}
|
||||
|
||||
type jobFailure struct {
|
||||
kind string
|
||||
reason error
|
||||
}
|
||||
|
||||
func (f jobFailure) Error() string {
|
||||
return f.reason.Error()
|
||||
}
|
||||
|
||||
func NewStartUpdateDownloadUseCase(dependencies StartUpdateDownloadDependencies) *StartUpdateDownloadUseCase {
|
||||
return &StartUpdateDownloadUseCase{
|
||||
store: dependencies.Store,
|
||||
jobs: dependencies.Jobs,
|
||||
helper: dependencies.Helper,
|
||||
fetcher: dependencies.Fetcher,
|
||||
verifier: dependencies.Verifier,
|
||||
runner: dependencies.Runner,
|
||||
backup: dependencies.Backup,
|
||||
staging: dependencies.Staging,
|
||||
logger: dependencies.Logger,
|
||||
currentVersion: dependencies.CurrentVersion,
|
||||
platform: dependencies.Platform,
|
||||
feedHost: update.HostOf(dependencies.FeedURL),
|
||||
now: dependencies.Now,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StartUpdateDownloadUseCase) Start() (update.UpdateStatus, error) {
|
||||
if !s.helper.Installed() {
|
||||
return update.UpdateStatus{}, update.ErrNotPrepared
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if job, found := s.jobs.Load(); found && job.Occupies() {
|
||||
return update.UpdateStatus{}, update.ErrJobInProgress
|
||||
}
|
||||
result, found := s.store.Load()
|
||||
if !found || !update.IsNewer(s.currentVersion, result.Latest) {
|
||||
return update.UpdateStatus{}, update.ErrNothingToUpdate
|
||||
}
|
||||
binary, checksums, signature, err := s.preflight(result)
|
||||
if err != nil {
|
||||
s.logger.Printf("update download refused: %v", err)
|
||||
return update.UpdateStatus{}, update.ErrPreflightRefused
|
||||
}
|
||||
|
||||
job := update.Job{
|
||||
Tag: result.Latest,
|
||||
State: update.StateDownloading,
|
||||
StartedAt: s.now().UTC(),
|
||||
TotalBytes: binary.Size,
|
||||
}
|
||||
if err := s.jobs.Save(job); err != nil {
|
||||
return update.UpdateStatus{}, err
|
||||
}
|
||||
status := buildUpdateStatus(s.store, s.jobs, s.currentVersion, true)
|
||||
go s.run(job, binary, checksums, signature)
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (s *StartUpdateDownloadUseCase) preflight(result update.CheckResult) (binary, checksums, signature update.Asset, err error) {
|
||||
names := []string{update.AssetName(result.Latest, s.platform), update.ChecksumsAssetName, update.SignatureAssetName}
|
||||
found := make([]update.Asset, 0, len(names))
|
||||
for _, name := range names {
|
||||
asset, ok := update.FindAsset(result.Assets, name)
|
||||
if !ok {
|
||||
return binary, checksums, signature, fmt.Errorf("asset %q is not listed for %s", name, result.Latest)
|
||||
}
|
||||
if !update.IsAllowedAssetURL(asset.URL, s.feedHost) {
|
||||
return binary, checksums, signature, fmt.Errorf("asset %q has a url outside the feed host or without https", name)
|
||||
}
|
||||
found = append(found, asset)
|
||||
}
|
||||
return found[0], found[1], found[2], nil
|
||||
}
|
||||
|
||||
func (s *StartUpdateDownloadUseCase) run(job update.Job, binary, checksums, signature update.Asset) {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
s.fail(job, downloadFailure(fmt.Errorf("panic: %v", recovered)))
|
||||
}
|
||||
}()
|
||||
if err := s.execute(&job, binary, checksums, signature); err != nil {
|
||||
s.fail(job, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StartUpdateDownloadUseCase) execute(job *update.Job, binary, checksums, signature update.Asset) error {
|
||||
binaryPath := s.staging.PathFor(job.Tag, binary.Name)
|
||||
checksumsPath := s.staging.PathFor(job.Tag, checksums.Name)
|
||||
signaturePath := s.staging.PathFor(job.Tag, signature.Name)
|
||||
|
||||
if err := s.staging.Prepare(job.Tag); err != nil {
|
||||
return downloadFailure(err)
|
||||
}
|
||||
if err := s.fetchSmall(checksums, checksumsPath); err != nil {
|
||||
return downloadFailure(err)
|
||||
}
|
||||
if err := s.fetchSmall(signature, signaturePath); err != nil {
|
||||
return downloadFailure(err)
|
||||
}
|
||||
if err := s.fetchBinary(job, binary, binaryPath); err != nil {
|
||||
return downloadFailure(err)
|
||||
}
|
||||
|
||||
job.State = update.StateVerifying
|
||||
if err := s.jobs.Save(*job); err != nil {
|
||||
return downloadFailure(err)
|
||||
}
|
||||
if err := s.verify(job.Tag, binary.Name, binaryPath, checksumsPath, signaturePath); err != nil {
|
||||
return jobFailure{kind: update.ErrorKindVerification, reason: err}
|
||||
}
|
||||
if _, err := s.backup.Backup(); err != nil {
|
||||
return downloadFailure(err)
|
||||
}
|
||||
|
||||
job.State = update.StateReady
|
||||
job.StagedBinary = binaryPath
|
||||
job.DownloadedBytes = 0
|
||||
job.TotalBytes = 0
|
||||
return s.jobs.Save(*job)
|
||||
}
|
||||
|
||||
func (s *StartUpdateDownloadUseCase) fetchSmall(asset update.Asset, destination string) error {
|
||||
if err := s.fetcher.Fetch(asset, destination, ignoreProgress); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.assertDeclaredSize(asset, destination)
|
||||
}
|
||||
|
||||
func (s *StartUpdateDownloadUseCase) fetchBinary(job *update.Job, asset update.Asset, destination string) error {
|
||||
lastPercent := -1
|
||||
report := func(done int64, _ int64) {
|
||||
current := *job
|
||||
current.DownloadedBytes = done
|
||||
percent := 0
|
||||
if progress := current.Progress(); progress != nil {
|
||||
percent = progress.Percent
|
||||
}
|
||||
if percent == lastPercent && done < asset.Size {
|
||||
return
|
||||
}
|
||||
lastPercent = percent
|
||||
_ = s.jobs.Save(current)
|
||||
}
|
||||
if err := s.fetcher.Fetch(asset, destination, report); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.assertDeclaredSize(asset, destination)
|
||||
}
|
||||
|
||||
func ignoreProgress(int64, int64) {}
|
||||
|
||||
func (s *StartUpdateDownloadUseCase) assertDeclaredSize(asset update.Asset, path string) error {
|
||||
size, err := s.staging.Size(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if asset.Size > 0 && size != asset.Size {
|
||||
return fmt.Errorf("received %d bytes for %q, the release declares %d", size, asset.Name, asset.Size)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StartUpdateDownloadUseCase) verify(tag, assetName, binaryPath, checksumsPath, signaturePath string) error {
|
||||
if err := s.verifier.Verify(checksumsPath, signaturePath); err != nil {
|
||||
return fmt.Errorf("signature check: %w", err)
|
||||
}
|
||||
content, err := s.staging.ReadChecksums(checksumsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expected, found := update.ChecksumFor(content, assetName)
|
||||
if !found {
|
||||
return fmt.Errorf("checksums.txt has no line for %q", assetName)
|
||||
}
|
||||
actual, err := update.FileSHA256(binaryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if actual != expected {
|
||||
return fmt.Errorf("sha-256 mismatch for %q", assetName)
|
||||
}
|
||||
if err := s.staging.MakeExecutable(binaryPath); err != nil {
|
||||
return err
|
||||
}
|
||||
output, err := s.runner.Version(binaryPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("running the staged binary: %w", err)
|
||||
}
|
||||
if output != tag {
|
||||
return fmt.Errorf("the staged binary reports version %q, expected %q", output, tag)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func downloadFailure(err error) error {
|
||||
return jobFailure{kind: update.ErrorKindDownload, reason: err}
|
||||
}
|
||||
|
||||
func (s *StartUpdateDownloadUseCase) fail(job update.Job, err error) {
|
||||
kind := update.ErrorKindDownload
|
||||
var failure jobFailure
|
||||
if errors.As(err, &failure) {
|
||||
kind = failure.kind
|
||||
}
|
||||
s.logger.Printf("update to %s failed (%s): %v", job.Tag, kind, err)
|
||||
_ = s.staging.Remove(job.Tag)
|
||||
job.State = update.StateFailed
|
||||
job.Error = kind
|
||||
job.DownloadedBytes = 0
|
||||
job.TotalBytes = 0
|
||||
job.StagedBinary = ""
|
||||
if saveErr := s.jobs.Save(job); saveErr != nil {
|
||||
s.logger.Printf("update job record could not be saved: %v", saveErr)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user