add basic update functionality

If enabled, jfa-go pings buildrone (hosted at builds.hrfee.pw) every 30
min for new updates. If there is one, it gets information (and if
applicable, a binary) from the appropriate source (buildrone, github, or
dockerhub) and displays it on the admin page. You can switch update
channels between stable and unstable. For binary releases, updates are
downloaded automatically and installed when the user presses update.

Since this obviously introduces some "phone-home" functionality into
jfa-go, I just want to say IPs are not and will not be logged by
buildrone, although I may later introduce functionality to give a rough
idea of the number of users (again, no IPs stored). The whole thing can
also be turned off in settings.
pull/75/head
Harvey Tindall 4 years ago
parent 9787fce275
commit 92332206f0
No known key found for this signature in database
GPG Key ID: BBC65952848FB1A2

@ -18,7 +18,12 @@ steps:
- apt install build-essential python3-pip curl software-properties-common sed upx -y - apt install build-essential python3-pip curl software-properties-common sed upx -y
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -) - (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
- apt install nodejs - apt install nodejs
- curl -sL https://git.io/goreleaser | bash - curl -sL https://git.io/goreleaser > goreleaser
- chmod +x goreleaser
- ./scripts/version.sh ./goreleaser
- wget https://builds.hrfee.pw/upload.py
- pip3 install requests
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag internal=true'
trigger: trigger:
event: event:
- tag - tag
@ -46,6 +51,9 @@ steps:
command_timeout: 50m command_timeout: 50m
script: script:
- /mnt/buildx/jfa-go/build.sh stable - /mnt/buildx/jfa-go/build.sh stable
- wget https://builds.hrfee.pw/upload.py
- pip3 install requests
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-stable=true'
trigger: trigger:
event: event:
- tag - tag
@ -66,12 +74,12 @@ steps:
- apt install build-essential python3-pip curl software-properties-common sed upx -y - apt install build-essential python3-pip curl software-properties-common sed upx -y
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -) - (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
- apt install nodejs - apt install nodejs
- curl -sL https://git.io/goreleaser > goreleaser.sh - curl -sL https://git.io/goreleaser > goreleaser
- chmod +x goreleaser.sh - chmod +x goreleaser
- ./goreleaser.sh --snapshot --skip-publish --rm-dist - ./scripts/version.sh ./goreleaser --snapshot --skip-publish --rm-dist
- wget https://builds.hrfee.pw/upload.py - wget https://builds.hrfee.pw/upload.py
- pip3 install requests - pip3 install requests
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go ./dist/*.tar.gz' - bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --upload ./dist/*.tar.gz --tag internal-git=true'
environment: environment:
BUILDRONE_KEY: BUILDRONE_KEY:
from_secret: BUILDRONE_KEY from_secret: BUILDRONE_KEY
@ -108,6 +116,9 @@ steps:
command_timeout: 50m command_timeout: 50m
script: script:
- /mnt/buildx/jfa-go/build.sh - /mnt/buildx/jfa-go/build.sh
- wget https://builds.hrfee.pw/upload.py
- pip3 install requests
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag docker-unstable=true'
trigger: trigger:
branch: branch:
- main - main
@ -132,9 +143,9 @@ steps:
- apt install build-essential python3-pip curl software-properties-common sed upx -y - apt install build-essential python3-pip curl software-properties-common sed upx -y
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -) - (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
- apt install nodejs - apt install nodejs
- curl -sL https://git.io/goreleaser > goreleaser.sh - curl -sL https://git.io/goreleaser > goreleaser
- chmod +x goreleaser.sh - chmod +x goreleaser
- ./goreleaser.sh --snapshot --skip-publish --rm-dist - ./scripts/version.sh ./goreleaser --snapshot --skip-publish --rm-dist
trigger: trigger:
event: event:

@ -30,6 +30,8 @@ builds:
- dir: ./ - dir: ./
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
ldflags:
- -s -w -X main.version={{.Env.VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary
goos: goos:
- linux - linux
- windows - windows

@ -16,7 +16,7 @@ ENV GOARCH=$TARGETARCH
COPY --from=support /opt/build /opt/build COPY --from=support /opt/build /opt/build
RUN (cd /opt/build; make compile) RUN (cd /opt/build; make compile UPDATER=docker)
FROM golang:latest FROM golang:latest

@ -10,6 +10,14 @@ VERSION ?= $(shell git describe --exact-match HEAD 2> /dev/null || echo vgit)
VERSION := $(shell echo $(VERSION) | sed 's/v//g') VERSION := $(shell echo $(VERSION) | sed 's/v//g')
COMMIT ?= $(shell git rev-parse --short HEAD || echo unknown) COMMIT ?= $(shell git rev-parse --short HEAD || echo unknown)
UPDATER ?= off
BUILDFLAGS := -X main.version=$(VERSION) -X main.commit=$(COMMIT)
ifeq ($(UPDATER), on)
BUILDFLAGS := $(BUILDFLAGS) -X main.updater=binary
else ifneq ($(UPDATER), off)
BUILDFLAGS := $(BUILDFLAGS) -X main.updater=$(UPDATER)
endif
npm: npm:
$(info installing npm dependencies) $(info installing npm dependencies)
npm install npm install
@ -56,14 +64,14 @@ compile:
$(GOBINARY) mod download $(GOBINARY) mod download
$(info Building) $(info Building)
mkdir -p build mkdir -p build
cd build && CGO_ENABLED=0 $(GOBINARY) build -ldflags="-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT)" -o ./jfa-go ../*.go cd build && CGO_ENABLED=0 $(GOBINARY) build -ldflags="-s -w $(BUILDFLAGS)" -o ./jfa-go ../*.go
compile-debug: compile-debug:
$(info Downloading deps) $(info Downloading deps)
$(GOBINARY) mod download $(GOBINARY) mod download
$(info Building) $(info Building)
mkdir -p build mkdir -p build
cd build && CGO_ENABLED=0 $(GOBINARY) build -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT)" -o ./jfa-go ../*.go cd build && CGO_ENABLED=0 $(GOBINARY) build -ldflags "$(BUILDFLAGS)" -o ./jfa-go ../*.go
compress: compress:
upx --lzma build/jfa-go upx --lzma build/jfa-go

@ -1440,6 +1440,39 @@ func (app *appContext) SetEmailState(gc *gin.Context) {
respondBool(200, true, gc) respondBool(200, true, gc)
} }
// @Summary Returns whether there's a new update, and extra info if there is.
// @Produce json
// @Success 200 {object} checkUpdateDTO
// @Router /config/update [get]
// @tags Configuration
func (app *appContext) CheckUpdate(gc *gin.Context) {
if !app.newUpdate {
app.update = Update{}
}
gc.JSON(200, checkUpdateDTO{New: app.newUpdate, Update: app.update})
}
// @Summary Apply an update.
// @Produce json
// @Success 200 {object} boolResponse
// @Success 400 {object} stringResponse
// @Success 500 {object} boolResponse
// @Router /config/update [post]
// @tags Configuration
func (app *appContext) ApplyUpdate(gc *gin.Context) {
if !app.update.CanUpdate {
respond(400, "Update is manual", gc)
return
}
err := app.update.update()
if err != nil {
app.err.Printf("Failed to apply update: %s", err)
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Returns the custom email (generating it if not set) and list of used variables in it. // @Summary Returns the custom email (generating it if not set) and list of used variables in it.
// @Produce json // @Produce json
// @Success 200 {object} customEmailDTO // @Success 200 {object} customEmailDTO

@ -84,6 +84,28 @@ func (app *appContext) loadConfig() error {
emailEnabled = true emailEnabled = true
} }
app.MustSetValue("updates", "enabled", "true")
releaseChannel := app.config.Section("updates").Key("channel").String()
if app.config.Section("updates").Key("enabled").MustBool(false) {
v := version
if releaseChannel == "stable" {
if version == "git" {
v = "0.0.0"
}
} else if releaseChannel == "unstable" {
v = "git"
}
app.updater = newUpdater(baseURL, namespace, repo, v, commit, updater)
}
if releaseChannel == "" {
if version == "git" {
releaseChannel = "unstable"
} else {
releaseChannel = "stable"
}
app.MustSetValue("updates", "channel", releaseChannel)
}
app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String() app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String()
app.storage.loadCustomEmails() app.storage.loadCustomEmails()

@ -1,6 +1,35 @@
{ {
"order": [], "order": [],
"sections": { "sections": {
"updates": {
"order": [],
"meta": {
"name": "Updates",
"description": "Settings for update notifications and release channel."
},
"settings": {
"enabled": {
"name": "Enabled",
"required": true,
"requires_restart": true,
"type": "bool",
"value": true,
"description": "Enable/disable updating notifications and downloading/applying updates."
},
"channel": {
"name": "Release Channel",
"required": true,
"requires_restart": false,
"type": "select",
"options": [
["stable", "Stable"],
["unstable", "Unstable"]
],
"value": "",
"description": "Release channel for updates."
}
}
},
"jellyfin": { "jellyfin": {
"order": [], "order": [],
"meta": { "meta": {

@ -417,3 +417,31 @@ pre {
white-space: -o-pre-wrap; /* Opera 7 */ white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */ word-wrap: break-word; /* Internet Explorer 5.5+ */
} }
.circle {
height: 0.5rem;
width: 0.5rem;
border-radius: 50%;
}
.circle.\~urge {
background-color: var(--color-urge-200);
}
.markdown-box {
max-height: 20rem;
display: block;
overflow-y: scroll;
}
a:link {
color: var(--color-urge-200);
}
a:visited {
color: var(--color-urge-100);
}
a:hover, a:active {
color: var(--color-urge-200);
}

