Implement image updates (#580)
This commit is contained in:
parent
a78f92e7cc
commit
33e4058b93
20 changed files with 1174 additions and 748 deletions
|
|
@ -16,7 +16,25 @@ export const Base = Template.bind({});
|
|||
Base.args = {
|
||||
installed: "0.36",
|
||||
available: "0.40",
|
||||
// soc: false,
|
||||
// socTitle: "Mein Auto",
|
||||
// connected: true,
|
||||
};
|
||||
export const ReleaseNotes = Template.bind({});
|
||||
ReleaseNotes.args = {
|
||||
installed: "0.36",
|
||||
available: "0.40",
|
||||
releaseNotes: "Lorem ipsum dolor sit amet consectetur",
|
||||
};
|
||||
|
||||
export const Updater = Template.bind({});
|
||||
Updater.args = {
|
||||
installed: "0.36",
|
||||
available: "0.40",
|
||||
releaseNotes: "Lorem ipsum dolor sit amet consectetur",
|
||||
hasUpdater: true,
|
||||
};
|
||||
|
||||
export const Upgrade = Template.bind({});
|
||||
Upgrade.args = {
|
||||
installed: "0.36",
|
||||
available: "0.40",
|
||||
hasUpdater: true,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,15 +11,26 @@
|
|||
<fa-icon
|
||||
icon="chevron-down"
|
||||
class="expand-icon"
|
||||
:class="{ 'expand-icon-rotated': releaseNotesExpanded }"
|
||||
:class="{ 'expand-icon-rotated': releaseNotesShown }"
|
||||
>
|
||||
</fa-icon>
|
||||
</a>
|
||||
</b>
|
||||
<b class="px-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
data-toggle="modal"
|
||||
data-target="#updateModal"
|
||||
v-if="hasUpdater"
|
||||
@click="toggleUpdater"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
<a
|
||||
:href="'https://github.com/andig/evcc/releases/tag/' + available"
|
||||
class="text-body"
|
||||
v-else
|
||||
>
|
||||
Download <fa-icon icon="chevron-down"></fa-icon>
|
||||
</a>
|
||||
|
|
@ -35,16 +46,71 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition name="fade">
|
||||
<div class="row p-3 bg-light" v-if="releaseNotesExpanded">
|
||||
<div class="row p-3 bg-light" v-if="releaseNotesShown">
|
||||
<div class="col-12" v-html="releaseNotes"></div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<transition name="display">
|
||||
<div
|
||||
id="updateModal"
|
||||
class="dialog"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
v-if="updaterShown"
|
||||
>
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title font-weight-bold">
|
||||
Aktualisierung durchführen
|
||||
</h4>
|
||||
<button type="button" class="close" @click="toggleUpdater">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="font-weight-bold">
|
||||
Nach Aktualisierung wird evcc neu gestartet.
|
||||
</p>
|
||||
<div
|
||||
class="progress"
|
||||
style="margin-top: 16px; margin-bottom: 16px"
|
||||
v-if="updateStarted"
|
||||
>
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: uploadProgress + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
<p>{{ updateStatus }}{{ uploadMessage }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
:class="{ disabled: updateStarted }"
|
||||
@click="toggleUpdater"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" @click="update">
|
||||
Installieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import "../icons";
|
||||
|
||||
export default {
|
||||
|
|
@ -53,11 +119,17 @@ export default {
|
|||
installed: String,
|
||||
available: String,
|
||||
releaseNotes: String,
|
||||
hasUpdater: Boolean,
|
||||
uploadMessage: String,
|
||||
uploadProgress: Number,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
dismissed: false,
|
||||
releaseNotesExpanded: false,
|
||||
releaseNotesShown: false,
|
||||
updaterShown: false,
|
||||
updateStarted: false,
|
||||
updateStatus: "",
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -66,7 +138,24 @@ export default {
|
|||
},
|
||||
toggleReleaseNotes: function (e) {
|
||||
e.preventDefault();
|
||||
this.releaseNotesExpanded = !this.releaseNotesExpanded;
|
||||
this.releaseNotesShown = !this.releaseNotesShown;
|
||||
},
|
||||
toggleUpdater: function (e) {
|
||||
e.preventDefault();
|
||||
if (!this.updateStarted) {
|
||||
this.updaterShown = !this.updaterShown;
|
||||
}
|
||||
},
|
||||
update: function () {
|
||||
axios
|
||||
.post("update")
|
||||
.then(() => {
|
||||
this.updateStatus = "Aktualisierung gestartet: ";
|
||||
this.updateStarted = true;
|
||||
})
|
||||
.catch((res) => {
|
||||
this.updateStatus = "Aktualisierung nicht möglich: " + res;
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -83,7 +172,7 @@ export default {
|
|||
watch: {
|
||||
available: function () {
|
||||
this.dismissed = false;
|
||||
this.releaseNotesExpanded = false;
|
||||
this.releaseNotesShown = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -94,12 +183,25 @@ export default {
|
|||
.fade-leave-active {
|
||||
transition: opacity 0.25s ease-in;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1050;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
outline: 0;
|
||||
}
|
||||
.display-enter-active {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
transition: transform 0.25s ease-in;
|
||||
transform: rotate(0);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,9 @@
|
|||
:installed="installedVersion"
|
||||
:available="store.state.availableVersion"
|
||||
:releaseNotes="store.state.releaseNotes"
|
||||
:hasUpdater="store.state.hasUpdater"
|
||||
:uploadMessage="store.state.uploadMessage"
|
||||
:uploadProgress="store.state.uploadProgress"
|
||||
></Version>
|
||||
|
||||
<router-view></router-view>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
|
@ -132,6 +134,9 @@ func demoConfig() (conf config) {
|
|||
viper.Set(k, v)
|
||||
}
|
||||
|
||||
// demo port
|
||||
viper.Set("uri", fmt.Sprintf("0.0.0.0:%d", defaultPort))
|
||||
|
||||
if err := viper.UnmarshalExact(&conf); err != nil {
|
||||
log.FATAL.Fatalf("failed loading demo config: %v", err)
|
||||
}
|
||||
|
|
|
|||
5
cmd/demoport.go
Normal file
5
cmd/demoport.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// +build !gokrazy
|
||||
|
||||
package cmd
|
||||
|
||||
const defaultPort = 7070
|
||||
5
cmd/demoport_gokrazy.go
Normal file
5
cmd/demoport_gokrazy.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// +build gokrazy
|
||||
|
||||
package cmd
|
||||
|
||||
const defaultPort = 80
|
||||
10
cmd/root.go
10
cmd/root.go
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/andig/evcc/server/updater"
|
||||
"github.com/andig/evcc/util"
|
||||
"github.com/andig/evcc/util/pipe"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
|
@ -162,7 +161,7 @@ func run(cmd *cobra.Command, args []string) {
|
|||
configureJavascript(conf.Javascript)
|
||||
|
||||
// start broadcasting values
|
||||
tee := &Tee{}
|
||||
tee := &util.Tee{}
|
||||
|
||||
// value cache
|
||||
cache := util.NewCache()
|
||||
|
|
@ -191,13 +190,12 @@ func run(cmd *cobra.Command, args []string) {
|
|||
|
||||
// metrics
|
||||
if viper.GetBool("metrics") {
|
||||
httpd.Handle("/metrics", promhttp.Handler())
|
||||
httpd.Router().Handle("/metrics", promhttp.Handler())
|
||||
}
|
||||
|
||||
// pprof
|
||||
if viper.GetBool("profile") {
|
||||
router := httpd.Handler.(*mux.Router)
|
||||
router.PathPrefix("/debug/").Handler(http.DefaultServeMux)
|
||||
httpd.Router().PathPrefix("/debug/").Handler(http.DefaultServeMux)
|
||||
}
|
||||
|
||||
// start HEMS server
|
||||
|
|
@ -214,7 +212,7 @@ func run(cmd *cobra.Command, args []string) {
|
|||
go tee.Run(valueChan)
|
||||
|
||||
// version check
|
||||
go updater.Run(log, valueChan)
|
||||
go updater.Run(log, httpd, tee, valueChan)
|
||||
|
||||
// capture log messages for UI
|
||||
util.CaptureLogs(valueChan)
|
||||
|
|
|
|||
29
cmd/tee.go
29
cmd/tee.go
|
|
@ -1,29 +0,0 @@
|
|||
package cmd
|
||||
|
||||
import "github.com/andig/evcc/util"
|
||||
|
||||
// Tee distributed parameters to subscribers
|
||||
type Tee struct {
|
||||
recv []chan<- util.Param
|
||||
}
|
||||
|
||||
// Attach creates a new receiver channel and attaches it to the tee
|
||||
func (t *Tee) Attach() <-chan util.Param {
|
||||
out := make(chan util.Param)
|
||||
t.Add(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Add attaches a receiver channel to the tee
|
||||
func (t *Tee) Add(out chan<- util.Param) {
|
||||
t.recv = append(t.recv, out)
|
||||
}
|
||||
|
||||
// Run starts parameter distribution
|
||||
func (t *Tee) Run(in <-chan util.Param) {
|
||||
for msg := range in {
|
||||
for _, recv := range t.recv {
|
||||
recv <- msg
|
||||
}
|
||||
}
|
||||
}
|
||||
4
go.mod
4
go.mod
|
|
@ -16,6 +16,7 @@ require (
|
|||
github.com/go-ping/ping v0.0.0-20201022122018-3977ed72668a
|
||||
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
|
||||
github.com/godbus/dbus/v5 v5.0.3
|
||||
github.com/gokrazy/updater v0.0.0-20210106211705-4d92b338dd24
|
||||
github.com/golang/mock v1.4.4
|
||||
github.com/golang/protobuf v1.4.3 // indirect
|
||||
github.com/google/go-github/v32 v32.1.0
|
||||
|
|
@ -52,12 +53,11 @@ require (
|
|||
github.com/spf13/jwalterweatherman v1.1.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.7.1
|
||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
||||
github.com/thoas/go-funk v0.7.0
|
||||
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
|
||||
github.com/volkszaehler/mbmd v0.0.0-20201205173745-5106cb0b334e
|
||||
golang.org/x/net v0.0.0-20200904194848-62affa334b73
|
||||
golang.org/x/sys v0.0.0-20201231184435-2d18734c6014 // indirect
|
||||
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
|
||||
|
|
|
|||
7
go.sum
7
go.sum
|
|
@ -90,6 +90,8 @@ github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlV
|
|||
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gokrazy/updater v0.0.0-20210106211705-4d92b338dd24 h1:tGm83sgE6OiTIyHylO7ZBsBZcMocwLdvtBIOJbpyUIU=
|
||||
github.com/gokrazy/updater v0.0.0-20210106211705-4d92b338dd24/go.mod h1:PYOvzGOL4nlBmuxu7IyKQTFLaxr61+WPRNRzVtuYOHw=
|
||||
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||
|
|
@ -405,6 +407,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
@ -435,8 +438,8 @@ golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIA
|
|||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201231184435-2d18734c6014 h1:joucsQqXmyBVxViHCPFjG3hx8JzIFSaym3l3MM/Jsdg=
|
||||
golang.org/x/sys v0.0.0-20201231184435-2d18734c6014/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 h1:wHn06sgWHMO1VsQ8F+KzDJx/JzqfsNLnc+oEi07qD7s=
|
||||
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ func New(conf map[string]interface{}, site core.SiteAPI, cache *util.Cache, http
|
|||
|
||||
s.hostURI = s.callbackURI()
|
||||
|
||||
s.handlers(httpd.Router)
|
||||
s.handlers(httpd.Router())
|
||||
|
||||
return s, err
|
||||
}
|
||||
|
|
|
|||
1172
server/assets.go
1172
server/assets.go
File diff suppressed because it is too large
Load diff
|
|
@ -319,7 +319,6 @@ func SocketHandler(hub *SocketHub) http.HandlerFunc {
|
|||
// HTTPd wraps an http.Server and adds the root router
|
||||
type HTTPd struct {
|
||||
*http.Server
|
||||
*mux.Router
|
||||
}
|
||||
|
||||
// NewHTTPd creates HTTP server with configured routes for loadpoint
|
||||
|
|
@ -394,9 +393,12 @@ func NewHTTPd(url string, site core.SiteAPI, hub *SocketHub, cache *util.Cache)
|
|||
IdleTimeout: 120 * time.Second,
|
||||
ErrorLog: log.ERROR,
|
||||
},
|
||||
Router: router,
|
||||
}
|
||||
srv.SetKeepAlivesEnabled(true)
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
func (s *HTTPd) Router() *mux.Router {
|
||||
return s.Handler.(*mux.Router)
|
||||
}
|
||||
|
|
|
|||
107
server/updater/github.go
Normal file
107
server/updater/github.go
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/server"
|
||||
"github.com/google/go-github/v32/github"
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
const timeout = 30 * time.Second
|
||||
|
||||
type Repo struct {
|
||||
owner, repository string
|
||||
*github.Client
|
||||
}
|
||||
|
||||
func NewRepo(owner, repository string) *Repo {
|
||||
r := &Repo{
|
||||
owner: owner,
|
||||
repository: repository,
|
||||
Client: github.NewClient(nil),
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Repo) GetLatestRelease() (*github.RepositoryRelease, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
release, _, err := r.Repositories.GetLatestRelease(ctx, r.owner, r.repository)
|
||||
return release, err
|
||||
}
|
||||
|
||||
// ReleaseNotes returns github release notes for the (from,to] semver interval
|
||||
func (r *Repo) ReleaseNotes(from string) (rendered string, err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
var fromVersion *version.Version
|
||||
if fromVersion, err = version.NewVersion(from); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
releases, _, err := r.Repositories.ListReleases(ctx, r.owner, r.repository, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
history := bytes.NewBuffer([]byte{})
|
||||
for _, rel := range releases {
|
||||
tag := *rel.TagName
|
||||
|
||||
var ver *version.Version
|
||||
if ver, err = version.NewVersion(tag); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ver.LessThanOrEqual(fromVersion) {
|
||||
continue
|
||||
}
|
||||
|
||||
if notes := strings.TrimSpace(rel.GetBody()); notes != "" {
|
||||
var md string
|
||||
if md, _, err = r.Markdown(context.Background(), notes, &github.MarkdownOptions{
|
||||
Mode: "gfm",
|
||||
Context: fmt.Sprintf("%s/%s", server.Owner, server.Repository),
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
history.WriteString(fmt.Sprintf("<h1>%s</h1>\n", tag))
|
||||
history.WriteString(md)
|
||||
}
|
||||
}
|
||||
|
||||
return history.String(), nil
|
||||
}
|
||||
|
||||
func (r *Repo) FindReleaseAsset(name string) (int64, int, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
rel, _, err := r.Repositories.GetLatestRelease(ctx, r.owner, r.repository)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
for _, a := range rel.Assets {
|
||||
if *a.Name == name {
|
||||
return *a.ID, *a.Size, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, 0, fmt.Errorf("asset not found: %s", name)
|
||||
}
|
||||
|
||||
func (r *Repo) StreamAsset(id int64) (io.ReadCloser, error) {
|
||||
rc, _, err := r.Repositories.DownloadReleaseAsset(context.Background(), r.owner, r.repository, id, http.DefaultClient)
|
||||
return rc, err
|
||||
}
|
||||
99
server/updater/gokrazy.go
Normal file
99
server/updater/gokrazy.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// +build gokrazy
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gokrazy/updater"
|
||||
)
|
||||
|
||||
// update constants
|
||||
const (
|
||||
MB = 1024 * 1024
|
||||
RootOffset = 8192*512 + 100*MB
|
||||
RootSize = 500 * MB
|
||||
|
||||
RootFS = "evcc_%s.rootfs.gz"
|
||||
Password = "FIat2WTD8i3tJikkvIJI"
|
||||
Port = 8080
|
||||
)
|
||||
|
||||
// unzipReader transparently unpacks zip files
|
||||
func unzipReader(file io.ReadCloser) (io.ReadCloser, error) {
|
||||
if unzipped, err := gzip.NewReader(file); err == nil {
|
||||
return unzipped, err
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
|
||||
type countingWriter struct {
|
||||
count int
|
||||
C chan int
|
||||
}
|
||||
|
||||
func (cw *countingWriter) Write(p []byte) (n int, err error) {
|
||||
cw.count += len(p)
|
||||
cw.C <- cw.count
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
var mutex int32
|
||||
|
||||
// Update request handler
|
||||
func (u *watch) execute(assetID int64, size int) error {
|
||||
if !atomic.CompareAndSwapInt32(&mutex, 0, 1) {
|
||||
return errors.New("upgrade already running")
|
||||
}
|
||||
defer atomic.StoreInt32(&mutex, 0)
|
||||
|
||||
rootFS, err := u.repo.StreamAsset(assetID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rootFS, err = unzipReader(rootFS); err != nil {
|
||||
rootFS.Close()
|
||||
return err
|
||||
}
|
||||
defer rootFS.Close()
|
||||
|
||||
cw := &countingWriter{C: make(chan int)}
|
||||
|
||||
go func() {
|
||||
for v := range cw.C {
|
||||
u.Send("uploadProgress", 100*v/size)
|
||||
}
|
||||
u.Send("uploadProgress", 100)
|
||||
}()
|
||||
|
||||
uri := fmt.Sprintf("http://gokrazy:%s@localhost:%d/", Password, Port)
|
||||
target, err := updater.NewTarget(uri, http.DefaultClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.Send("uploadMessage", "uploading")
|
||||
if err := target.StreamTo("root", io.TeeReader(rootFS, cw)); err != nil {
|
||||
return fmt.Errorf("updating root file system: %w", err)
|
||||
}
|
||||
close(cw.C) // upload finished
|
||||
|
||||
u.Send("uploadMessage", "switching to non-active partition")
|
||||
if err := target.Switch(); err != nil {
|
||||
return fmt.Errorf("switching to non-active partition: %w", err)
|
||||
}
|
||||
|
||||
u.Send("uploadMessage", "rebooting")
|
||||
if err := target.Reboot(); err != nil {
|
||||
return fmt.Errorf("reboot: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
25
server/updater/run.go
Normal file
25
server/updater/run.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// +build !gokrazy
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"github.com/andig/evcc/server"
|
||||
"github.com/andig/evcc/util"
|
||||
"github.com/google/go-github/v32/github"
|
||||
)
|
||||
|
||||
// Run regularly checks version
|
||||
func Run(log *util.Logger, httpd webServer, tee util.TeeAttacher, outChan chan<- util.Param) {
|
||||
u := &watch{
|
||||
log: log,
|
||||
outChan: outChan,
|
||||
repo: NewRepo(server.Owner, server.Repository),
|
||||
}
|
||||
|
||||
c := make(chan *github.RepositoryRelease, 1)
|
||||
go u.watchReleases(server.Version, c) // endless
|
||||
|
||||
for rel := range c {
|
||||
u.Send("availableVersion", *rel.TagName)
|
||||
}
|
||||
}
|
||||
60
server/updater/run_gokrazy.go
Normal file
60
server/updater/run_gokrazy.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
// +build gokrazy
|
||||
|
||||
package updater
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/andig/evcc/server"
|
||||
"github.com/andig/evcc/util"
|
||||
"github.com/google/go-github/v32/github"
|
||||
)
|
||||
|
||||
var (
|
||||
latest *github.RepositoryRelease
|
||||
)
|
||||
|
||||
// Run regularly checks version
|
||||
func Run(log *util.Logger, httpd webServer, tee util.TeeAttacher, outChan chan<- util.Param) {
|
||||
u := &watch{
|
||||
log: log,
|
||||
outChan: outChan,
|
||||
repo: NewRepo(server.Owner, server.Repository),
|
||||
}
|
||||
|
||||
httpd.Router().PathPrefix("/api/update").HandlerFunc(u.updateHandler)
|
||||
|
||||
c := make(chan *github.RepositoryRelease, 1)
|
||||
go u.watchReleases(server.Version, c) // endless
|
||||
|
||||
for rel := range c {
|
||||
u.Send("availableVersion", rel)
|
||||
latest = rel
|
||||
}
|
||||
}
|
||||
|
||||
func (u *watch) updateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || latest == nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
name := fmt.Sprintf(RootFS, *latest.TagName)
|
||||
assetID, size, err := u.repo.FindReleaseAsset(name)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "rootfs image not found: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := u.execute(assetID, size); err != nil {
|
||||
u.log.ERROR.Printf("could not find release image: %v", err)
|
||||
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(w, "update failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/server"
|
||||
"github.com/andig/evcc/util"
|
||||
"github.com/google/go-github/v32/github"
|
||||
"github.com/hashicorp/go-version"
|
||||
latest "github.com/tcnksm/go-latest"
|
||||
)
|
||||
|
||||
var instance *updater
|
||||
|
||||
type updater struct {
|
||||
log *util.Logger
|
||||
cache chan<- util.Param
|
||||
}
|
||||
|
||||
func tag2semver(tag string) string {
|
||||
if strings.Count(tag, ".") < 2 {
|
||||
tag += ".0"
|
||||
}
|
||||
return tag
|
||||
}
|
||||
|
||||
func semver2tag(ver string) string {
|
||||
return strings.TrimSuffix(ver, ".0")
|
||||
}
|
||||
|
||||
// Run regularly checks version
|
||||
func Run(log *util.Logger, cache chan<- util.Param) {
|
||||
instance = &updater{
|
||||
log: log,
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
for range time.NewTicker(time.Hour).C {
|
||||
instance.checkVersion()
|
||||
}
|
||||
}
|
||||
|
||||
// checkVersion validates if updates are available
|
||||
func (u *updater) checkVersion() {
|
||||
githubTag := &latest.GithubTag{
|
||||
Owner: server.Owner,
|
||||
Repository: server.Repository,
|
||||
}
|
||||
|
||||
installed := tag2semver(server.Version)
|
||||
if res, err := latest.Check(githubTag, installed); err == nil {
|
||||
tag := semver2tag(res.Current)
|
||||
if res.Outdated {
|
||||
u.log.INFO.Printf("new version available - please upgrade to %s", tag)
|
||||
}
|
||||
|
||||
u.cache <- util.Param{
|
||||
Key: "availableVersion",
|
||||
Val: tag,
|
||||
}
|
||||
|
||||
u.fetchReleaseNotes()
|
||||
} else {
|
||||
u.log.ERROR.Println("version check failed:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// fetchReleaseNotes retrieves release notes up to semver and sends to client
|
||||
func (u *updater) fetchReleaseNotes() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if notes, err := releaseNotes(ctx, tag2semver(server.Version)); err == nil {
|
||||
u.cache <- util.Param{
|
||||
Key: "releaseNotes",
|
||||
Val: notes,
|
||||
}
|
||||
} else {
|
||||
u.log.WARN.Printf("couldn't download release notes: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// releaseNotes returns github release notes for the (from,to] semver interval
|
||||
func releaseNotes(ctx context.Context, from string) (rendered string, err error) {
|
||||
var fromVersion *version.Version
|
||||
if fromVersion, err = version.NewVersion(from); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
client := github.NewClient(nil)
|
||||
releases, _, err := client.Repositories.ListReleases(ctx, server.Owner, server.Repository, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
history := bytes.NewBuffer([]byte{})
|
||||
for _, rel := range releases {
|
||||
tag := *rel.TagName
|
||||
|
||||
var ver *version.Version
|
||||
if ver, err = version.NewVersion(tag2semver(tag)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if ver.LessThanOrEqual(fromVersion) {
|
||||
continue
|
||||
}
|
||||
|
||||
if notes := strings.TrimSpace(rel.GetBody()); notes != "" {
|
||||
var md string
|
||||
if md, _, err = client.Markdown(context.Background(), notes, &github.MarkdownOptions{
|
||||
Mode: "gfm",
|
||||
Context: fmt.Sprintf("%s/%s", server.Owner, server.Repository),
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
history.WriteString(fmt.Sprintf("<h1>%s</h1>\n", tag))
|
||||
history.WriteString(md)
|
||||
}
|
||||
}
|
||||
|
||||
return history.String(), nil
|
||||
}
|
||||
85
server/updater/watch.go
Normal file
85
server/updater/watch.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/andig/evcc/util"
|
||||
"github.com/google/go-github/v32/github"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
type webServer interface {
|
||||
Router() *mux.Router
|
||||
}
|
||||
|
||||
type watch struct {
|
||||
log *util.Logger
|
||||
outChan chan<- util.Param
|
||||
repo *Repo
|
||||
}
|
||||
|
||||
func (u *watch) Send(key string, val interface{}) {
|
||||
u.outChan <- util.Param{
|
||||
Key: key,
|
||||
Val: val,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *watch) watchReleases(installed string, out chan *github.RepositoryRelease) {
|
||||
for range time.NewTicker(6 * time.Hour).C {
|
||||
rel, err := u.findReleaseUpdate(installed)
|
||||
if err != nil {
|
||||
u.log.ERROR.Printf("version check failed: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if rel != nil {
|
||||
u.log.INFO.Printf("new version available: %s", *rel.TagName)
|
||||
out <- rel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// findReleaseUpdate validates if updates are available
|
||||
func (u *watch) findReleaseUpdate(installed string) (*github.RepositoryRelease, error) {
|
||||
rel, err := u.repo.GetLatestRelease()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rel.TagName == nil {
|
||||
return nil, errors.New("untagged release")
|
||||
}
|
||||
|
||||
v1, err := version.NewVersion(installed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v2, err := version.NewVersion(*rel.TagName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if v1.LessThan(v2) {
|
||||
go u.fetchReleaseNotes(installed)
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// no update
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// fetchReleaseNotes retrieves release notes up to semver and sends to client
|
||||
func (u *watch) fetchReleaseNotes(installed string) {
|
||||
if notes, err := u.repo.ReleaseNotes(installed); err == nil {
|
||||
u.outChan <- util.Param{
|
||||
Key: "releaseNotes",
|
||||
Val: notes,
|
||||
}
|
||||
} else {
|
||||
u.log.WARN.Printf("couldn't download release notes: %v", err)
|
||||
}
|
||||
}
|
||||
32
util/tee.go
Normal file
32
util/tee.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package util
|
||||
|
||||
// TeeAttacher allows to attach a listener to a tee
|
||||
type TeeAttacher interface {
|
||||
Attach() <-chan Param
|
||||
}
|
||||
|
||||
// Tee distributed parameters to subscribers
|
||||
type Tee struct {
|
||||
recv []chan<- Param
|
||||
}
|
||||
|
||||
// Attach creates a new receiver channel and attaches it to the tee
|
||||
func (t *Tee) Attach() <-chan Param {
|
||||
out := make(chan Param)
|
||||
t.add(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// add attaches a receiver channel to the tee
|
||||
func (t *Tee) add(out chan<- Param) {
|
||||
t.recv = append(t.recv, out)
|
||||
}
|
||||
|
||||
// Run starts parameter distribution
|
||||
func (t *Tee) Run(in <-chan Param) {
|
||||
for msg := range in {
|
||||
for _, recv := range t.recv {
|
||||
recv <- msg
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue