// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
package model
import (
"bufio"
"crypto/sha256"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"github.com/88250/gulu"
"github.com/imroc/req/v3"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/util"
"golang.org/x/mod/semver"
)
func execNewVerInstallPkg(newVerInstallPkgPath string) {
logging.LogInfof("installing the new version [%s]", newVerInstallPkgPath)
var cmd *exec.Cmd
if gulu.OS.IsWindows() {
cmd = exec.Command(newVerInstallPkgPath)
} else if gulu.OS.IsDarwin() {
exec.Command("chmod", "+x", newVerInstallPkgPath).CombinedOutput()
cmd = exec.Command("open", newVerInstallPkgPath)
} else {
logging.LogErrorf("unsupported platform for auto-installing package")
return
}
gulu.CmdAttr(cmd)
cmdErr := cmd.Run()
if nil != cmdErr {
logging.LogErrorf("exec install new version failed: %s", cmdErr)
return
}
}
func getNewVerInstallPkgPath() string {
if skipNewVerInstallPkg() {
return ""
}
downloadPkgURLs, checksum, err := getUpdatePkg()
if err != nil {
return ""
}
pkg := path.Base(downloadPkgURLs[0])
pkgPath := filepath.Join(util.TempDir, "install", pkg)
localChecksum, _ := sha256Hash(pkgPath)
if checksum != localChecksum {
return ""
}
return pkgPath
}
var checkDownloadInstallPkgLock = sync.Mutex{}
func checkDownloadInstallPkg() {
defer logging.Recover()
if skipNewVerInstallPkg() {
return
}
if !checkDownloadInstallPkgLock.TryLock() {
return
}
defer checkDownloadInstallPkgLock.Unlock()
downloadPkgURLs, checksum, err := getUpdatePkg()
if err != nil {
return
}
existingPkgPath := getNewVerInstallPkgPath()
if "" != existingPkgPath {
// 存在经过 sha256Hash 检查的安装包
util.PushUpdateMsg("update-pkg-ready", Conf.Language(62), 15*1000)
return
}
util.PushUpdateMsg("update-pkg-downloading", Conf.Language(103), 1000*7)
success := false
for _, downloadPkgURL := range downloadPkgURLs {
err = downloadInstallPkg(downloadPkgURL, checksum)
if err == nil {
success = true
break
}
}
if success {
util.PushUpdateMsg("update-pkg-ready", Conf.Language(62), 15*1000)
} else {
util.PushUpdateMsg("update-pkg-downloading", Conf.Language(104), 7000)
}
}
func getUpdatePkg() (downloadPkgURLs []string, checksum string, err error) {
defer logging.Recover()
result, err := util.GetRhyResult(false)
if err != nil {
return
}
ver := result["ver"].(string)
if isVersionUpToDate(ver) {
err = fmt.Errorf("version is up to date")
return
}
var suffix string
if gulu.OS.IsWindows() {
if "arm64" == runtime.GOARCH {
suffix = "win-arm64.exe"
} else {
suffix = "win.exe"
}
} else if gulu.OS.IsDarwin() {
if "arm64" == runtime.GOARCH {
suffix = "mac-arm64.dmg"
} else {
suffix = "mac.dmg"
}
}
pkg := "siyuan-" + ver + "-" + suffix
b3logURL := "https://release.b3log.org/siyuan/" + pkg
liuyunURL := "https://release.liuyun.io/siyuan/" + pkg
githubURL := "https://github.com/siyuan-note/siyuan/releases/download/v" + ver + "/" + pkg
ghproxyURL := "https://ghfast.top/" + githubURL
if util.IsChinaCloud() {
downloadPkgURLs = append(downloadPkgURLs, b3logURL)
downloadPkgURLs = append(downloadPkgURLs, liuyunURL)
downloadPkgURLs = append(downloadPkgURLs, ghproxyURL)
downloadPkgURLs = append(downloadPkgURLs, githubURL)
} else {
downloadPkgURLs = append(downloadPkgURLs, b3logURL)
downloadPkgURLs = append(downloadPkgURLs, liuyunURL)
downloadPkgURLs = append(downloadPkgURLs, githubURL)
downloadPkgURLs = append(downloadPkgURLs, ghproxyURL)
}
checksums := result["checksums"].(map[string]interface{})
checksum = checksums[pkg].(string)
if "" == checksum {
err = fmt.Errorf("checksum is empty")
return
}
return
}
func downloadInstallPkg(pkgURL, checksum string) (err error) {
if "" == pkgURL || "" == checksum {
return
}
pkg := path.Base(pkgURL)
savePath := filepath.Join(util.TempDir, "install", pkg)
if gulu.File.IsExist(savePath) {
localChecksum, _ := sha256Hash(savePath)
if localChecksum == checksum {
return
}
}
err = os.MkdirAll(filepath.Join(util.TempDir, "install"), 0755)
if err != nil {
logging.LogErrorf("create temp install dir failed: %s", err)
return
}
logging.LogInfof("downloading install package [%s]", pkgURL)
client := req.C().SetTLSHandshakeTimeout(7 * time.Second).SetTimeout(10 * time.Minute).DisableInsecureSkipVerify()
callback := func(info req.DownloadInfo) {
progress := fmt.Sprintf("%.2f%%", float64(info.DownloadedSize)/float64(info.Response.ContentLength)*100.0)
// logging.LogDebugf("downloading install package [%s %s]", pkgURL, progress)
util.PushStatusBar(fmt.Sprintf(Conf.Language(133), progress))
}
_, err = client.R().SetOutputFile(savePath).SetDownloadCallbackWithInterval(callback, 1*time.Second).Get(pkgURL)
if err != nil {
logging.LogErrorf("download install package [%s] failed: %s", pkgURL, err)
return
}
localChecksum, _ := sha256Hash(savePath)
if checksum != localChecksum {
logging.LogErrorf("verify checksum failed, download install package [%s] checksum [%s] not equal to downloaded [%s] checksum [%s]", pkgURL, checksum, savePath, localChecksum)
return
}
logging.LogInfof("downloaded install package [%s] to [%s]", pkgURL, savePath)
util.PushStatusBar(Conf.Language(62))
return
}
func sha256Hash(filename string) (ret string, err error) {
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close()
hash := sha256.New()
reader := bufio.NewReader(file)
buf := make([]byte, 1024*1024*4)
for {
switch n, readErr := reader.Read(buf); readErr {
case nil:
hash.Write(buf[:n])
case io.EOF:
return fmt.Sprintf("%x", hash.Sum(nil)), nil
default:
return "", err
}
}
}
type Announcement struct {
Id string `json:"id"`
Title string `json:"title"`
URL string `json:"url"`
Region int `json:"region"`
}
func getAnnouncements() (ret []*Announcement) {
result, err := util.GetRhyResult(false)
if err != nil {
logging.LogErrorf("get announcement failed: %s", err)
return
}
if nil == result["announcement"] {
return
}
announcements := result["announcement"].([]interface{})
for _, announcement := range announcements {
ann := announcement.(map[string]interface{})
ret = append(ret, &Announcement{
Id: ann["id"].(string),
Title: ann["title"].(string),
URL: ann["url"].(string),
Region: int(ann["region"].(float64)),
})
}
return
}
func CheckUpdate(showMsg bool) {
if !showMsg {
return
}
if Conf.System.IsMicrosoftStore {
return
}
result, err := util.GetRhyResult(showMsg)
if err != nil {
return
}
ver := result["ver"].(string)
releaseLang := result["release"].(string)
if releaseLangArg := result["release_"+Conf.Lang]; nil != releaseLangArg {
releaseLang = releaseLangArg.(string)
}
if isVersionUpToDate(ver) {
util.PushUpdateMsg("update-notify", Conf.Language(10), 3000)
} else {
util.PushUpdateMsg("update-notify", fmt.Sprintf(Conf.Language(9), ""+releaseLang+""), 15000)
}
go func() {
defer logging.Recover()
checkDownloadInstallPkg()
}()
}
func isVersionUpToDate(releaseVer string) bool {
return semver.Compare("v"+releaseVer, "v"+util.Ver) <= 0
}
// skipInstallPkgPlatformCached 缓存平台相关判断,-1 未初始化,0 表示不跳过,1 表示跳过
var skipInstallPkgPlatformCached = -1
func skipNewVerInstallPkg() bool {
if skipInstallPkgPlatformCached == -1 {
skipInstallPkgPlatformCached = 0
if !gulu.OS.IsWindows() && !gulu.OS.IsDarwin() {
skipInstallPkgPlatformCached = 1
} else if util.ISMicrosoftStore || util.ContainerStd != util.Container {
skipInstallPkgPlatformCached = 1
} else if gulu.OS.IsWindows() {
plat := strings.ToLower(Conf.System.OSPlatform)
// Windows 7, 8 and Server 2012 are no longer supported https://github.com/siyuan-note/siyuan/issues/7347
if strings.Contains(plat, " 7 ") || strings.Contains(plat, " 8 ") || strings.Contains(plat, "2012") {
skipInstallPkgPlatformCached = 1
}
}
}
if skipInstallPkgPlatformCached == 1 || !Conf.System.DownloadInstallPkg {
return true
}
return false
}