mirror of https://github.com/hrfee/jfa-go
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
504 lines
12 KiB
504 lines
12 KiB
package main
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"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"
|
|
)
|
|
|
|
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
|
|
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)
|
|
if tag.Version == "" {
|
|
err = errors.New("Tag was empty")
|
|
}
|
|
return tag, resp.StatusCode, err
|
|
}
|
|
|
|
func (t *Tag) IsNew() bool {
|
|
return t.Version[:7] != commit
|
|
}
|
|
|
|
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
|
|
}
|
|
defer resp.Body.Close()
|
|
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)
|
|
}
|
|
}
|