siyuan/kernel/api/system.go

724 lines
16 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 <https://www.gnu.org/licenses/>.
package api
import (
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/88250/gulu"
"github.com/88250/lute"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/conf"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func getNetwork(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
maskedConf, err := model.GetMaskedConf()
if err != nil {
ret.Code = -1
ret.Msg = "get conf failed: " + err.Error()
return
}
ret.Data = map[string]interface{}{
"proxy": maskedConf.System.NetworkProxy,
}
}
func getChangelog(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
data := map[string]interface{}{"show": false, "html": ""}
ret.Data = data
changelogsDir := filepath.Join(util.WorkingDir, "changelogs")
if !gulu.File.IsDir(changelogsDir) {
return
}
if !model.Conf.ShowChangelog {
return
}
changelogPath := filepath.Join(changelogsDir, "v"+util.Ver, "v"+util.Ver+"_"+model.Conf.Lang+".md")
if !gulu.File.IsExist(changelogPath) {
changelogPath = filepath.Join(changelogsDir, "v"+util.Ver, "v"+util.Ver+".md")
if !gulu.File.IsExist(changelogPath) {
logging.LogErrorf("changelog not found: %s", changelogPath)
return
}
}
contentData, err := os.ReadFile(changelogPath)
if err != nil {
logging.LogErrorf("read changelog failed: %s", err)
return
}
model.Conf.ShowChangelog = false
luteEngine := lute.New()
htmlContent := luteEngine.MarkdownStr("", string(contentData))
htmlContent = util.LinkTarget(htmlContent, "")
data["show"] = true
data["html"] = htmlContent
ret.Data = data
}
func getEmojiConf(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
builtConfPath := filepath.Join(util.AppearancePath, "emojis", "conf.json")
data, err := os.ReadFile(builtConfPath)
if err != nil {
logging.LogErrorf("read emojis conf.json failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
var conf []map[string]interface{}
if err = gulu.JSON.UnmarshalJSON(data, &conf); err != nil {
logging.LogErrorf("unmarshal emojis conf.json failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
customConfDir := filepath.Join(util.DataDir, "emojis")
custom := map[string]interface{}{
"id": "custom",
"title": "Custom",
"title_zh_cn": "自定义",
"title_ja_jp": "カスタム",
}
items := []map[string]interface{}{}
custom["items"] = items
if gulu.File.IsDir(customConfDir) {
model.CustomEmojis = sync.Map{}
customEmojis, err := os.ReadDir(customConfDir)
if err != nil {
logging.LogErrorf("read custom emojis failed: %s", err)
} else {
for _, customEmoji := range customEmojis {
name := customEmoji.Name()
if strings.HasPrefix(name, ".") {
continue
}
if customEmoji.IsDir() {
// 子级
subCustomEmojis, err := os.ReadDir(filepath.Join(customConfDir, name))
if err != nil {
logging.LogErrorf("read custom emojis failed: %s", err)
continue
}
for _, subCustomEmoji := range subCustomEmojis {
if subCustomEmoji.IsDir() {
continue
}
name = subCustomEmoji.Name()
if strings.HasPrefix(name, ".") {
continue
}
addCustomEmoji(customEmoji.Name()+"/"+name, &items)
}
continue
}
addCustomEmoji(name, &items)
}
}
}
custom["items"] = items
conf = append([]map[string]interface{}{custom}, conf...)
ret.Data = conf
return
}
func addCustomEmoji(name string, items *[]map[string]interface{}) {
ext := filepath.Ext(name)
nameWithoutExt := strings.TrimSuffix(name, ext)
emoji := map[string]interface{}{
"unicode": name,
"description": nameWithoutExt,
"description_zh_cn": nameWithoutExt,
"description_ja_jp": nameWithoutExt,
"keywords": nameWithoutExt,
}
*items = append(*items, emoji)
imgSrc := "/emojis/" + name
model.CustomEmojis.Store(nameWithoutExt, imgSrc)
}
func checkUpdate(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
showMsg := arg["showMsg"].(bool)
model.CheckUpdate(showMsg)
}
func exportLog(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
zipPath := model.ExportSystemLog()
ret.Data = map[string]interface{}{
"zip": zipPath,
}
}
func exportConf(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
logging.LogInfof("exporting conf...")
name := "siyuan-conf-" + time.Now().Format("20060102150405") + ".json"
tmpDir := filepath.Join(util.TempDir, "export")
if err := os.MkdirAll(tmpDir, 0755); err != nil {
logging.LogErrorf("export conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
clonedConf := &model.AppConf{}
if err := copier.CopyWithOption(clonedConf, model.Conf, copier.Option{IgnoreEmpty: false, DeepCopy: true}); err != nil {
logging.LogErrorf("export conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
if nil != clonedConf.Appearance {
clonedConf.Appearance.DarkThemes = nil
clonedConf.Appearance.LightThemes = nil
clonedConf.Appearance.Icons = nil
}
if nil != clonedConf.Editor {
clonedConf.Editor.Emoji = []string{}
}
if nil != clonedConf.Export {
clonedConf.Export.PandocBin = ""
}
clonedConf.UserData = ""
clonedConf.Account = nil
clonedConf.AccessAuthCode = ""
if nil != clonedConf.System {
clonedConf.System.ID = ""
clonedConf.System.Name = ""
clonedConf.System.OSPlatform = ""
clonedConf.System.Container = ""
clonedConf.System.IsMicrosoftStore = false
clonedConf.System.IsInsider = false
}
clonedConf.Sync = nil
clonedConf.Stat = nil
clonedConf.Api = nil
clonedConf.Repo = nil
clonedConf.Publish = nil
clonedConf.CloudRegion = 0
clonedConf.DataIndexState = 0
data, err := gulu.JSON.MarshalIndentJSON(clonedConf, "", " ")
if err != nil {
logging.LogErrorf("export conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
tmp := filepath.Join(tmpDir, name)
if err = os.WriteFile(tmp, data, 0644); err != nil {
logging.LogErrorf("export conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
zipFile, err := gulu.Zip.Create(tmp + ".zip")
if err != nil {
logging.LogErrorf("export conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
if err = zipFile.AddEntry(name, tmp); err != nil {
logging.LogErrorf("export conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
if err = zipFile.Close(); err != nil {
logging.LogErrorf("export conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
logging.LogInfof("exported conf")
zipPath := "/export/" + name + ".zip"
ret.Data = map[string]interface{}{
"name": name,
"zip": zipPath,
}
}
func importConf(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(200, ret)
logging.LogInfof("importing conf...")
form, err := c.MultipartForm()
if err != nil {
logging.LogErrorf("read upload file failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
files := form.File["file"]
if 1 != len(files) {
ret.Code = -1
ret.Msg = "invalid upload file"
return
}
f := files[0]
fh, err := f.Open()
if err != nil {
logging.LogErrorf("read upload file failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
data, err := io.ReadAll(fh)
fh.Close()
if err != nil {
logging.LogErrorf("read upload file failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
importDir := filepath.Join(util.TempDir, "import")
if err = os.MkdirAll(importDir, 0755); err != nil {
logging.LogErrorf("import conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
tmp := filepath.Join(importDir, f.Filename)
if err = os.WriteFile(tmp, data, 0644); err != nil {
logging.LogErrorf("import conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
tmpDir := filepath.Join(importDir, "conf")
if err = gulu.Zip.Unzip(tmp, tmpDir); err != nil {
logging.LogErrorf("import conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
entries, err := os.ReadDir(tmpDir)
if err != nil {
logging.LogErrorf("import conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
if 1 != len(entries) {
logging.LogErrorf("invalid conf package")
ret.Code = -1
ret.Msg = "invalid conf package"
return
}
tmp = filepath.Join(tmpDir, entries[0].Name())
data, err = os.ReadFile(tmp)
if err != nil {
logging.LogErrorf("import conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
importedConf := model.NewAppConf()
if err = gulu.JSON.UnmarshalJSON(data, importedConf); err != nil {
logging.LogErrorf("import conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
if err = copier.CopyWithOption(model.Conf, importedConf, copier.Option{IgnoreEmpty: true, DeepCopy: true}); err != nil {
logging.LogErrorf("import conf failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
logging.LogInfof("imported conf")
}
func getConf(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
maskedConf, err := model.GetMaskedConf()
if err != nil {
ret.Code = -1
ret.Msg = "get conf failed: " + err.Error()
return
}
if !maskedConf.Sync.Enabled || (0 == maskedConf.Sync.Provider && !model.IsSubscriber()) {
maskedConf.Sync.Stat = model.Conf.Language(53)
}
// REF: https://github.com/siyuan-note/siyuan/issues/11364
role := model.GetGinContextRole(c)
if model.IsReadOnlyRole(role) {
maskedConf.ReadOnly = true
}
if !model.IsValidRole(role, []model.Role{
model.RoleAdministrator,
}) {
model.HideConfSecret(maskedConf)
}
ret.Data = map[string]interface{}{
"conf": maskedConf,
"start": !util.IsUILoaded,
}
}
func setUILayout(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
if util.ReadOnly {
return
}
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
param, err := gulu.JSON.MarshalJSON(arg["layout"])
if err != nil {
ret.Code = -1
ret.Msg = err.Error()
return
}
uiLayout := &conf.UILayout{}
if err = gulu.JSON.UnmarshalJSON(param, uiLayout); err != nil {
ret.Code = -1
ret.Msg = err.Error()
return
}
model.Conf.SetUILayout(uiLayout)
model.Conf.Save()
}
func setAPIToken(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
token := arg["token"].(string)
model.Conf.Api.Token = token
model.Conf.Save()
}
func setAccessAuthCode(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
aac := arg["accessAuthCode"].(string)
if model.MaskedAccessAuthCode == aac {
aac = model.Conf.AccessAuthCode
}
model.Conf.AccessAuthCode = aac
model.Conf.Save()
session := util.GetSession(c)
workspaceSession := util.GetWorkspaceSession(session)
workspaceSession.AccessAuthCode = aac
session.Save(c)
go func() {
time.Sleep(200 * time.Millisecond)
util.ReloadUI()
}()
return
}
func setFollowSystemLockScreen(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
lockScreenMode := int(arg["lockScreenMode"].(float64))
model.Conf.System.LockScreenMode = lockScreenMode
model.Conf.Save()
return
}
func getSysFonts(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
ret.Data = util.GetSysFonts(model.Conf.Lang)
}
func version(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
ret.Data = util.Ver
}
func currentTime(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
ret.Data = util.CurrentTimeMillis()
}
func bootProgress(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
progress, details := util.GetBootProgressDetails()
ret.Data = map[string]interface{}{"progress": progress, "details": details}
}
func setAppearanceMode(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
mode := int(arg["mode"].(float64))
model.Conf.Appearance.Mode = mode
if 0 == mode {
model.Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, model.Conf.Appearance.ThemeLight, "theme.js"))
} else {
model.Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, model.Conf.Appearance.ThemeDark, "theme.js"))
}
model.Conf.Save()
ret.Data = map[string]interface{}{
"appearance": model.Conf.Appearance,
}
}
func setNetworkServe(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
networkServe := arg["networkServe"].(bool)
model.Conf.System.NetworkServe = networkServe
model.Conf.Save()
util.PushMsg(model.Conf.Language(42), 1000*15)
time.Sleep(time.Second * 3)
}
func setGoogleAnalytics(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
googleAnalytics := arg["googleAnalytics"].(bool)
model.Conf.System.DisableGoogleAnalytics = !googleAnalytics
model.Conf.Save()
}
func setUploadErrLog(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
uploadErrLog := arg["uploadErrLog"].(bool)
model.Conf.System.UploadErrLog = uploadErrLog
model.Conf.Save()
util.PushMsg(model.Conf.Language(42), 1000*15)
time.Sleep(time.Second * 3)
}
func setAutoLaunch(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
autoLaunch := int(arg["autoLaunch"].(float64))
model.Conf.System.AutoLaunch2 = autoLaunch
model.Conf.Save()
}
func setDownloadInstallPkg(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
downloadInstallPkg := arg["downloadInstallPkg"].(bool)
model.Conf.System.DownloadInstallPkg = downloadInstallPkg
model.Conf.Save()
}
func setNetworkProxy(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
scheme := arg["scheme"].(string)
host := arg["host"].(string)
port := arg["port"].(string)
model.Conf.System.NetworkProxy = &conf.NetworkProxy{
Scheme: scheme,
Host: host,
Port: port,
}
model.Conf.Save()
proxyURL := model.Conf.System.NetworkProxy.String()
util.SetNetworkProxy(proxyURL)
util.PushMsg(model.Conf.Language(102), 3000)
}
func addUIProcess(c *gin.Context) {
pid := c.Query("pid")
util.UIProcessIDs.Store(pid, true)
}
func exit(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
forceArg := arg["force"]
var force bool
if nil != forceArg {
force = forceArg.(bool)
}
execInstallPkgArg := arg["execInstallPkg"] // 0默认检查新版本1不执行新版本安装2执行新版本安装
execInstallPkg := 0
if nil != execInstallPkgArg {
execInstallPkg = int(execInstallPkgArg.(float64))
}
exitCode := model.Close(force, true, execInstallPkg)
ret.Code = exitCode
switch exitCode {
case 0:
case 1: // 同步执行失败
ret.Msg = model.Conf.Language(96) + "<div class=\"fn__space\"></div><button class=\"b3-button b3-button--white\">" + model.Conf.Language(97) + "</button>"
ret.Data = map[string]interface{}{"closeTimeout": 0}
case 2: // 提示新安装包
ret.Msg = model.Conf.Language(61)
ret.Data = map[string]interface{}{"closeTimeout": 0}
}
}