mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-06-28 20:37:46 +02:00
build time is included in the binary, so the buildrone release date is compared to it when deciding if something is an update or not.
577 lines
14 KiB
Go
577 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"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"
|
|
)
|
|
|
|
var buildTimeUnix string
|
|
var buildTime time.Time = func() time.Time {
|
|
i, _ := strconv.ParseInt(buildTimeUnix, 10, 64)
|
|
return time.Unix(i, 0)
|
|
}()
|
|
|
|
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 {
|
|
// fmt.Printf(`Updater intializing with "%s", "%s", "%s", "%s", "%s", "%s"\n`, buildroneURL, namespace, repo, version, commit, buildType)
|
|
bType := off
|
|
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)
|
|
// fmt.Printf("Pinging URL \"%s\" for updates\n", url)
|
|
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)
|
|
if tag.Version == "" {
|
|
err = errors.New("Tag was empty")
|
|
}
|
|
return tag, resp.StatusCode, err
|
|
}
|
|
|
|
func (t *Tag) IsNew() bool {
|
|
// fmt.Printf("Build Time: %+v, Release Date: %+v", buildTime, t.ReleaseDate)
|
|
return t.Version[:7] != commit && t.Ready && t.ReleaseDate.After(buildTime)
|
|
}
|
|
|
|
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 ""
|
|
}
|
|
tray := ""
|
|
if TRAY {
|
|
tray = "TrayIcon_"
|
|
}
|
|
return tray + 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
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
status = -1
|
|
return
|
|
}
|
|
zp, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
|
if err != nil {
|
|
status = -1
|
|
return
|
|
}
|
|
for _, zf := range zp.File {
|
|
if zf.Name != ud.binary {
|
|
continue
|
|
}
|
|
var file string
|
|
file, err = os.Executable()
|
|
if err != nil {
|
|
return
|
|
}
|
|
var path string
|
|
path, err = filepath.EvalSymlinks(file)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var info fs.FileInfo
|
|
info, err = os.Stat(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
mode := info.Mode()
|
|
var unzippedFile io.ReadCloser
|
|
unzippedFile, err = zf.Open()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer unzippedFile.Close()
|
|
var f *os.File
|
|
f, err = os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer f.Close()
|
|
_, err = io.Copy(f, unzippedFile)
|
|
if err != nil {
|
|
return
|
|
}
|
|
applyUpdate = func() error {
|
|
oldName := path + "-" + version + "-" + commit
|
|
err := os.Rename(path, oldName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.Rename(path+"_", path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.Remove(oldName)
|
|
}
|
|
return
|
|
}
|
|
// gz, err := gzip.NewReader(resp.Body)
|
|
// if err != nil {
|
|
// status = -1
|
|
// return
|
|
// }
|
|
// defer gz.Close()
|
|
// 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 {
|
|
// var file string
|
|
// file, err = os.Executable()
|
|
// if err != nil {
|
|
// return
|
|
// }
|
|
// var path string
|
|
// path, err = filepath.EvalSymlinks(file)
|
|
// if err != nil {
|
|
// return
|
|
// }
|
|
// var info fs.FileInfo
|
|
// info, err = os.Stat(path)
|
|
// if err != nil {
|
|
// return
|
|
// }
|
|
// mode := info.Mode()
|
|
// var f *os.File
|
|
// f, err = os.OpenFile(path+"_", os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
|
|
// if err != nil {
|
|
// return
|
|
// }
|
|
// defer f.Close()
|
|
// _, err = io.Copy(f, tarReader)
|
|
// if err != nil {
|
|
// return
|
|
// }
|
|
// applyUpdate = func() error {
|
|
// oldName := path + "-" + version + "-" + commit
|
|
// err := os.Rename(path, oldName)
|
|
// if err != nil {
|
|
// return err
|
|
// }
|
|
// err = os.Rename(path+"_", path)
|
|
// if err != nil {
|
|
// return err
|
|
// }
|
|
// return os.Remove(oldName)
|
|
// }
|
|
// 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 err != nil && strings.Contains(err.Error(), "strconv.ParseInt") {
|
|
app.err.Println("No new updates available.")
|
|
} else if status != -1 { // -1 means updates disabled, we don't need to log it.
|
|
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)
|
|
}
|
|
}
|