@ -8,6 +8,8 @@ import (
"strings" "strings"
) )
const binaryType = "external"
var localFS fs.FS var localFS fs.FS
var langFS fs.FS var langFS fs.FS

@ -6,6 +6,8 @@ import (
"log" "log"
) )
const binaryType = "internal"
//go:embed data data/html data/web data/web/css data/web/js //go:embed data data/html data/web data/web/css data/web/js
var loFS embed.FS var loFS embed.FS

@ -11,6 +11,7 @@ replace github.com/hrfee/jfa-go/common => ./common
replace github.com/hrfee/jfa-go/ombi => ./ombi replace github.com/hrfee/jfa-go/ombi => ./ombi
require ( require (
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/evanw/esbuild v0.8.50 // indirect github.com/evanw/esbuild v0.8.50 // indirect
github.com/fatih/color v1.10.0 github.com/fatih/color v1.10.0
@ -34,6 +35,7 @@ require (
github.com/mailgun/mailgun-go/v4 v4.3.0 github.com/mailgun/mailgun-go/v4 v4.3.0
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14
github.com/swaggo/gin-swagger v1.3.0 github.com/swaggo/gin-swagger v1.3.0
@ -41,7 +43,8 @@ require (
github.com/ugorji/go v1.2.0 // indirect github.com/ugorji/go v1.2.0 // indirect
github.com/writeas/go-strip-markdown v2.0.1+incompatible github.com/writeas/go-strip-markdown v2.0.1+incompatible
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 // indirect
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 // indirect golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 // indirect
golang.org/x/tools v0.1.0 // indirect golang.org/x/tools v0.1.0 // indirect
google.golang.org/protobuf v1.25.0 // indirect google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/ini.v1 v1.62.0 gopkg.in/ini.v1 v1.62.0

@ -17,6 +17,8 @@ github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJ
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -188,6 +190,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCb
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
@ -266,6 +270,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -293,6 +299,8 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbq
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 h1:cEhElsAv9LUt9ZUUocxzWe05oFLVd+AA2nstydTeI8g=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

@ -254,6 +254,21 @@
</label> </label>
</form> </form>
</div> </div>
<div id="modal-update" class="modal">
<div class="modal-content wide card">
<span class="heading">{{ .strings.updates }} <span class="modal-close">&times;</span></span>
<p class="content">
<h2>
<a id="update-version"></a> (<span class="monospace" id="update-commit"></span>)
</h2>
<p class="content" id="update-description"></p>
<p class="support" id="update-date"></p>
<div class="content markdown-box" id="update-changelog"></div>
</p>
<span class="button ~info !normal full-width center" id="update-download">{{ .strings.download }}</span>
<span class="button ~urge !normal full-width center" id="update-update">{{ .strings.update }}</span>
</div>
</div>
<div id="notification-box"></div> <div id="notification-box"></div>
<span class="dropdown" tabindex="0" id="lang-dropdown"> <span class="dropdown" tabindex="0" id="lang-dropdown">
<span class="button ~urge dropdown-button"> <span class="button ~urge dropdown-button">

@ -24,6 +24,9 @@
"enabled": "Enabled", "enabled": "Enabled",
"disabled": "Disabled", "disabled": "Disabled",
"admin": "Admin", "admin": "Admin",
"updates": "Updates",
"update": "Update",
"download": "Download",
"lastActiveTime": "Last Active", "lastActiveTime": "Last Active",
"from": "From", "from": "From",
"user": "User", "user": "User",
@ -93,6 +96,7 @@
"saveEmail": "Email saved.", "saveEmail": "Email saved.",
"sentAnnouncement": "Announcement sent.", "sentAnnouncement": "Announcement sent.",
"setOmbiDefaults": "Stored ombi defaults.", "setOmbiDefaults": "Stored ombi defaults.",
"updateApplied": "Update applied, please restart.",
"errorConnection": "Couldn't connect to jfa-go.", "errorConnection": "Couldn't connect to jfa-go.",
"error401Unauthorized": "Unauthorized. Try refreshing the page.", "error401Unauthorized": "Unauthorized. Try refreshing the page.",
"errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.", "errorSettingsAppliedNoHomescreenLayout": "Settings were applied, but applying homescreen layout may have failed.",
@ -115,7 +119,11 @@
"errorFailureCheckLogs": "Failed (check console/logs)", "errorFailureCheckLogs": "Failed (check console/logs)",
"errorPartialFailureCheckLogs": "Partial failure (check console/logs)", "errorPartialFailureCheckLogs": "Partial failure (check console/logs)",
"errorUserCreated": "Failed to create user {n}.", "errorUserCreated": "Failed to create user {n}.",
"errorSendWelcomeEmail": "Failed to send welcome email (check console/logs)" "errorSendWelcomeEmail": "Failed to send welcome email (check console/logs)",
"errorApplyUpdate": "Failed to apply update, try manually.",
"errorCheckUpdate": "Failed to check for update.",
"updateAvailable": "A new update is available, check settings.",
"noUpdatesAvailable": "No new updates available."
}, },
"quantityStrings": { "quantityStrings": {
"modifySettingsFor": { "modifySettingsFor": {

@ -48,6 +48,14 @@ var (
commit string commit string
) )
var temp = func() string {
temp := "/tmp"
if PLATFORM == "windows" {
temp = os.Getenv("TEMP")
}
return temp
}()
var serverTypes = map[string]string{ var serverTypes = map[string]string{
"jellyfin": "Jellyfin", "jellyfin": "Jellyfin",
"emby": "Emby (experimental)", "emby": "Emby (experimental)",
@ -90,6 +98,10 @@ type appContext struct {
version string version string
quit chan os.Signal quit chan os.Signal
URLBase string URLBase string
updater *Updater
newUpdate bool // Whether whatever's in update is new.
tag Tag
update Update
} }
func generateSecret(length int) (string, error) { func generateSecret(length int) (string, error) {
@ -521,6 +533,10 @@ func start(asDaemon, firstCall bool) {
if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer { if app.config.Section("password_resets").Key("enabled").MustBool(false) && serverType == mediabrowser.JellyfinServer {
go app.StartPWR() go app.StartPWR()
} }
if app.config.Section("updates").Key("enabled").MustBool(false) {
go app.checkForUpdates()
}
} else { } else {
debugMode = false debugMode = false
address = "0.0.0.0:8056" address = "0.0.0.0:8056"
@ -636,11 +652,7 @@ func printVersion() {
func main() { func main() {
printVersion() printVersion()
folder := "/tmp" SOCK = filepath.Join(temp, SOCK)
if PLATFORM == "windows" {
folder = os.Getenv("TEMP")
}
SOCK = filepath.Join(folder, SOCK)
fmt.Println("Socket:", SOCK) fmt.Println("Socket:", SOCK)
if flagPassed("test") { if flagPassed("test") {
TEST = true TEST = true

@ -214,3 +214,8 @@ type extendExpiryDTO struct {
Hours int `json:"hours" example:"2"` // Number of hours to add. Hours int `json:"hours" example:"2"` // Number of hours to add.
Minutes int `json:"minutes" example:"3"` // Number of minutes to add. Minutes int `json:"minutes" example:"3"` // Number of minutes to add.
} }
type checkUpdateDTO struct {
New bool `json:"new"` // Whether or not there's a new update.
Update Update `json:"update"`
}

6
package-lock.json generated

@ -236,9 +236,9 @@
"integrity": "sha1-vfpzUplmTfr9NFKe1PhSKidf6lY=" "integrity": "sha1-vfpzUplmTfr9NFKe1PhSKidf6lY="
}, },
"esbuild": { "esbuild": {
"version": "0.8.53", "version": "0.8.56",
"resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.8.53.tgz", "resolved": "https://registry.npm.taobao.org/esbuild/download/esbuild-0.8.56.tgz",
"integrity": "sha1-tAi7DKGynasT2Lv31Z9Zr+Z3boY=" "integrity": "sha1-nHw9bmFNtzZ6+jSK2wqyh8KWc14="
}, },
"escalade": { "escalade": {
"version": "3.1.1", "version": "3.1.1",

@ -19,7 +19,7 @@
"dependencies": { "dependencies": {
"@ts-stack/markdown": "^1.3.0", "@ts-stack/markdown": "^1.3.0",
"a17t": "^0.4.0", "a17t": "^0.4.0",
"esbuild": "^0.8.53", "esbuild": "^0.8.56",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"mjml": "^4.8.0", "mjml": "^4.8.0",
"remixicon": "^2.5.0", "remixicon": "^2.5.0",

@ -140,6 +140,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
// api.POST(p + "/setDefaults", app.SetDefaults) // api.POST(p + "/setDefaults", app.SetDefaults)
api.POST(p+"/users/settings", app.ApplySettings) api.POST(p+"/users/settings", app.ApplySettings)
api.POST(p+"/users/announce", app.Announce) api.POST(p+"/users/announce", app.Announce)
api.GET(p+"/config/update", app.CheckUpdate)
api.POST(p+"/config/update", app.ApplyUpdate)
api.GET(p+"/config/emails", app.GetEmails) api.GET(p+"/config/emails", app.GetEmails)
api.GET(p+"/config/emails/:id", app.GetEmail) api.GET(p+"/config/emails/:id", app.GetEmail)
api.POST(p+"/config/emails/:id", app.SetEmail) api.POST(p+"/config/emails/:id", app.SetEmail)

@ -1,27 +0,0 @@
import subprocess
import sys
try:
version = sys.argv[1].replace('v', '')
except IndexError:
version = "git"
if version == "auto":
try:
version = subprocess.check_output("git describe --exact-match HEAD".split()).decode("utf-8").rstrip().replace('v', '')
except subprocess.CalledProcessError as e:
if e.returncode == 128:
version = "git"
commit = subprocess.check_output("git rev-parse --short HEAD".split()).decode("utf-8").rstrip()
file = f'package main; const VERSION = "{version}"; const COMMIT = "{commit}";'
try:
writeto = sys.argv[2]
except IndexError:
writeto = "version.go"
with open(writeto, 'w') as f:
f.write(file)

@ -0,0 +1,3 @@
#!/bin/bash
VERSION=$(git describe --exact-match HEAD 2> /dev/null || echo 'vgit')
VERSION="$(echo $VERSION | sed 's/v//g')" $@

@ -7,6 +7,7 @@ import { accountsList } from "./modules/accounts.js";
import { settingsList } from "./modules/settings.js"; import { settingsList } from "./modules/settings.js";
import { ProfileEditor } from "./modules/profiles.js"; import { ProfileEditor } from "./modules/profiles.js";
import { _get, _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js"; import { _get, _post, notificationBox, whichAnimationEvent, toggleLoader } from "./modules/common.js";
import { Updater } from "./modules/update.js";
loadTheme(); loadTheme();
(document.getElementById('button-theme') as HTMLSpanElement).onclick = toggleTheme; (document.getElementById('button-theme') as HTMLSpanElement).onclick = toggleTheme;
@ -59,9 +60,12 @@ window.availableProfiles = window.availableProfiles || [];
window.modals.customizeEmails = new Modal(document.getElementById("modal-customize")); window.modals.customizeEmails = new Modal(document.getElementById("modal-customize"));
window.modals.extendExpiry = new Modal(document.getElementById("modal-extend-expiry")); window.modals.extendExpiry = new Modal(document.getElementById("modal-extend-expiry"));
window.modals.updateInfo = new Modal(document.getElementById("modal-update"));
})(); })();
var inviteCreator = new createInvite(); var inviteCreator = new createInvite();
var accounts = new accountsList(); var accounts = new accountsList();
window.invites = new inviteList(); window.invites = new inviteList();
@ -154,6 +158,7 @@ function login(username: string, password: string, run?: (state?: number) => voi
} else { } else {
const data = this.response; const data = this.response;
window.token = data["token"]; window.token = data["token"];
window.updater = new Updater(); // mmm, a race condition
window.modals.login.close(); window.modals.login.close();
setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000); setInterval(() => { window.invites.reload(); accounts.reload(); }, 30*1000);
const currentTab = window.tabs.current; const currentTab = window.tabs.current;
@ -198,4 +203,3 @@ login("", "");
return false; return false;
} }
}); });

@ -598,6 +598,14 @@ export class settingsList {
`; `;
(editButton.querySelector("span.button") as HTMLSpanElement).onclick = this._emailEditor.showList; (editButton.querySelector("span.button") as HTMLSpanElement).onclick = this._emailEditor.showList;
this.addSection(name, settings.sections[name], editButton); this.addSection(name, settings.sections[name], editButton);
} else if (name == "updates") {
const icon = document.createElement("span") as HTMLSpanElement;
if (window.updater.updateAvailable) {
icon.classList.add("button", "~urge");
icon.innerHTML = `<i class="ri-download-line" title="${window.lang.strings("update")}"></i>`;
icon.onclick = () => window.updater.checkForUpdates(window.modals.updateInfo.show);
}
this.addSection(name, settings.sections[name], icon);
} else { } else {
this.addSection(name, settings.sections[name]); this.addSection(name, settings.sections[name]);
} }

@ -0,0 +1,124 @@
import { _get, _post, toggleLoader } from "../modules/common.js";
import { Marked, Renderer } from "@ts-stack/markdown";
interface updateDTO {
new: boolean;
update: Update;
}
export class Updater implements updater {
private _update: Update;
private _date: Date;
updateAvailable = false;
checkForUpdates = (run?: (req: XMLHttpRequest) => void) => _get("/config/update", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 200) {
window.notifications.customError("errorCheckUpdate", window.lang.notif("errorCheckUpdate"));
return
}
let resp = req.response as updateDTO;
if (resp.new) {
this.update = resp.update;
if (run) { run(req); }
// } else {
// window.notifications.customPositive("noUpdatesAvailable", "", window.lang.notif("noUpdatesAvailable"));
}
}
});
get date(): number { return Math.floor(this._date.getTime() / 1000); }
set date(unix: number) {
this._date = new Date(unix * 1000);
document.getElementById("update-date").textContent = this._date.toDateString() + " " + this._date.toLocaleTimeString();
}
get description(): string { return this._update.description; }
set description(description: string) {
this._update.description = description;
const el = document.getElementById("update-description") as HTMLParagraphElement;
el.textContent = description;
if (this.version == "git") {
el.classList.add("monospace");
} else {
el.classList.remove("monospace");
}
}
get changelog(): string { return this._update.changelog; }
set changelog(changelog: string) {
this._update.changelog = changelog;
document.getElementById("update-changelog").innerHTML = Marked.parse(changelog);
}
get version(): string { return this._update.version; }
set version(version: string) {
this._update.version = version;
document.getElementById("update-version").textContent = version;
}
get commit(): string { return this._update.commit; }
set commit(commit: string) {
this._update.commit = commit;
document.getElementById("update-commit").textContent = commit.slice(0, 7);
}
get link(): string { return this._update.link; }
set link(link: string) {
this._update.link = link;
(document.getElementById("update-version") as HTMLAnchorElement).href = link;
}
get download_link(): string { return this._update.download_link; }
set download_link(link: string) { this._update.download_link = link; }
get can_update(): boolean { return this._update.can_update; }
set can_update(can: boolean) {
this._update.can_update = can;
const download = document.getElementById("update-download") as HTMLSpanElement;
const update = document.getElementById("update-update") as HTMLSpanElement;
if (can) {
download.classList.add("unfocused");
update.classList.remove("unfocused");
} else {
download.onclick = () => window.open(this._update.download_link || this._update.link);
download.classList.remove("unfocused");
update.classList.add("unfocused");
}
}
get update(): Update { return this._update; }
set update(update: Update) {
this._update = update;
this.version = update.version;
this.commit = update.commit;
this.date = update.date;
this.description = update.description;
this.changelog = update.changelog;
this.link = update.link;
this.download_link = update.download_link;
this.can_update = update.can_update;
}
constructor() {
const update = document.getElementById("update-update") as HTMLSpanElement;
update.onclick = () => {
toggleLoader(update);
_post("/config/update", null, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
toggleLoader(update);
if (req.status != 200) {
window.notifications.customError("applyUpdateError", window.lang.notif("errorApplyUpdate"));
} else {
window.notifications.customSuccess("applyUpdate", window.lang.notif("updateApplied"));
}
window.modals.updateInfo.close();
}
});
};
this.checkForUpdates(() => {
this.updateAvailable = true;
window.notifications.customPositive("updateAvailable", "", window.lang.notif("updateAvailable"));
});
}
}

