-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathupdater.go
155 lines (122 loc) · 3.26 KB
/
updater.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// Copyright (c) 2018-2022, R.I. Pienaar and the Choria Project contributors
//
// SPDX-License-Identifier: Apache-2.0
package updater
import (
"crypto/sha256"
"errors"
"fmt"
"io"
"log"
"net/url"
"os"
"sync"
)
// Spec describes an available package update
type Spec struct {
BinaryPath string `json:"binary"`
BinaryURI *url.URL `json:"uri,omitempty"`
Sha256Hash string `json:"hash"`
Signature string `json:"signature,omitempty"`
}
// Downloader can download releases from repositories
type Downloader interface {
Configure(*Config) error
FetchSpec() (*Spec, error)
FetchBinary(spec *Spec, target string) error
}
var mu = &sync.Mutex{}
// FetchSpec retrieves the available update specification matching opts
func FetchSpec(opts ...Option) (*Spec, error) {
mu.Lock()
defer mu.Unlock()
config, err := newConfig(opts...)
if err != nil {
return nil, fmt.Errorf("invalid updater configuration: %s", err)
}
spec, err := config.Downloader.FetchSpec()
if err != nil {
return nil, fmt.Errorf("release %s not found: %s", config.Version, err)
}
return spec, nil
}
// Apply applies a specific update
func Apply(opts ...Option) error {
mu.Lock()
defer mu.Unlock()
config, err := newConfig(opts...)
if err != nil {
return fmt.Errorf("invalid updater configuration: %s", err)
}
spec, err := config.Downloader.FetchSpec()
if err != nil {
return fmt.Errorf("release %s not found: %s", config.Version, err)
}
config.Log.Printf("Starting update process to %s from %s", config.Version, config.SourceRepo)
newpath := config.TargetFile + ".new"
err = config.Downloader.FetchBinary(spec, newpath)
if err != nil {
return fmt.Errorf("download failed: %s", err)
}
config.Log.Printf("Saved downloaded binary to %s", newpath)
if !validateChecksum(newpath, spec) {
return fmt.Errorf("downloaded file had an invalid checksum")
}
backup, err := backupTarget(config)
if err != nil {
return fmt.Errorf("could not create backup: %s", err)
}
config.Log.Printf("Created backup of current binary to %s", backup)
err = swapNew(newpath, backup, config)
return err
}
func swapNew(newpath string, backup string, c *Config) error {
oldpath := fmt.Sprintf("%s.old", c.TargetFile)
err := os.Rename(c.TargetFile, oldpath)
if err != nil {
return errors.New(err.Error())
}
defer os.Remove(oldpath)
err = os.Rename(newpath, c.TargetFile)
if err != nil {
rerr := os.Rename(backup, c.TargetFile)
if rerr != nil {
return &rollbackErr{err, rerr}
}
return err
}
return nil
}
func backupTarget(c *Config) (string, error) {
backuppath := fmt.Sprintf("%s.backup", c.TargetFile)
stat, err := os.Stat(c.TargetFile)
if err != nil {
return "", errors.New(err.Error())
}
_ = os.Remove(backuppath)
out, err := os.OpenFile(backuppath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, stat.Mode())
if err != nil {
return "", err
}
defer out.Close()
in, err := os.Open(c.TargetFile)
if err != nil {
return "", err
}
defer in.Close()
_, err = io.Copy(out, in)
return backuppath, err
}
func validateChecksum(newpath string, spec *Spec) bool {
f, err := os.Open(newpath)
if err != nil {
log.Fatal(err)
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
log.Fatal(err)
}
sum := fmt.Sprintf("%x", h.Sum(nil))
return sum == spec.Sha256Hash
}