@ -30,6 +30,24 @@ declare interface Window {
language: string; language: string;
lang: Lang; lang: Lang;
langFile: {}; langFile: {};
updater: updater;
}
declare interface Update {
version: string;
commit: string;
date: number;
description: string;
changelog: string;
link: string;
download_link?: string;
can_update: boolean;
}
declare interface updater extends Update {
checkForUpdates: (run?: (req: XMLHttpRequest) => void) => void;
updateAvailable: boolean;
update: Update;
} }
declare interface Lang { declare interface Lang {
@ -78,6 +96,7 @@ declare interface Modals {
editor: Modal; editor: Modal;
customizeEmails: Modal; customizeEmails: Modal;
extendExpiry: Modal; extendExpiry: Modal;
updateInfo: Modal;
} }
interface Invite { interface Invite {

@ -0,0 +1,487 @@
package main
import (
"archive/tar"
"compress/gzip"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/hrfee/jfa-go/common"
)
const (
baseURL = "https://builds.hrfee.pw"
namespace = "hrfee"
repo = "jfa-go"
)
type GHRelease struct {
HTMLURL string `json:"html_url"`
ID int `json:"id"`
TagName string `json:"tag_name"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
PublishedAt time.Time `json:"published_at"`
Assets []GHAsset `json:"assets"`
Body string `json:"body"`
}
type GHAsset struct {
Name string `json:"name"`
State string `json:"state"`
UpdatedAt time.Time `json:"updated_at"`
BrowserDownloadURL string `json:"browser_download_url"`
}
type UnixTime struct {
time.Time
}
func (t *UnixTime) UnmarshalJSON(b []byte) (err error) {
unix, err := strconv.ParseInt(strings.TrimPrefix(strings.TrimSuffix(string(b), "\""), "\""), 10, 64)
if err != nil {
return
}
t.Time = time.Unix(unix, 0)
return
}
func (t UnixTime) MarshalJSON() ([]byte, error) {
if t.Time == (time.Time{}) {
return []byte("\"\""), nil
}
return []byte("\"" + strconv.FormatInt(t.Time.Unix(), 10) + "\""), nil
}
var updater string
type BuildType int
const (
off BuildType = iota
internal // Internal assets through go:embed, no data/.
external // External assets in data/, accesses through app.localFS.
docker // Only notify of new updates, no self-updating.
)
type ApplyUpdate func() error
type Update struct {
Version string `json:"version"` // vX.X.X or git
Commit string `json:"commit"`
ReleaseDate int64 `json:"date"` // unix time
Description string `json:"description"` // Commit Name/Release title.
Changelog string `json:"changelog"` // Changelog, if applicable
Link string `json:"link"` // Link to commit/release page,
DownloadLink string `json:"download_link"` // Optional link to download page.
CanUpdate bool `json:"can_update"` // Whether or not update can be done automatically.
update ApplyUpdate `json:"-"` // Function to apply update if possible.
}
type Tag struct {
Ready bool `json:"ready"` // Whether or not build on this tag has completed.
Version string `json:"version,omitempty"` // Version/Commit
ReleaseDate UnixTime `json:"date"`
}
var goos = map[string]string{
"darwin": "macOS",
"linux": "Linux",
"windows": "Windows",
}
var goarch = map[string]string{
"amd64": "x86_64",
"arm64": "arm64",
"arm": "armv6",
}
// func newDockerBuild() Update {
// var tag string
// if version == "git" {
// tag = "docker-unstable"
// } else {
// tag = "docker-latest"
// }
// }
type Updater struct {
version, commit, tag, url, namespace, name string
stable bool
buildType BuildType
httpClient *http.Client
timeoutHandler common.TimeoutHandler
binary string
}
func newUpdater(buildroneURL, namespace, repo, version, commit, buildType string) *Updater {
bType := off
fmt.Println("BT", buildType)
tag := ""
switch buildType {
case "binary":
if binaryType == "internal" {
bType = internal
tag = "internal"
} else {
bType = external
tag = "external"
}
case "docker":
bType = docker
if version == "git" {
tag = "docker-unstable"
} else {
tag = "docker-latest"
}
default:
bType = off
}
if commit == "unknown" {
bType = off
}
if version == "git" && bType != docker {
tag += "-git"
}
binary := "jfa-go"
if runtime.GOOS == "windows" {
binary += ".exe"
}
return &Updater{
httpClient: &http.Client{Timeout: 10 * time.Second},
timeoutHandler: common.NewTimeoutHandler("updater", buildroneURL, true),
version: version,
commit: commit,
buildType: bType,
tag: tag,
url: buildroneURL,
namespace: namespace,
name: repo,
binary: binary,
}
}
type BuildDTO struct {
ID int64 // `json:"id"`
Name string // `json:"name"`
Date time.Time // `json:"date"`
Link string // `json:"link"`
Message string
Branch string // `json:"branch"`
Tags map[string]Tag
}
func (ud *Updater) GetTag() (Tag, int, error) {
if ud.buildType == off {
return Tag{}, -1, nil
}
url := fmt.Sprintf("%s/repo/%s/%s/tag/latest/%s", ud.url, ud.namespace, ud.name, ud.tag)
req, _ := http.NewRequest("GET", url, nil)
resp, err := ud.httpClient.Do(req)
defer ud.timeoutHandler()
if err != nil || resp.StatusCode != 200 {
return Tag{}, resp.StatusCode, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return Tag{}, -1, err
}
var tag Tag
err = json.Unmarshal(body, &tag)
return tag, resp.StatusCode, err
}
func (t *Tag) IsNew() bool {
return t.Version == version
}
func (ud *Updater) getRelease() (release GHRelease, status int, err error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", ud.namespace, ud.name)
req, _ := http.NewRequest("GET", url, nil)
resp, err := ud.httpClient.Do(req)
status = resp.StatusCode
defer ud.timeoutHandler()
if err != nil || resp.StatusCode != 200 {
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return
}
err = json.Unmarshal(body, &release)
return
}
func (ud *Updater) GetUpdate(tag Tag) (update Update, status int, err error) {
switch ud.buildType {
case internal:
if ud.tag == "internal-git" {
update, status, err = ud.getUpdateInternalGit(tag)
} else if ud.tag == "internal" {
update, status, err = ud.getUpdateInternal(tag)
}
case external, docker:
if strings.Contains(ud.tag, "git") || ud.tag == "docker-unstable" {
update, status, err = ud.getCommitGit(tag)
} else {
var release GHRelease
release, status, err = ud.getRelease()
if err != nil {
return
}
update = Update{
Changelog: release.Body,
Description: release.Name,
Version: release.TagName,
Commit: tag.Version,
Link: release.HTMLURL,
ReleaseDate: release.PublishedAt.Unix(),
}
}
if ud.buildType == docker {
update.DownloadLink = fmt.Sprintf("https://hub.docker.com/r/%s/%s/tags", ud.namespace, ud.name)
}
}
return
}
func (ud *Updater) getUpdateInternal(tag Tag) (update Update, status int, err error) {
release, status, err := ud.getRelease()
update = Update{
Changelog: release.Body,
Description: release.Name,
Version: release.TagName,
Commit: tag.Version,
Link: release.HTMLURL,
ReleaseDate: release.PublishedAt.Unix(),
}
if err != nil || status != 200 {
return
}
updateFunc, status, err := ud.downloadInternal(&release.Assets, tag)
if err == nil && status == 200 {
update.CanUpdate = true
update.update = updateFunc
}
return
}
func (ud *Updater) getCommitGit(tag Tag) (update Update, status int, err error) {
url := fmt.Sprintf("%s/repo/%s/%s/build/%s", ud.url, ud.namespace, ud.name, tag.Version)
req, _ := http.NewRequest("GET", url, nil)
resp, err := ud.httpClient.Do(req)
status = resp.StatusCode
defer ud.timeoutHandler()
if err != nil || resp.StatusCode != 200 {
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return
}
var build BuildDTO
err = json.Unmarshal(body, &build)
if err != nil {
return
}
update = Update{
Description: build.Name,
Version: "git",
Commit: tag.Version,
Link: build.Link,
ReleaseDate: tag.ReleaseDate.Unix(),
}
return
}
func (ud *Updater) getUpdateInternalGit(tag Tag) (update Update, status int, err error) {
update, status, err = ud.getCommitGit(tag)
if err != nil || status != 200 {
return
}
updateFunc, status, err := ud.downloadInternalGit()
if err == nil && status == 200 {
update.CanUpdate = true
update.update = updateFunc
}
return
}
func getBuildName() string {
operatingSystem, ok := goos[runtime.GOOS]
if !ok {
for _, v := range goos {
if strings.Contains(v, runtime.GOOS) {
operatingSystem = v
break
}
}
}
if operatingSystem == "" {
return ""
}
arch, ok := goarch[runtime.GOARCH]
if !ok {
for _, v := range goarch {
if strings.Contains(v, runtime.GOARCH) {
arch = v
break
}
}
}
if arch == "" {
return ""
}
return operatingSystem + "_" + arch
}
func (ud *Updater) downloadInternal(assets *[]GHAsset, tag Tag) (applyUpdate ApplyUpdate, status int, err error) {
return ud.pullInternal(ud.getInternalURL(assets, tag))
}
func (ud *Updater) downloadInternalGit() (applyUpdate ApplyUpdate, status int, err error) {
return ud.pullInternal(ud.getInternalGitURL())
}
func (ud *Updater) getInternalURL(assets *[]GHAsset, tag Tag) string {
buildName := getBuildName()
if buildName == "" {
return ""
}
url := ""
for _, asset := range *assets {
if strings.Contains(asset.Name, buildName) {
url = asset.BrowserDownloadURL
break
}
}
return url
}
func (ud *Updater) getInternalGitURL() string {
buildName := getBuildName()
if buildName == "" {
return ""
}
return fmt.Sprintf("%s/repo/%s/%s/latest/file/%s", ud.url, ud.namespace, ud.name, buildName)
}
func (ud *Updater) pullInternal(url string) (applyUpdate ApplyUpdate, status int, err error) {
if url == "" {
return
}
req, _ := http.NewRequest("GET", url, nil)
resp, err := ud.httpClient.Do(req)
status = resp.StatusCode
if err != nil || resp.StatusCode != 200 {
return
}
gz, err := gzip.NewReader(resp.Body)
if err != nil {
status = -1
return
}
tarReader := tar.NewReader(gz)
var header *tar.Header
for {
header, err = tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
status = -1
return
}
switch header.Typeflag {
case tar.TypeReg:
// Search only for file named ud.binary
if header.Name == ud.binary {
applyUpdate = func() error {
defer gz.Close()
defer resp.Body.Close()
file, err := os.Executable()
if err != nil {
return err
}
path, err := filepath.EvalSymlinks(file)
if err != nil {
return err
}
info, err := os.Stat(path)
if err != nil {
return err
}
mode := info.Mode()
f, err := os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, tarReader)
if err != nil {
return err
}
return os.Rename(path+"_", path)
}
return
}
}
}
err = errors.New("Couldn't find file: " + ud.binary)
return
}
// func newInternalBuild() Update {
// tag := "internal"
// func update(path string) err {
// if
// fp, err := os.Executable()
// if err != nil {
// return err
// }
// fullPath, err := filepath.EvalSymlinks(fp)
// if err != nil {
// return err
// }
// newBinary,
// }
func (app *appContext) checkForUpdates() {
for {
go func() {
tag, status, err := app.updater.GetTag()
if status != 200 || err != nil {
if strings.Contains(err.Error(), "strconv.ParseInt") {
app.err.Println("No new updates available.")
} else {
app.err.Printf("Failed to get latest tag (%d): %v", status, err)
}
return
}
if tag != app.tag && tag.IsNew() {
app.info.Println("Update found")
update, status, err := app.updater.GetUpdate(tag)
if status != 200 || err != nil {
app.err.Printf("Failed to get update (%d): %v", status, err)
return
}
app.tag = tag
app.update = update
app.newUpdate = true
}
}()
time.Sleep(30 * time.Minute)
}
}
Loading…
Cancel
Save