This commit is contained in:
Liang Ding 2022-05-26 15:18:53 +08:00
parent e650b8100c
commit f40ed985e1
No known key found for this signature in database
GPG key ID: 136F30F901A2231D
1214 changed files with 345766 additions and 9 deletions

92
kernel/api/account.go Normal file
View file

@ -0,0 +1,92 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func useActivationcode(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
code := arg["data"].(string)
err := model.UseActivationcode(code)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func checkActivationcode(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
code := arg["data"].(string)
ret.Code, ret.Msg = model.CheckActivationcode(code)
}
func deactivateUser(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
err := model.DeactivateUser()
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func login(c *gin.Context) {
ret := gulu.Ret.NewResult()
ret.Code = -1
arg := map[string]interface{}{}
if err := c.BindJSON(&arg); nil != err {
ret.Code = -1
ret.Msg = "parses request failed"
c.JSON(http.StatusOK, ret)
return
}
name := arg["userName"].(string)
password := arg["userPassword"].(string)
captcha := arg["captcha"].(string)
result, err := model.Login(name, password, captcha)
if nil != err {
return
}
c.JSON(http.StatusOK, result)
}

223
kernel/api/asset.go Normal file
View file

@ -0,0 +1,223 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"os"
"path/filepath"
"strings"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func getDocImageAssets(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
assets, err := model.DocImageAssets(id)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = assets
}
func setFileAnnotation(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
p := arg["path"].(string)
p = strings.ReplaceAll(p, "%23", "#")
data := arg["data"].(string)
writePath, err := resolveFileAnnotationAbsPath(p)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
if err := gulu.File.WriteFileSafer(writePath, []byte(data), 0644); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
model.IncWorkspaceDataVer()
}
func getFileAnnotation(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
p := arg["path"].(string)
p = strings.ReplaceAll(p, "%23", "#")
readPath, err := resolveFileAnnotationAbsPath(p)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
if !gulu.File.IsExist(readPath) {
ret.Code = 1
return
}
data, err := os.ReadFile(readPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"data": string(data),
}
}
func resolveFileAnnotationAbsPath(assetRelPath string) (ret string, err error) {
filePath := strings.TrimSuffix(assetRelPath, ".sya")
absPath, err := model.GetAssetAbsPath(filePath)
if nil != err {
return
}
dir := filepath.Dir(absPath)
base := filepath.Base(assetRelPath)
ret = filepath.Join(dir, base)
return
}
func removeUnusedAsset(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
p := arg["path"].(string)
asset := model.RemoveUnusedAsset(p)
ret.Data = map[string]interface{}{
"path": asset,
}
}
func removeUnusedAssets(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
paths := model.RemoveUnusedAssets()
ret.Data = map[string]interface{}{
"paths": paths,
}
}
func getUnusedAssets(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
unusedAssets := model.UnusedAssets()
ret.Data = map[string]interface{}{
"unusedAssets": unusedAssets,
}
}
func resolveAssetPath(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
path := arg["path"].(string)
p, err := model.GetAssetAbsPath(path)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 3000}
return
}
ret.Data = p
return
}
func uploadCloud(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
rootID := arg["id"].(string)
err := model.UploadAssets2Cloud(rootID)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 3000}
} else {
util.PushMsg(model.Conf.Language(41), 3000)
}
}
func insertLocalAssets(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
assetPathsArg := arg["assetPaths"].([]interface{})
var assetPaths []string
for _, pathArg := range assetPathsArg {
assetPaths = append(assetPaths, pathArg.(string))
}
id := arg["id"].(string)
succMap, err := model.InsertLocalAssets(id, assetPaths)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"succMap": succMap,
}
}

92
kernel/api/attr.go Normal file
View file

@ -0,0 +1,92 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func getBookmarkLabels(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
ret.Data = model.BookmarkLabels()
}
func getBlockAttrs(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
ret.Data = model.GetBlockAttrs(id)
}
func setBlockAttrs(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
attrs := arg["attrs"].(map[string]interface{})
nameValues := map[string]string{}
for name, value := range attrs {
nameValues[name] = value.(string)
}
err := model.SetBlockAttrs(id, nameValues)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func resetBlockAttrs(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
attrs := arg["attrs"].(map[string]interface{})
nameValues := map[string]string{}
for name, value := range attrs {
nameValues[name] = value.(string)
}
err := model.ResetBlockAttrs(id, nameValues)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}

129
kernel/api/backup.go Normal file
View file

@ -0,0 +1,129 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/dustin/go-humanize"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func removeCloudBackup(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
err := model.RemoveCloudBackup()
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func downloadCloudBackup(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
err := model.DownloadBackup()
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func uploadLocalBackup(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
err := model.UploadBackup()
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func recoverLocalBackup(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
err := model.RecoverLocalBackup()
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func createLocalBackup(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
err := model.CreateLocalBackup()
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func getLocalBackup(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
backup, err := model.GetLocalBackup()
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"backup": backup,
}
}
func getCloudSpace(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
sync, backup, size, assetSize, totalSize, err := model.GetCloudSpace()
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
util.PushErrMsg(err.Error(), 3000)
return
}
hTrafficUploadSize := humanize.Bytes(uint64(model.Conf.User.UserTrafficUpload))
hTrafficDownloadSize := humanize.Bytes(uint64(model.Conf.User.UserTrafficDownload))
ret.Data = map[string]interface{}{
"sync": sync,
"backup": backup,
"hAssetSize": assetSize,
"hSize": size,
"hTotalSize": totalSize,
"hTrafficUploadSize": hTrafficUploadSize,
"hTrafficDownloadSize": hTrafficDownloadSize,
}
}

276
kernel/api/bazaar.go Normal file
View file

@ -0,0 +1,276 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func getBazaarPackageREAME(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
repoURL := arg["repoURL"].(string)
repoHash := arg["repoHash"].(string)
ret.Data = map[string]interface{}{
"html": model.GetPackageREADME(repoURL, repoHash),
}
}
func getBazaarWidget(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
ret.Data = map[string]interface{}{
"packages": model.BazaarWidgets(),
}
}
func installBazaarWidget(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
repoURL := arg["repoURL"].(string)
repoHash := arg["repoHash"].(string)
packageName := arg["packageName"].(string)
err := model.InstallBazaarWidget(repoURL, repoHash, packageName)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
util.PushMsg(model.Conf.Language(69), 3000)
ret.Data = map[string]interface{}{
"packages": model.BazaarWidgets(),
}
}
func uninstallBazaarWidget(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
packageName := arg["packageName"].(string)
err := model.UninstallBazaarWidget(packageName)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"packages": model.BazaarWidgets(),
}
}
func getBazaarIcon(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
ret.Data = map[string]interface{}{
"packages": model.BazaarIcons(),
}
}
func installBazaarIcon(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
repoURL := arg["repoURL"].(string)
repoHash := arg["repoHash"].(string)
packageName := arg["packageName"].(string)
err := model.InstallBazaarIcon(repoURL, repoHash, packageName)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
util.PushMsg(model.Conf.Language(69), 3000)
ret.Data = map[string]interface{}{
"packages": model.BazaarIcons(),
"appearance": model.Conf.Appearance,
}
}
func uninstallBazaarIcon(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
packageName := arg["packageName"].(string)
err := model.UninstallBazaarIcon(packageName)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"packages": model.BazaarIcons(),
"appearance": model.Conf.Appearance,
}
}
func getBazaarTemplate(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
ret.Data = map[string]interface{}{
"packages": model.BazaarTemplates(),
}
}
func installBazaarTemplate(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
repoURL := arg["repoURL"].(string)
repoHash := arg["repoHash"].(string)
packageName := arg["packageName"].(string)
err := model.InstallBazaarTemplate(repoURL, repoHash, packageName)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"packages": model.BazaarTemplates(),
}
util.PushMsg(model.Conf.Language(69), 3000)
}
func uninstallBazaarTemplate(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
packageName := arg["packageName"].(string)
err := model.UninstallBazaarTemplate(packageName)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"packages": model.BazaarTemplates(),
}
}
func getBazaarTheme(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
ret.Data = map[string]interface{}{
"packages": model.BazaarThemes(),
}
}
func installBazaarTheme(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
repoURL := arg["repoURL"].(string)
repoHash := arg["repoHash"].(string)
packageName := arg["packageName"].(string)
mode := arg["mode"].(float64)
update := false
if nil != arg["update"] {
update = arg["update"].(bool)
}
err := model.InstallBazaarTheme(repoURL, repoHash, packageName, int(mode), update)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
util.PushMsg(model.Conf.Language(69), 3000)
ret.Data = map[string]interface{}{
"packages": model.BazaarThemes(),
"appearance": model.Conf.Appearance,
}
}
func uninstallBazaarTheme(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
packageName := arg["packageName"].(string)
err := model.UninstallBazaarTheme(packageName)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"packages": model.BazaarThemes(),
"appearance": model.Conf.Appearance,
}
}

282
kernel/api/block.go Normal file
View file

@ -0,0 +1,282 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"fmt"
"net/http"
"github.com/88250/gulu"
"github.com/88250/lute/html"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/util"
)
func setBlockReminder(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
timed := arg["timed"].(string) // yyyyMMddHHmmss
err := model.SetBlockReminder(id, timed)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 7000}
return
}
}
func checkBlockFold(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
ret.Data = sql.IsBlockFolded(id)
}
func checkBlockExist(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
b, err := model.GetBlock(id)
if filesys.ErrUnableLockFile == err {
ret.Code = 2
ret.Data = id
return
}
ret.Data = nil != b
}
func getDocInfo(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
info := model.GetDocInfo(id)
ret.Data = info
}
func getRecentUpdatedBlocks(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
blocks := model.RecentUpdatedBlocks()
ret.Data = blocks
}
func getBlockWordCount(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
blockRuneCount, blockWordCount, rootBlockRuneCount, rootBlockWordCount := model.BlockWordCount(id)
ret.Data = map[string]interface{}{
"blockRuneCount": blockRuneCount,
"blockWordCount": blockWordCount,
"rootBlockRuneCount": rootBlockRuneCount,
"rootBlockWordCount": rootBlockWordCount,
}
}
func getRefText(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
ret.Data = model.GetBlockRefText(id)
}
func getRefIDs(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
refIDs, refTexts, defIDs := model.GetBlockRefIDs(id)
ret.Data = map[string][]string{
"refIDs": refIDs,
"refTexts": refTexts,
"defIDs": defIDs,
}
}
func getRefIDsByFileAnnotationID(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
refIDs, refTexts := model.GetBlockRefIDsByFileAnnotationID(id)
ret.Data = map[string][]string{
"refIDs": refIDs,
"refTexts": refTexts,
}
}
func getBlockDefIDsByRefText(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
anchor := arg["anchor"].(string)
excludeIDsArg := arg["excludeIDs"].([]interface{})
var excludeIDs []string
for _, excludeID := range excludeIDsArg {
excludeIDs = append(excludeIDs, excludeID.(string))
}
excludeIDs = nil // 不限制虚拟引用搜索自己 https://ld246.com/article/1633243424177
ids := model.GetBlockDefIDsByRefText(anchor, excludeIDs)
ret.Data = ids
}
func getBlockBreadcrumb(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
blockPath, err := model.BuildBlockBreadcrumb(id)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = blockPath
}
func getBlockInfo(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
block, err := model.GetBlock(id)
if filesys.ErrUnableLockFile == err {
ret.Code = 2
ret.Data = id
return
}
if nil == block {
ret.Code = 1
ret.Msg = fmt.Sprintf(model.Conf.Language(15), id)
return
}
var rootChildID string
b := block
for i := 0; i < 128; i++ {
parentID := b.ParentID
if "" == parentID {
rootChildID = b.ID
break
}
if b, _ = model.GetBlock(parentID); nil == b {
util.LogErrorf("not found parent")
break
}
}
root, err := model.GetBlock(block.RootID)
if filesys.ErrUnableLockFile == err {
ret.Code = 2
ret.Data = id
return
}
rootTitle := root.IAL["title"]
rootTitle = html.UnescapeString(rootTitle)
ret.Data = map[string]string{
"box": block.Box,
"path": block.Path,
"rootID": block.RootID,
"rootTitle": rootTitle,
"rootChildID": rootChildID,
"rootIcon": root.IAL["icon"],
}
}
func getBlockDOM(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
dom := model.GetBlockDOM(id)
ret.Data = map[string]string{
"id": id,
"dom": dom,
}
}

301
kernel/api/block_op.go Normal file
View file

@ -0,0 +1,301 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/88250/lute"
"github.com/88250/lute/ast"
"github.com/88250/protyle"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func appendBlock(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
data := arg["data"].(string)
dataType := arg["dataType"].(string)
parentID := arg["parentID"].(string)
if "markdown" == dataType {
luteEngine := model.NewLute()
data = dataBlockDOM(data, luteEngine)
}
transactions := []*model.Transaction{
{
DoOperations: []*model.Operation{
{
Action: "appendInsert",
Data: data,
ParentID: parentID,
},
},
},
}
err := model.PerformTransactions(&transactions)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
model.WaitForWritingFiles()
ret.Data = transactions
broadcastTransactions(transactions)
}
func prependBlock(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
data := arg["data"].(string)
dataType := arg["dataType"].(string)
parentID := arg["parentID"].(string)
if "markdown" == dataType {
luteEngine := model.NewLute()
data = dataBlockDOM(data, luteEngine)
}
transactions := []*model.Transaction{
{
DoOperations: []*model.Operation{
{
Action: "prependInsert",
Data: data,
ParentID: parentID,
},
},
},
}
err := model.PerformTransactions(&transactions)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
model.WaitForWritingFiles()
ret.Data = transactions
broadcastTransactions(transactions)
}
func insertBlock(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
data := arg["data"].(string)
dataType := arg["dataType"].(string)
var parentID, previousID string
if nil != arg["parentID"] {
parentID = arg["parentID"].(string)
}
if nil != arg["previousID"] {
previousID = arg["previousID"].(string)
}
if "markdown" == dataType {
luteEngine := model.NewLute()
data = dataBlockDOM(data, luteEngine)
}
transactions := []*model.Transaction{
{
DoOperations: []*model.Operation{
{
Action: "insert",
Data: data,
ParentID: parentID,
PreviousID: previousID,
},
},
},
}
err := model.PerformTransactions(&transactions)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
model.WaitForWritingFiles()
ret.Data = transactions
broadcastTransactions(transactions)
}
func updateBlock(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
data := arg["data"].(string)
dataType := arg["dataType"].(string)
id := arg["id"].(string)
luteEngine := model.NewLute()
if "markdown" == dataType {
data = dataBlockDOM(data, luteEngine)
}
tree := luteEngine.BlockDOM2Tree(data)
if nil == tree || nil == tree.Root || nil == tree.Root.FirstChild {
ret.Code = -1
ret.Msg = "parse tree failed"
return
}
block, err := model.GetBlock(id)
if nil != err {
ret.Code = -1
ret.Msg = "get block failed: " + err.Error()
return
}
var transactions []*model.Transaction
if "NodeDocument" == block.Type {
oldTree, err := model.LoadTree(block.Box, block.Path)
if nil != err {
ret.Code = -1
ret.Msg = "load tree failed: " + err.Error()
return
}
var toRemoves []*ast.Node
var ops []*model.Operation
for n := oldTree.Root.FirstChild; nil != n; n = n.Next {
toRemoves = append(toRemoves, n)
ops = append(ops, &model.Operation{Action: "delete", ID: n.ID})
}
for _, n := range toRemoves {
n.Unlink()
}
ops = append(ops, &model.Operation{Action: "appendInsert", Data: data, ParentID: id})
transactions = append(transactions, &model.Transaction{
DoOperations: ops,
})
} else {
if "NodeListItem" == block.Type {
// 使用 API `api/block/updateBlock` 更新列表项时渲染错误 https://github.com/siyuan-note/siyuan/issues/4658
tree.Root.AppendChild(tree.Root.FirstChild.FirstChild) // 将列表下的第一个列表项移到文档结尾,移动以后根下面直接挂列表项,渲染器可以正常工作
tree.Root.FirstChild.Unlink() // 删除列表
tree.Root.FirstChild.Unlink() // 继续删除列表 IAL
}
tree.Root.FirstChild.SetIALAttr("id", id)
data = luteEngine.Tree2BlockDOM(tree, luteEngine.RenderOptions)
transactions = []*model.Transaction{
{
DoOperations: []*model.Operation{
{
Action: "update",
ID: id,
Data: data,
},
},
},
}
}
err = model.PerformTransactions(&transactions)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
model.WaitForWritingFiles()
ret.Data = transactions
broadcastTransactions(transactions)
}
func deleteBlock(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
transactions := []*model.Transaction{
{
DoOperations: []*model.Operation{
{
Action: "delete",
ID: id,
},
},
},
}
err := model.PerformTransactions(&transactions)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
ret.Data = transactions
broadcastTransactions(transactions)
}
func broadcastTransactions(transactions []*model.Transaction) {
evt := util.NewCmdResult("transactions", 0, util.PushModeBroadcast, util.PushModeBroadcast)
evt.Data = transactions
util.PushEvent(evt)
}
func dataBlockDOM(data string, luteEngine *lute.Lute) (ret string) {
ret = luteEngine.Md2BlockDOM(data)
if "" == ret {
// 使用 API 插入空字符串出现错误 https://github.com/siyuan-note/siyuan/issues/3931
blankParagraph := protyle.NewParagraph()
ret = lute.RenderNodeBlockDOM(blankParagraph, luteEngine.ParseOptions, luteEngine.RenderOptions)
}
return
}

52
kernel/api/bookmark.go Normal file
View file

@ -0,0 +1,52 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func getBookmark(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
ret.Data = model.BuildBookmark()
}
func renameBookmark(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
oldBookmark := arg["oldBookmark"].(string)
newBookmark := arg["newBookmark"].(string)
if err := model.RenameBookmark(oldBookmark, newBookmark); nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
}

31
kernel/api/clipboard.go Normal file
View file

@ -0,0 +1,31 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"github.com/88250/clipboard"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
)
func readFilePaths(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(200, ret)
paths, _ := clipboard.ReadFilePaths()
ret.Data = paths
}

219
kernel/api/export.go Normal file
View file

@ -0,0 +1,219 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"path"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func exportDataInFolder(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
exportFolder := arg["folder"].(string)
err := model.ExportDataInFolder(exportFolder)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 7000}
return
}
}
func exportData(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
zipPath := model.ExportData()
ret.Data = map[string]interface{}{
"zip": zipPath,
}
}
func batchExportMd(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
p := arg["path"].(string)
zipPath := model.BatchExportMarkdown(notebook, p)
ret.Data = map[string]interface{}{
"name": path.Base(zipPath),
"zip": zipPath,
}
}
func exportMd(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
name, zipPath := model.ExportMarkdown(id)
ret.Data = map[string]interface{}{
"name": name,
"zip": zipPath,
}
}
func exportSY(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
name, zipPath := model.ExportSY(id)
ret.Data = map[string]interface{}{
"name": name,
"zip": zipPath,
}
}
func exportMdContent(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
hPath, content := model.ExportMarkdownContent(id)
ret.Data = map[string]interface{}{
"hPath": hPath,
"content": content,
}
}
func exportDocx(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
savePath := arg["savePath"].(string)
err := model.ExportDocx(id, savePath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 7000}
return
}
}
func exportMdHTML(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
savePath := arg["savePath"].(string)
name, content := model.ExportMarkdownHTML(id, savePath, false)
ret.Data = map[string]interface{}{
"id": id,
"name": name,
"content": content,
}
}
func exportHTML(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
pdf := arg["pdf"].(bool)
savePath := arg["savePath"].(string)
name, content := model.ExportHTML(id, savePath, pdf)
ret.Data = map[string]interface{}{
"id": id,
"name": name,
"content": content,
}
}
func addPDFOutline(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
path := arg["path"].(string)
err := model.AddPDFOutline(id, path)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func exportPreview(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
stdHTML := model.Preview(id)
ret.Data = map[string]interface{}{
"html": stdHTML,
}
}

135
kernel/api/extension.go Normal file
View file

@ -0,0 +1,135 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"bytes"
"io"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func extensionCopy(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(200, ret)
form, _ := c.MultipartForm()
dom := form.Value["dom"][0]
assets := filepath.Join(util.DataDir, "assets")
if notebookVal := form.Value["notebook"]; 0 < len(notebookVal) {
assets = filepath.Join(util.DataDir, notebookVal[0], "assets")
if !gulu.File.IsDir(assets) {
assets = filepath.Join(util.DataDir, "assets")
}
}
if err := os.MkdirAll(assets, 0755); nil != err {
util.LogErrorf("create assets folder [%s] failed: %s", assets, err)
ret.Msg = err.Error()
return
}
luteEngine := model.NewLute()
md := luteEngine.HTML2Md(dom)
md = strings.TrimSpace(md)
ret.Data = map[string]interface{}{
"md": md,
}
uploaded := map[string]string{}
for originalName, file := range form.File {
oName, err := url.PathUnescape(originalName)
if nil != err {
if strings.Contains(originalName, "%u") {
originalName = strings.ReplaceAll(originalName, "%u", "\\u")
originalName, err = strconv.Unquote("\"" + originalName + "\"")
if nil != err {
continue
}
oName, err = url.PathUnescape(originalName)
if nil != err {
continue
}
} else {
continue
}
}
u, _ := url.Parse(oName)
if "" == u.Path {
continue
}
fName := path.Base(u.Path)
fName = util.FilterUploadFileName(fName)
f, err := file[0].Open()
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
break
}
data, err := io.ReadAll(f)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
break
}
ext := path.Ext(fName)
fName = fName[0 : len(fName)-len(ext)]
if "" == ext && bytes.HasPrefix(data, []byte("<svg ")) && bytes.HasSuffix(data, []byte("</svg>")) {
ext = ".svg"
}
fName = fName + "-" + ast.NewNodeID() + ext
writePath := filepath.Join(assets, fName)
if err = gulu.File.WriteFileSafer(writePath, data, 0644); nil != err {
ret.Code = -1
ret.Msg = err.Error()
break
}
uploaded[oName] = "assets/" + fName
}
for k, v := range uploaded {
if "" == md {
// 复制单个图片的情况
md = "![](" + v + ")"
break
}
md = strings.ReplaceAll(md, "]("+k+")", "]("+v+")")
p, err := url.Parse(k)
if nil != err {
continue
}
md = strings.ReplaceAll(md, "]("+p.Path+")", "]("+v+")")
}
ret.Data = map[string]interface{}{
"md": md,
}
ret.Msg = model.Conf.Language(72)
}

147
kernel/api/file.go Normal file
View file

@ -0,0 +1,147 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"errors"
"fmt"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func getFile(c *gin.Context) {
ret := gulu.Ret.NewResult()
arg, ok := util.JsonArg(c, ret)
if !ok {
c.JSON(http.StatusOK, ret)
return
}
filePath := arg["path"].(string)
filePath = filepath.Join(util.WorkspaceDir, filePath)
info, err := os.Stat(filePath)
if os.IsNotExist(err) {
c.Status(404)
return
}
if nil != err {
util.LogErrorf("stat [%s] failed: %s", filePath, err)
c.Status(500)
return
}
if info.IsDir() {
util.LogErrorf("file [%s] is a directory", filePath)
c.Status(405)
return
}
if err = model.ServeFile(c, filePath); nil != err {
c.Status(http.StatusConflict)
return
}
}
func putFile(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
filePath := c.PostForm("path")
filePath = filepath.Join(util.WorkspaceDir, filePath)
isDirStr := c.PostForm("isDir")
isDir, _ := strconv.ParseBool(isDirStr)
var err error
if isDir {
err = os.MkdirAll(filePath, 0755)
if nil != err {
util.LogErrorf("make a dir [%s] failed: %s", filePath, err)
}
} else {
file, _ := c.FormFile("file")
if nil == file {
util.LogErrorf("form file is nil [path=%s]", filePath)
c.Status(400)
return
}
dir := filepath.Dir(filePath)
if err = os.MkdirAll(dir, 0755); nil != err {
util.LogErrorf("put a file [%s] make dir [%s] failed: %s", filePath, dir, err)
} else {
if filesys.IsLocked(filePath) {
msg := fmt.Sprintf("file [%s] is locked", filePath)
util.LogErrorf(msg)
err = errors.New(msg)
} else {
err = writeFile(file, filePath)
if nil != err {
util.LogErrorf("put a file [%s] failed: %s", filePath, err)
}
}
}
}
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
modTimeStr := c.PostForm("modTime")
modTimeInt, err := strconv.ParseInt(modTimeStr, 10, 64)
if nil != err {
util.LogErrorf("parse mod time [%s] failed: %s", modTimeStr, err)
c.Status(500)
return
}
modTime := millisecond2Time(modTimeInt)
if err = os.Chtimes(filePath, modTime, modTime); nil != err {
util.LogErrorf("change time failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func writeFile(file *multipart.FileHeader, dst string) error {
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
err = gulu.File.WriteFileSaferByReader(dst, src, 0644)
if nil != err {
return err
}
return nil
}
func millisecond2Time(t int64) time.Time {
sec := t / 1000
msec := t % 1000
return time.Unix(sec, msec*int64(time.Millisecond))
}

636
kernel/api/filetree.go Normal file
View file

@ -0,0 +1,636 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"fmt"
"net/http"
"path"
"regexp"
"strings"
"unicode/utf8"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func refreshFiletree(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
model.RefreshFileTree()
}
func doc2Heading(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
srcID := arg["srcID"].(string)
targetID := arg["targetID"].(string)
after := arg["after"].(bool)
srcTreeBox, srcTreePath, err := model.Doc2Heading(srcID, targetID, after)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
ret.Data = map[string]interface{}{
"srcTreeBox": srcTreeBox,
"srcTreePath": srcTreePath,
}
}
func heading2Doc(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
srcHeadingID := arg["srcHeadingID"].(string)
targetNotebook := arg["targetNoteBook"].(string)
targetPath := arg["targetPath"].(string)
srcRootBlockID, targetPath, err := model.Heading2Doc(srcHeadingID, targetNotebook, targetPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
model.WaitForWritingFiles()
tree, err := model.LoadTree(targetNotebook, targetPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
name := path.Base(targetPath)
box := model.Conf.Box(targetNotebook)
files, _, _ := model.ListDocTree(targetNotebook, path.Dir(targetPath), model.Conf.FileTree.Sort)
evt := util.NewCmdResult("heading2doc", 0, util.PushModeBroadcast, util.PushModeNone)
evt.Data = map[string]interface{}{
"box": box,
"path": targetPath,
"files": files,
"name": name,
"id": tree.Root.ID,
"srcRootBlockID": srcRootBlockID,
}
evt.Callback = arg["callback"]
util.PushEvent(evt)
}
func li2Doc(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
srcListItemID := arg["srcListItemID"].(string)
targetNotebook := arg["targetNoteBook"].(string)
targetPath := arg["targetPath"].(string)
srcRootBlockID, targetPath, err := model.ListItem2Doc(srcListItemID, targetNotebook, targetPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
model.WaitForWritingFiles()
tree, err := model.LoadTree(targetNotebook, targetPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
name := path.Base(targetPath)
box := model.Conf.Box(targetNotebook)
files, _, _ := model.ListDocTree(targetNotebook, path.Dir(targetPath), model.Conf.FileTree.Sort)
evt := util.NewCmdResult("li2doc", 0, util.PushModeBroadcast, util.PushModeNone)
evt.Data = map[string]interface{}{
"box": box,
"path": targetPath,
"files": files,
"name": name,
"id": tree.Root.ID,
"srcRootBlockID": srcRootBlockID,
}
evt.Callback = arg["callback"]
util.PushEvent(evt)
}
func getHPathByPath(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
p := arg["path"].(string)
hPath, err := model.GetHPathByPath(notebook, p)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = hPath
}
func getHPathByID(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
hPath, err := model.GetHPathByID(id)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = hPath
}
func getFullHPathByID(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
hPath, err := model.GetFullHPathByID(id)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = hPath
}
func moveDoc(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
fromNotebook := arg["fromNotebook"].(string)
toNotebook := arg["toNotebook"].(string)
fromPath := arg["fromPath"].(string)
toPath := arg["toPath"].(string)
newPath, err := model.MoveDoc(fromNotebook, fromPath, toNotebook, toPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 7000}
return
}
evt := util.NewCmdResult("moveDoc", 0, util.PushModeBroadcast, util.PushModeNone)
evt.Data = map[string]interface{}{
"fromNotebook": fromNotebook,
"toNotebook": toNotebook,
"fromPath": fromPath,
"toPath": toPath,
"newPath": newPath,
}
util.PushEvent(evt)
}
func removeDoc(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
p := arg["path"].(string)
err := model.RemoveDoc(notebook, p)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
evt := util.NewCmdResult("remove", 0, util.PushModeBroadcast, util.PushModeNone)
evt.Data = map[string]interface{}{
"box": notebook,
"path": p,
}
util.PushEvent(evt)
}
func renameDoc(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
p := arg["path"].(string)
title := arg["title"].(string)
err := model.RenameDoc(notebook, p, title)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
return
}
func duplicateDoc(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
err := model.DuplicateDoc(id)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 7000}
return
}
block, _ := model.GetBlock(id)
p := block.Path
notebook := block.Box
box := model.Conf.Box(notebook)
tree, err := model.LoadTree(box.ID, p)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
pushCreate(box, p, tree.Root.ID, arg)
}
func createDoc(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
p := arg["path"].(string)
title := arg["title"].(string)
md := arg["md"].(string)
err := model.CreateDocByMd(notebook, p, title, md)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 7000}
return
}
box := model.Conf.Box(notebook)
tree, err := model.LoadTree(box.ID, p)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
pushCreate(box, p, tree.Root.ID, arg)
}
func createDailyNote(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
p, err := model.CreateDailyNote(notebook)
if nil != err {
if model.ErrBoxNotFound == err {
ret.Code = 1
} else {
ret.Code = -1
}
ret.Msg = err.Error()
return
}
box := model.Conf.Box(notebook)
model.WaitForWritingFiles()
tree, err := model.LoadTree(box.ID, p)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
evt := util.NewCmdResult("createdailynote", 0, util.PushModeBroadcast, util.PushModeNone)
name := path.Base(p)
files, _, _ := model.ListDocTree(box.ID, path.Dir(p), model.Conf.FileTree.Sort)
evt.Data = map[string]interface{}{
"box": box,
"path": p,
"files": files,
"name": name,
"id": tree.Root.ID,
}
evt.Callback = arg["callback"]
util.PushEvent(evt)
}
func createDocWithMd(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
hPath := arg["path"].(string)
markdown := arg["markdown"].(string)
baseName := path.Base(hPath)
dir := path.Dir(hPath)
r, _ := regexp.Compile("\r\n|\r|\n|\u2028|\u2029|\t|/")
baseName = r.ReplaceAllString(baseName, "")
if 512 < utf8.RuneCountInString(baseName) {
baseName = gulu.Str.SubStr(baseName, 512)
}
hPath = path.Join(dir, baseName)
if !strings.HasPrefix(hPath, "/") {
hPath = "/" + hPath
}
id, err := model.CreateWithMarkdown(notebook, hPath, markdown)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = id
box := model.Conf.Box(notebook)
b, _ := model.GetBlock(id)
p := b.Path
pushCreate(box, p, id, arg)
}
func lockFile(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
locked, filePath := model.LockFileByBlockID(id)
if !locked {
ret.Code = -1
ret.Msg = fmt.Sprintf(model.Conf.Language(75), filePath)
ret.Data = map[string]interface{}{"closeTimeout": 5000}
}
}
func getDocNameTemplate(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
box := model.Conf.Box(notebook)
nameTemplate := model.Conf.FileTree.CreateDocNameTemplate
if nil != box {
nameTemplate = box.GetConf().CreateDocNameTemplate
}
if "" == nameTemplate {
nameTemplate = model.Conf.FileTree.CreateDocNameTemplate
}
name, err := model.RenderCreateDocNameTemplate(nameTemplate)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"name": name,
}
}
func changeSort(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
pathsArg := arg["paths"].([]interface{})
var paths []string
for _, p := range pathsArg {
paths = append(paths, p.(string))
}
model.ChangeFileTreeSort(notebook, paths)
}
func searchDocs(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
k := arg["k"].(string)
ret.Data = model.SearchDocsByKeyword(k)
}
func listDocsByPath(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
p := arg["path"].(string)
sortParam := arg["sort"]
sortMode := model.Conf.FileTree.Sort
if nil != sortParam {
sortMode = int(sortParam.(float64))
}
files, totals, err := model.ListDocTree(notebook, p, sortMode)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
if model.Conf.FileTree.MaxListCount < totals {
util.PushMsg(fmt.Sprintf(model.Conf.Language(48), len(files)), 7000)
}
ret.Data = map[string]interface{}{
"box": notebook,
"path": p,
"files": files,
}
// 持久化文档面板排序
model.Conf.FileTree.Sort = sortMode
model.Conf.Save()
}
func getDoc(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
idx := arg["index"]
index := 0
if nil != idx {
index = int(idx.(float64))
}
k := arg["k"]
var keyword string
if nil != k {
keyword = k.(string)
}
m := arg["mode"] // 0: 仅当前 ID1向上 2向下3上下都加载4加载末尾
mode := 0
if nil != m {
mode = int(m.(float64))
}
s := arg["size"]
size := 102400 // 默认最大加载块数
if nil != s {
size = int(s.(float64))
}
blockCount, content, parentID, parent2ID, rootID, typ, eof, boxID, docPath, err := model.GetDoc(id, index, keyword, mode, size)
if filesys.ErrUnableLockFile == err {
ret.Code = 2
ret.Data = id
return
}
if model.ErrBlockNotFound == err {
ret.Code = 3
return
}
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"id": id,
"mode": mode,
"parentID": parentID,
"parent2ID": parent2ID,
"rootID": rootID,
"type": typ,
"content": content,
"blockCount": blockCount,
"eof": eof,
"box": boxID,
"path": docPath,
}
}
func pushCreate(box *model.Box, p, treeID string, arg map[string]interface{}) {
evt := util.NewCmdResult("create", 0, util.PushModeBroadcast, util.PushModeNone)
name := path.Base(p)
files, _, _ := model.ListDocTree(box.ID, path.Dir(p), model.Conf.FileTree.Sort)
evt.Data = map[string]interface{}{
"box": box,
"path": p,
"files": files,
"name": name,
"id": treeID,
}
evt.Callback = arg["callback"]
util.PushEvent(evt)
}

64
kernel/api/format.go Normal file
View file

@ -0,0 +1,64 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func netImg2LocalAssets(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
err := model.NetImg2LocalAssets(id)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
}
func autoSpace(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
err := model.AutoSpace(id)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
}

131
kernel/api/graph.go Normal file
View file

@ -0,0 +1,131 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/conf"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func resetGraph(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
graph := conf.NewGlobalGraph()
model.Conf.Graph.Global = graph
model.Conf.Save()
ret.Data = map[string]interface{}{
"conf": graph,
}
}
func resetLocalGraph(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
graph := conf.NewLocalGraph()
model.Conf.Graph.Local = graph
model.Conf.Save()
ret.Data = map[string]interface{}{
"conf": graph,
}
}
func getGraph(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
query := arg["k"].(string)
graphConf, err := gulu.JSON.MarshalJSON(arg["conf"])
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
global := conf.NewGlobalGraph()
if err = gulu.JSON.UnmarshalJSON(graphConf, global); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
model.Conf.Graph.Global = global
model.Conf.Save()
boxID, nodes, links := model.BuildGraph(query)
ret.Data = map[string]interface{}{
"nodes": nodes,
"links": links,
"conf": global,
"box": boxID,
"reqId": arg["reqId"],
}
util.RandomSleep(200, 500)
}
func getLocalGraph(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
keyword := arg["k"].(string)
id := arg["id"].(string)
graphConf, err := gulu.JSON.MarshalJSON(arg["conf"])
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
local := conf.NewLocalGraph()
if err = gulu.JSON.UnmarshalJSON(graphConf, local); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
model.Conf.Graph.Local = local
model.Conf.Save()
boxID, nodes, links := model.BuildTreeGraph(id, keyword)
ret.Data = map[string]interface{}{
"id": id,
"box": boxID,
"nodes": nodes,
"links": links,
"conf": local,
"reqId": arg["reqId"],
}
util.RandomSleep(200, 500)
}

178
kernel/api/history.go Normal file
View file

@ -0,0 +1,178 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"time"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func getNotebookHistory(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
histories, err := model.GetNotebookHistory()
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"histories": histories,
}
}
func getAssetsHistory(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
histories, err := model.GetAssetsHistory()
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"histories": histories,
}
}
func clearWorkspaceHistory(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
util.PushMsg(model.Conf.Language(100), 1000*60*15)
time.Sleep(3 * time.Second)
err := model.ClearWorkspaceHistory()
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
util.PushMsg(model.Conf.Language(99), 1000*5)
}
func getDocHistory(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
histories, err := model.GetDocHistory(notebook)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"box": notebook,
"histories": histories,
}
}
func getDocHistoryContent(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
historyPath := arg["historyPath"].(string)
content, err := model.GetDocHistoryContent(historyPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"content": content,
}
}
func rollbackDocHistory(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
historyPath := arg["historyPath"].(string)
err := model.RollbackDocHistory(notebook, historyPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"box": notebook,
}
}
func rollbackAssetsHistory(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
historyPath := arg["historyPath"].(string)
err := model.RollbackAssetsHistory(historyPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func rollbackNotebookHistory(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
historyPath := arg["historyPath"].(string)
err := model.RollbackNotebookHistory(historyPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}

179
kernel/api/import.go Normal file
View file

@ -0,0 +1,179 @@
// SiYuan - Build Your Eternal Digital Garden
// 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"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func importSY(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(200, ret)
form, err := c.MultipartForm()
if nil != err {
util.LogErrorf("parse import .sy.zip failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
files := form.File["file"]
if 1 > len(files) {
util.LogErrorf("parse import .sy.zip failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
file := files[0]
reader, err := file.Open()
if nil != err {
util.LogErrorf("read import .sy.zip failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
importDir := filepath.Join(util.TempDir, "import")
if err = os.MkdirAll(importDir, 0755); nil != err {
util.LogErrorf("make import dir [%s] failed: %s", importDir, err)
ret.Code = -1
ret.Msg = err.Error()
return
}
writePath := filepath.Join(util.TempDir, "import", file.Filename)
defer os.RemoveAll(writePath)
writer, err := os.OpenFile(writePath, os.O_RDWR|os.O_CREATE, 0644)
if nil != err {
util.LogErrorf("open import .sy.zip [%s] failed: %s", writePath, err)
ret.Code = -1
ret.Msg = err.Error()
return
}
if _, err = io.Copy(writer, reader); nil != err {
util.LogErrorf("write import .sy.zip failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
writer.Close()
reader.Close()
notebook := form.Value["notebook"][0]
toPath := form.Value["toPath"][0]
err = model.ImportSY(writePath, notebook, toPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func importData(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
form, err := c.MultipartForm()
if nil != err {
util.LogErrorf("import data failed: %s", err)
ret.Code = -1
ret.Msg = err.Error()
return
}
if 1 > len(form.File["file"]) {
util.LogErrorf("import data failed: %s", err)
ret.Code = -1
ret.Msg = "file not found"
return
}
tmpImport := filepath.Join(util.TempDir, "import")
err = os.MkdirAll(tmpImport, 0755)
if nil != err {
ret.Code = -1
ret.Msg = "create temp import dir failed"
return
}
dataZipPath := filepath.Join(tmpImport, util.CurrentTimeSecondsStr()+".zip")
defer os.RemoveAll(dataZipPath)
dataZipFile, err := os.Create(dataZipPath)
if nil != err {
util.LogErrorf("create temp file failed: %s", err)
ret.Code = -1
ret.Msg = "create temp file failed"
return
}
file := form.File["file"][0]
fileReader, err := file.Open()
if nil != err {
util.LogErrorf("open upload file failed: %s", err)
ret.Code = -1
ret.Msg = "open file failed"
return
}
_, err = io.Copy(dataZipFile, fileReader)
if nil != err {
util.LogErrorf("read upload file failed: %s", err)
ret.Code = -1
ret.Msg = "read file failed"
return
}
if err = dataZipFile.Close(); nil != err {
util.LogErrorf("close file failed: %s", err)
ret.Code = -1
ret.Msg = "close file failed"
return
}
fileReader.Close()
err = model.ImportData(dataZipPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func importStdMd(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
localPath := arg["localPath"].(string)
toPath := arg["toPath"].(string)
err := model.ImportFromLocalPath(notebook, localPath, toPath)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}

68
kernel/api/inbox.go Normal file
View file

@ -0,0 +1,68 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func removeShorthands(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
idsArg := arg["ids"].([]interface{})
var ids []string
for _, id := range idsArg {
ids = append(ids, id.(string))
}
err := model.RemoveCloudShorthands(ids)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
}
func getShorthands(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
page := int(arg["page"].(float64))
data, err := model.GetCloudShorthands(page)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
ret.Data = data
}

144
kernel/api/lute.go Normal file
View file

@ -0,0 +1,144 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"path/filepath"
"strings"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/88250/lute/render"
"github.com/88250/protyle"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func copyStdMarkdown(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
ret.Data = model.CopyStdMarkdown(id)
}
func html2BlockDOM(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
dom := arg["dom"].(string)
luteEngine := model.NewLute()
markdown, err := luteEngine.HTML2Markdown(dom)
if nil != err {
ret.Data = "Failed to convert"
return
}
var unlinks []*ast.Node
tree := parse.Parse("", []byte(markdown), luteEngine.ParseOptions)
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeListItem == n.Type && nil == n.FirstChild {
newNode := protyle.NewParagraph()
n.AppendChild(newNode)
n.SetIALAttr("updated", util.TimeFromID(newNode.ID))
return ast.WalkSkipChildren
} else if ast.NodeBlockquote == n.Type && nil == n.FirstChild.Next {
unlinks = append(unlinks, n)
}
return ast.WalkContinue
})
for _, n := range unlinks {
n.Unlink()
}
if "std" == model.Conf.System.Container {
// 处理本地资源文件复制
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || ast.NodeLinkDest != n.Type {
return ast.WalkContinue
}
if "" == n.TokensStr() {
return ast.WalkContinue
}
localPath := n.TokensStr()
if strings.HasPrefix(localPath, "http") {
return ast.WalkContinue
}
localPath = strings.TrimPrefix(localPath, "file://")
if gulu.OS.IsWindows() {
localPath = strings.TrimPrefix(localPath, "/")
}
if !gulu.File.IsExist(localPath) {
return ast.WalkContinue
}
name := filepath.Base(localPath)
ext := filepath.Ext(name)
name = name[0 : len(name)-len(ext)]
name = name + "-" + ast.NewNodeID() + ext
targetPath := filepath.Join(util.DataDir, "assets", name)
if err = gulu.File.CopyFile(localPath, targetPath); nil != err {
util.LogErrorf("copy asset from [%s] to [%s] failed: %s", localPath, targetPath, err)
return ast.WalkStop
}
n.Tokens = gulu.Str.ToBytes("assets/" + name)
return ast.WalkContinue
})
}
renderer := render.NewBlockRenderer(tree, luteEngine.RenderOptions)
output := renderer.Render()
ret.Data = gulu.Str.FromBytes(output)
}
func spinBlockDOM(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
dom := arg["dom"].(string)
luteEngine := model.NewLute()
dom = luteEngine.SpinBlockDOM(dom)
ret.Data = map[string]interface{}{
"dom": dom,
}
}

277
kernel/api/notebook.go Normal file
View file

@ -0,0 +1,277 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"strings"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func setNotebookIcon(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
boxID := arg["notebook"].(string)
icon := arg["icon"].(string)
model.SetBoxIcon(boxID, icon)
}
func changeSortNotebook(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
idsArg := arg["notebooks"].([]interface{})
var ids []string
for _, p := range idsArg {
ids = append(ids, p.(string))
}
model.ChangeBoxSort(ids)
}
func renameNotebook(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
name := arg["name"].(string)
err := model.RenameBox(notebook, name)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
evt := util.NewCmdResult("renamenotebook", 0, util.PushModeBroadcast, util.PushModeNone)
evt.Data = map[string]interface{}{
"box": notebook,
"name": name,
}
util.PushEvent(evt)
}
func removeNotebook(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
err := model.RemoveBox(notebook)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
evt := util.NewCmdResult("unmount", 0, util.PushModeBroadcast, 0)
evt.Data = map[string]interface{}{
"box": notebook,
}
evt.Callback = arg["callback"]
util.PushEvent(evt)
}
func createNotebook(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
name := arg["name"].(string)
id, err := model.CreateBox(name)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
existed, err := model.Mount(id)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"notebook": model.Conf.Box(id),
}
evt := util.NewCmdResult("createnotebook", 0, util.PushModeBroadcast, util.PushModeNone)
evt.Data = map[string]interface{}{
"box": model.Conf.Box(id),
"existed": existed,
}
util.PushEvent(evt)
}
func openNotebook(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
util.PushMsg(model.Conf.Language(45), 1000*60*15)
existed, err := model.Mount(notebook)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
evt := util.NewCmdResult("mount", 0, util.PushModeBroadcast, util.PushModeNone)
evt.Data = map[string]interface{}{
"box": model.Conf.Box(notebook),
"existed": existed,
}
evt.Callback = arg["callback"]
util.PushEvent(evt)
}
func closeNotebook(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
model.Unmount(notebook)
}
func getNotebookConf(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
box := model.Conf.Box(notebook)
ret.Data = map[string]interface{}{
"box": box.ID,
"name": box.Name,
"conf": box.GetConf(),
}
}
func setNotebookConf(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
notebook := arg["notebook"].(string)
box := model.Conf.Box(notebook)
param, err := gulu.JSON.MarshalJSON(arg["conf"])
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
boxConf := box.GetConf()
if err = gulu.JSON.UnmarshalJSON(param, boxConf); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
boxConf.RefCreateSavePath = strings.TrimSpace(boxConf.RefCreateSavePath)
if "" != boxConf.RefCreateSavePath {
if !strings.HasSuffix(boxConf.RefCreateSavePath, "/") {
boxConf.RefCreateSavePath += "/"
}
}
boxConf.DailyNoteSavePath = strings.TrimSpace(boxConf.DailyNoteSavePath)
if "" != boxConf.DailyNoteSavePath {
if !strings.HasPrefix(boxConf.DailyNoteSavePath, "/") {
boxConf.DailyNoteSavePath = "/" + boxConf.DailyNoteSavePath
}
}
if "/" == boxConf.DailyNoteSavePath {
ret.Code = -1
ret.Msg = model.Conf.Language(49)
return
}
boxConf.DailyNoteTemplatePath = strings.TrimSpace(boxConf.DailyNoteTemplatePath)
if "" != boxConf.DailyNoteTemplatePath {
if !strings.HasSuffix(boxConf.DailyNoteTemplatePath, ".md") {
boxConf.DailyNoteTemplatePath += ".md"
}
if !strings.HasPrefix(boxConf.DailyNoteTemplatePath, "/") {
boxConf.DailyNoteTemplatePath = "/" + boxConf.DailyNoteTemplatePath
}
}
box.SaveConf(boxConf)
ret.Data = boxConf
}
func lsNotebooks(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
notebooks, err := model.ListNotebooks()
if nil != err {
return
}
ret.Data = map[string]interface{}{
"notebooks": notebooks,
}
}

49
kernel/api/outline.go Normal file
View file

@ -0,0 +1,49 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func getDocOutline(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
if nil == arg["id"] {
return
}
rootID := arg["id"].(string)
headings, err := model.Outline(rootID)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
ret.Data = headings
}

96
kernel/api/ref.go Normal file
View file

@ -0,0 +1,96 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func refreshBacklink(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
model.RefreshBacklink(id)
}
func getBacklink(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
if nil == arg["id"] {
return
}
id := arg["id"].(string)
keyword := arg["k"].(string)
mentionKeyword := arg["mk"].(string)
beforeLen := arg["beforeLen"].(float64)
boxID, backlinks, backmentions, linkRefsCount, mentionsCount := model.BuildTreeBacklink(id, keyword, mentionKeyword, int(beforeLen))
ret.Data = map[string]interface{}{
"backlinks": backlinks,
"linkRefsCount": linkRefsCount,
"backmentions": backmentions,
"mentionsCount": mentionsCount,
"k": keyword,
"mk": mentionKeyword,
"box": boxID,
}
util.RandomSleep(200, 500)
}
func createBacklink(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
defID := arg["defID"].(string)
refID := arg["refID"].(string)
refText := arg["refText"].(string)
isDynamic := arg["isDynamic"].(bool)
refRootID, err := model.CreateBacklink(defID, refID, refText, isDynamic)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = map[string]interface{}{
"defID": defID,
"refID": refID,
"refRootID": refRootID,
"refText": refText,
}
}

246
kernel/api/router.go Normal file
View file

@ -0,0 +1,246 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
)
func ServeAPI(ginServer *gin.Engine) {
// 不需要鉴权
ginServer.Handle("GET", "/api/system/bootProgress", bootProgress)
ginServer.Handle("POST", "/api/system/bootProgress", bootProgress)
ginServer.Handle("GET", "/api/system/version", version)
ginServer.Handle("POST", "/api/system/version", version)
ginServer.Handle("POST", "/api/system/currentTime", currentTime)
ginServer.Handle("POST", "/api/system/uiproc", addUIProcess)
ginServer.Handle("POST", "/api/system/loginAuth", model.LoginAuth)
ginServer.Handle("POST", "/api/system/logoutAuth", model.LogoutAuth)
// 需要鉴权
ginServer.Handle("POST", "/api/system/getEmojiConf", model.CheckAuth, getEmojiConf)
ginServer.Handle("POST", "/api/system/setAccessAuthCode", model.CheckAuth, setAccessAuthCode)
ginServer.Handle("POST", "/api/system/setNetworkServe", model.CheckAuth, setNetworkServe)
ginServer.Handle("POST", "/api/system/setUploadErrLog", model.CheckAuth, setUploadErrLog)
ginServer.Handle("POST", "/api/system/setNetworkProxy", model.CheckAuth, setNetworkProxy)
ginServer.Handle("POST", "/api/system/setWorkspaceDir", model.CheckAuth, setWorkspaceDir)
ginServer.Handle("POST", "/api/system/listWorkspaceDirs", model.CheckAuth, listWorkspaceDirs)
ginServer.Handle("POST", "/api/system/setAppearanceMode", model.CheckAuth, setAppearanceMode)
ginServer.Handle("POST", "/api/system/getSysFonts", model.CheckAuth, getSysFonts)
ginServer.Handle("POST", "/api/system/setE2EEPasswd", model.CheckAuth, setE2EEPasswd)
ginServer.Handle("POST", "/api/system/exit", model.CheckAuth, exit)
ginServer.Handle("POST", "/api/system/setUILayout", model.CheckAuth, setUILayout)
ginServer.Handle("POST", "/api/system/getConf", model.CheckAuth, getConf)
ginServer.Handle("POST", "/api/system/checkUpdate", model.CheckAuth, checkUpdate)
ginServer.Handle("POST", "/api/account/login", model.CheckAuth, login)
ginServer.Handle("POST", "/api/account/checkActivationcode", model.CheckAuth, checkActivationcode)
ginServer.Handle("POST", "/api/account/useActivationcode", model.CheckAuth, useActivationcode)
ginServer.Handle("POST", "/api/account/deactivate", model.CheckAuth, deactivateUser)
ginServer.Handle("POST", "/api/notebook/lsNotebooks", model.CheckAuth, lsNotebooks)
ginServer.Handle("POST", "/api/notebook/openNotebook", model.CheckAuth, openNotebook)
ginServer.Handle("POST", "/api/notebook/closeNotebook", model.CheckAuth, closeNotebook)
ginServer.Handle("POST", "/api/notebook/getNotebookConf", model.CheckAuth, getNotebookConf)
ginServer.Handle("POST", "/api/notebook/setNotebookConf", model.CheckAuth, setNotebookConf)
ginServer.Handle("POST", "/api/notebook/createNotebook", model.CheckAuth, createNotebook)
ginServer.Handle("POST", "/api/notebook/removeNotebook", model.CheckAuth, removeNotebook)
ginServer.Handle("POST", "/api/notebook/renameNotebook", model.CheckAuth, renameNotebook)
ginServer.Handle("POST", "/api/notebook/changeSortNotebook", model.CheckAuth, changeSortNotebook)
ginServer.Handle("POST", "/api/notebook/setNotebookIcon", model.CheckAuth, setNotebookIcon)
ginServer.Handle("POST", "/api/filetree/searchDocs", model.CheckAuth, searchDocs)
ginServer.Handle("POST", "/api/filetree/listDocsByPath", model.CheckAuth, listDocsByPath)
ginServer.Handle("POST", "/api/filetree/getDoc", model.CheckAuth, getDoc)
ginServer.Handle("POST", "/api/filetree/getDocNameTemplate", model.CheckAuth, getDocNameTemplate)
ginServer.Handle("POST", "/api/filetree/changeSort", model.CheckAuth, changeSort)
ginServer.Handle("POST", "/api/filetree/lockFile", model.CheckAuth, lockFile)
ginServer.Handle("POST", "/api/filetree/createDocWithMd", model.CheckAuth, model.CheckReadonly, createDocWithMd)
ginServer.Handle("POST", "/api/filetree/createDailyNote", model.CheckAuth, model.CheckReadonly, createDailyNote)
ginServer.Handle("POST", "/api/filetree/createDoc", model.CheckAuth, model.CheckReadonly, createDoc)
ginServer.Handle("POST", "/api/filetree/renameDoc", model.CheckAuth, model.CheckReadonly, renameDoc)
ginServer.Handle("POST", "/api/filetree/removeDoc", model.CheckAuth, model.CheckReadonly, removeDoc)
ginServer.Handle("POST", "/api/filetree/moveDoc", model.CheckAuth, model.CheckReadonly, moveDoc)
ginServer.Handle("POST", "/api/filetree/duplicateDoc", model.CheckAuth, model.CheckReadonly, duplicateDoc)
ginServer.Handle("POST", "/api/filetree/getHPathByPath", model.CheckAuth, getHPathByPath)
ginServer.Handle("POST", "/api/filetree/getHPathByID", model.CheckAuth, getHPathByID)
ginServer.Handle("POST", "/api/filetree/getFullHPathByID", model.CheckAuth, getFullHPathByID)
ginServer.Handle("POST", "/api/filetree/doc2Heading", model.CheckAuth, model.CheckReadonly, doc2Heading)
ginServer.Handle("POST", "/api/filetree/heading2Doc", model.CheckAuth, model.CheckReadonly, heading2Doc)
ginServer.Handle("POST", "/api/filetree/li2Doc", model.CheckAuth, model.CheckReadonly, li2Doc)
ginServer.Handle("POST", "/api/filetree/refreshFiletree", model.CheckAuth, model.CheckReadonly, refreshFiletree)
ginServer.Handle("POST", "/api/format/autoSpace", model.CheckAuth, model.CheckReadonly, autoSpace)
ginServer.Handle("POST", "/api/format/netImg2LocalAssets", model.CheckAuth, model.CheckReadonly, netImg2LocalAssets)
ginServer.Handle("POST", "/api/history/getNotebookHistory", model.CheckAuth, getNotebookHistory)
ginServer.Handle("POST", "/api/history/rollbackNotebookHistory", model.CheckAuth, rollbackNotebookHistory)
ginServer.Handle("POST", "/api/history/getAssetsHistory", model.CheckAuth, getAssetsHistory)
ginServer.Handle("POST", "/api/history/rollbackAssetsHistory", model.CheckAuth, rollbackAssetsHistory)
ginServer.Handle("POST", "/api/history/getDocHistory", model.CheckAuth, getDocHistory)
ginServer.Handle("POST", "/api/history/getDocHistoryContent", model.CheckAuth, getDocHistoryContent)
ginServer.Handle("POST", "/api/history/rollbackDocHistory", model.CheckAuth, model.CheckReadonly, rollbackDocHistory)
ginServer.Handle("POST", "/api/history/clearWorkspaceHistory", model.CheckAuth, model.CheckReadonly, clearWorkspaceHistory)
ginServer.Handle("POST", "/api/outline/getDocOutline", model.CheckAuth, getDocOutline)
ginServer.Handle("POST", "/api/bookmark/getBookmark", model.CheckAuth, getBookmark)
ginServer.Handle("POST", "/api/bookmark/renameBookmark", model.CheckAuth, renameBookmark)
ginServer.Handle("POST", "/api/tag/getTag", model.CheckAuth, getTag)
ginServer.Handle("POST", "/api/tag/renameTag", model.CheckAuth, renameTag)
ginServer.Handle("POST", "/api/tag/removeTag", model.CheckAuth, removeTag)
ginServer.Handle("POST", "/api/lute/spinBlockDOM", model.CheckAuth, spinBlockDOM) // 未测试
ginServer.Handle("POST", "/api/lute/html2BlockDOM", model.CheckAuth, html2BlockDOM)
ginServer.Handle("POST", "/api/lute/copyStdMarkdown", model.CheckAuth, copyStdMarkdown)
ginServer.Handle("POST", "/api/query/sql", model.CheckAuth, SQL)
ginServer.Handle("POST", "/api/search/searchTag", model.CheckAuth, searchTag)
ginServer.Handle("POST", "/api/search/searchTemplate", model.CheckAuth, searchTemplate)
ginServer.Handle("POST", "/api/search/searchWidget", model.CheckAuth, searchWidget)
ginServer.Handle("POST", "/api/search/searchRefBlock", model.CheckAuth, searchRefBlock)
ginServer.Handle("POST", "/api/search/searchEmbedBlock", model.CheckAuth, searchEmbedBlock)
ginServer.Handle("POST", "/api/search/fullTextSearchBlock", model.CheckAuth, fullTextSearchBlock)
ginServer.Handle("POST", "/api/search/searchAsset", model.CheckAuth, searchAsset)
ginServer.Handle("POST", "/api/search/findReplace", model.CheckAuth, findReplace)
ginServer.Handle("POST", "/api/block/getBlockInfo", model.CheckAuth, getBlockInfo)
ginServer.Handle("POST", "/api/block/getBlockDOM", model.CheckAuth, getBlockDOM)
ginServer.Handle("POST", "/api/block/getBlockBreadcrumb", model.CheckAuth, getBlockBreadcrumb)
ginServer.Handle("POST", "/api/block/getRefIDs", model.CheckAuth, getRefIDs)
ginServer.Handle("POST", "/api/block/getRefIDsByFileAnnotationID", model.CheckAuth, getRefIDsByFileAnnotationID)
ginServer.Handle("POST", "/api/block/getBlockDefIDsByRefText", model.CheckAuth, getBlockDefIDsByRefText)
ginServer.Handle("POST", "/api/block/getRefText", model.CheckAuth, getRefText)
ginServer.Handle("POST", "/api/block/getBlockWordCount", model.CheckAuth, getBlockWordCount)
ginServer.Handle("POST", "/api/block/getRecentUpdatedBlocks", model.CheckAuth, getRecentUpdatedBlocks)
ginServer.Handle("POST", "/api/block/getDocInfo", model.CheckAuth, getDocInfo)
ginServer.Handle("POST", "/api/block/checkBlockExist", model.CheckAuth, checkBlockExist)
ginServer.Handle("POST", "/api/block/checkBlockFold", model.CheckAuth, checkBlockFold)
ginServer.Handle("POST", "/api/block/insertBlock", model.CheckAuth, insertBlock)
ginServer.Handle("POST", "/api/block/prependBlock", model.CheckAuth, prependBlock)
ginServer.Handle("POST", "/api/block/appendBlock", model.CheckAuth, appendBlock)
ginServer.Handle("POST", "/api/block/updateBlock", model.CheckAuth, updateBlock)
ginServer.Handle("POST", "/api/block/deleteBlock", model.CheckAuth, deleteBlock)
ginServer.Handle("POST", "/api/block/setBlockReminder", model.CheckAuth, setBlockReminder)
ginServer.Handle("POST", "/api/file/getFile", model.CheckAuth, getFile)
ginServer.Handle("POST", "/api/file/putFile", model.CheckAuth, putFile)
ginServer.Handle("POST", "/api/ref/refreshBacklink", model.CheckAuth, refreshBacklink)
ginServer.Handle("POST", "/api/ref/getBacklink", model.CheckAuth, getBacklink)
ginServer.Handle("POST", "/api/ref/createBacklink", model.CheckAuth, model.CheckReadonly, createBacklink)
ginServer.Handle("POST", "/api/attr/getBookmarkLabels", model.CheckAuth, getBookmarkLabels)
ginServer.Handle("POST", "/api/attr/resetBlockAttrs", model.CheckAuth, model.CheckReadonly, resetBlockAttrs)
ginServer.Handle("POST", "/api/attr/setBlockAttrs", model.CheckAuth, model.CheckReadonly, setBlockAttrs)
ginServer.Handle("POST", "/api/attr/getBlockAttrs", model.CheckAuth, getBlockAttrs)
ginServer.Handle("POST", "/api/cloud/getCloudSpace", model.CheckAuth, getCloudSpace)
ginServer.Handle("POST", "/api/backup/getLocalBackup", model.CheckAuth, getLocalBackup)
ginServer.Handle("POST", "/api/backup/createLocalBackup", model.CheckAuth, model.CheckReadonly, createLocalBackup)
ginServer.Handle("POST", "/api/backup/recoverLocalBackup", model.CheckAuth, model.CheckReadonly, recoverLocalBackup)
ginServer.Handle("POST", "/api/backup/uploadLocalBackup", model.CheckAuth, model.CheckReadonly, uploadLocalBackup)
ginServer.Handle("POST", "/api/backup/downloadCloudBackup", model.CheckAuth, model.CheckReadonly, downloadCloudBackup)
ginServer.Handle("POST", "/api/backup/removeCloudBackup", model.CheckAuth, model.CheckReadonly, removeCloudBackup)
ginServer.Handle("POST", "/api/sync/setSyncEnable", model.CheckAuth, setSyncEnable)
ginServer.Handle("POST", "/api/sync/setCloudSyncDir", model.CheckAuth, setCloudSyncDir)
ginServer.Handle("POST", "/api/sync/createCloudSyncDir", model.CheckAuth, model.CheckReadonly, createCloudSyncDir)
ginServer.Handle("POST", "/api/sync/removeCloudSyncDir", model.CheckAuth, model.CheckReadonly, removeCloudSyncDir)
ginServer.Handle("POST", "/api/sync/listCloudSyncDir", model.CheckAuth, listCloudSyncDir)
ginServer.Handle("POST", "/api/sync/performSync", model.CheckAuth, performSync)
ginServer.Handle("POST", "/api/sync/performBootSync", model.CheckAuth, performBootSync)
ginServer.Handle("POST", "/api/sync/getBootSync", model.CheckAuth, getBootSync)
ginServer.Handle("POST", "/api/sync/getSyncDirection", model.CheckAuth, getSyncDirection)
ginServer.Handle("POST", "/api/inbox/getShorthands", model.CheckAuth, getShorthands)
ginServer.Handle("POST", "/api/inbox/removeShorthands", model.CheckAuth, removeShorthands)
ginServer.Handle("POST", "/api/extension/copy", model.CheckAuth, extensionCopy)
ginServer.Handle("POST", "/api/clipboard/readFilePaths", model.CheckAuth, readFilePaths)
ginServer.Handle("POST", "/api/asset/uploadCloud", model.CheckAuth, model.CheckReadonly, uploadCloud)
ginServer.Handle("POST", "/api/asset/insertLocalAssets", model.CheckAuth, model.CheckReadonly, insertLocalAssets)
ginServer.Handle("POST", "/api/asset/resolveAssetPath", model.CheckAuth, resolveAssetPath)
ginServer.Handle("POST", "/api/asset/upload", model.CheckAuth, model.CheckReadonly, model.Upload)
ginServer.Handle("POST", "/api/asset/setFileAnnotation", model.CheckAuth, model.CheckReadonly, setFileAnnotation)
ginServer.Handle("POST", "/api/asset/getFileAnnotation", model.CheckAuth, getFileAnnotation)
ginServer.Handle("POST", "/api/asset/getUnusedAssets", model.CheckAuth, getUnusedAssets)
ginServer.Handle("POST", "/api/asset/removeUnusedAsset", model.CheckAuth, model.CheckReadonly, removeUnusedAsset)
ginServer.Handle("POST", "/api/asset/removeUnusedAssets", model.CheckAuth, model.CheckReadonly, removeUnusedAssets)
ginServer.Handle("POST", "/api/asset/getDocImageAssets", model.CheckAuth, model.CheckReadonly, getDocImageAssets)
ginServer.Handle("POST", "/api/export/batchExportMd", model.CheckAuth, batchExportMd)
ginServer.Handle("POST", "/api/export/exportMd", model.CheckAuth, exportMd)
ginServer.Handle("POST", "/api/export/exportSY", model.CheckAuth, exportSY)
ginServer.Handle("POST", "/api/export/exportMdContent", model.CheckAuth, exportMdContent)
ginServer.Handle("POST", "/api/export/exportHTML", model.CheckAuth, exportHTML)
ginServer.Handle("POST", "/api/export/exportMdHTML", model.CheckAuth, exportMdHTML)
ginServer.Handle("POST", "/api/export/exportDocx", model.CheckAuth, exportDocx)
ginServer.Handle("POST", "/api/export/addPDFOutline", model.CheckAuth, addPDFOutline)
ginServer.Handle("POST", "/api/export/preview", model.CheckAuth, exportPreview)
ginServer.Handle("POST", "/api/export/exportData", model.CheckAuth, exportData)
ginServer.Handle("POST", "/api/export/exportDataInFolder", model.CheckAuth, exportDataInFolder)
ginServer.Handle("POST", "/api/import/importStdMd", model.CheckAuth, model.CheckReadonly, importStdMd)
ginServer.Handle("POST", "/api/import/importData", model.CheckAuth, model.CheckReadonly, importData)
ginServer.Handle("POST", "/api/import/importSY", model.CheckAuth, model.CheckReadonly, importSY)
ginServer.Handle("POST", "/api/template/render", model.CheckAuth, renderTemplate)
ginServer.Handle("POST", "/api/template/docSaveAsTemplate", model.CheckAuth, docSaveAsTemplate)
ginServer.Handle("POST", "/api/transactions", model.CheckAuth, model.CheckReadonly, performTransactions)
ginServer.Handle("POST", "/api/setting/setAccount", model.CheckAuth, setAccount)
ginServer.Handle("POST", "/api/setting/setEditor", model.CheckAuth, setEditor)
ginServer.Handle("POST", "/api/setting/setExport", model.CheckAuth, setExport)
ginServer.Handle("POST", "/api/setting/setFiletree", model.CheckAuth, setFiletree)
ginServer.Handle("POST", "/api/setting/setSearch", model.CheckAuth, setSearch)
ginServer.Handle("POST", "/api/setting/setKeymap", model.CheckAuth, setKeymap)
ginServer.Handle("POST", "/api/setting/setAppearance", model.CheckAuth, setAppearance)
ginServer.Handle("POST", "/api/setting/getCloudUser", model.CheckAuth, getCloudUser)
ginServer.Handle("POST", "/api/setting/logoutCloudUser", model.CheckAuth, logoutCloudUser)
ginServer.Handle("POST", "/api/setting/login2faCloudUser", model.CheckAuth, login2faCloudUser)
ginServer.Handle("POST", "/api/setting/getCustomCSS", model.CheckAuth, getCustomCSS)
ginServer.Handle("POST", "/api/setting/setCustomCSS", model.CheckAuth, setCustomCSS)
ginServer.Handle("POST", "/api/setting/setEmoji", model.CheckAuth, setEmoji)
ginServer.Handle("POST", "/api/setting/setSearchCaseSensitive", model.CheckAuth, setSearchCaseSensitive)
ginServer.Handle("POST", "/api/graph/resetGraph", model.CheckAuth, resetGraph)
ginServer.Handle("POST", "/api/graph/resetLocalGraph", model.CheckAuth, resetLocalGraph)
ginServer.Handle("POST", "/api/graph/getGraph", model.CheckAuth, getGraph)
ginServer.Handle("POST", "/api/graph/getLocalGraph", model.CheckAuth, getLocalGraph)
ginServer.Handle("POST", "/api/bazaar/getBazaarWidget", model.CheckAuth, getBazaarWidget)
ginServer.Handle("POST", "/api/bazaar/installBazaarWidget", model.CheckAuth, installBazaarWidget)
ginServer.Handle("POST", "/api/bazaar/uninstallBazaarWidget", model.CheckAuth, uninstallBazaarWidget)
ginServer.Handle("POST", "/api/bazaar/getBazaarIcon", model.CheckAuth, getBazaarIcon)
ginServer.Handle("POST", "/api/bazaar/installBazaarIcon", model.CheckAuth, installBazaarIcon)
ginServer.Handle("POST", "/api/bazaar/uninstallBazaarIcon", model.CheckAuth, uninstallBazaarIcon)
ginServer.Handle("POST", "/api/bazaar/getBazaarTemplate", model.CheckAuth, getBazaarTemplate)
ginServer.Handle("POST", "/api/bazaar/installBazaarTemplate", model.CheckAuth, installBazaarTemplate)
ginServer.Handle("POST", "/api/bazaar/uninstallBazaarTemplate", model.CheckAuth, uninstallBazaarTemplate)
ginServer.Handle("POST", "/api/bazaar/getBazaarTheme", model.CheckAuth, getBazaarTheme)
ginServer.Handle("POST", "/api/bazaar/installBazaarTheme", model.CheckAuth, installBazaarTheme)
ginServer.Handle("POST", "/api/bazaar/uninstallBazaarTheme", model.CheckAuth, uninstallBazaarTheme)
ginServer.Handle("POST", "/api/bazaar/getBazaarPackageREAME", model.CheckAuth, getBazaarPackageREAME)
}

205
kernel/api/search.go Normal file
View file

@ -0,0 +1,205 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"strings"
"github.com/88250/gulu"
"github.com/88250/lute/html"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func findReplace(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
k := arg["k"].(string)
r := arg["r"].(string)
idsArg := arg["ids"].([]interface{})
var ids []string
for _, id := range idsArg {
ids = append(ids, id.(string))
}
err := model.FindReplace(k, r, ids)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 3000}
return
}
return
}
func searchAsset(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
k := arg["k"].(string)
ret.Data = model.SearchAssetsByName(k)
return
}
func searchTag(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
k := arg["k"].(string)
tags := model.SearchTags(k)
ret.Data = map[string]interface{}{
"tags": tags,
"k": k,
}
}
func searchWidget(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
keyword := arg["k"].(string)
blocks := model.SearchWidget(keyword)
ret.Data = map[string]interface{}{
"blocks": blocks,
"k": keyword,
}
}
func searchTemplate(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
keyword := arg["k"].(string)
blocks := model.SearchTemplate(keyword)
ret.Data = map[string]interface{}{
"blocks": blocks,
"k": keyword,
}
}
func searchEmbedBlock(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
stmt := arg["stmt"].(string)
excludeIDsArg := arg["excludeIDs"].([]interface{})
var excludeIDs []string
for _, excludeID := range excludeIDsArg {
excludeIDs = append(excludeIDs, excludeID.(string))
}
headingMode := 0 // 0带标题下方块
headingModeArg := arg["headingMode"]
if nil != headingModeArg {
headingMode = int(headingModeArg.(float64))
}
blocks := model.SearchEmbedBlock(stmt, excludeIDs, headingMode)
ret.Data = map[string]interface{}{
"blocks": blocks,
}
}
func searchRefBlock(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
rootID := arg["rootID"].(string)
id := arg["id"].(string)
keyword := arg["k"].(string)
beforeLen := int(arg["beforeLen"].(float64))
blocks, newDoc := model.SearchRefBlock(id, rootID, keyword, beforeLen)
ret.Data = map[string]interface{}{
"blocks": blocks,
"newDoc": newDoc,
"k": html.EscapeHTMLStr(keyword),
"reqId": arg["reqId"],
}
}
func fullTextSearchBlock(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
query := arg["query"].(string)
pathArg := arg["path"]
var path string
if nil != pathArg {
path = pathArg.(string)
}
var box string
if "" != path {
box = strings.Split(path, "/")[0]
path = strings.TrimPrefix(path, box)
}
var types map[string]bool
if nil != arg["types"] {
typesArg := arg["types"].(map[string]interface{})
types = map[string]bool{}
for t, b := range typesArg {
types[t] = b.(bool)
}
}
querySyntaxArg := arg["querySyntax"]
var querySyntax bool
if nil != querySyntaxArg {
querySyntax = querySyntaxArg.(bool)
}
blocks := model.FullTextSearchBlock(query, box, path, types, querySyntax)
ret.Data = blocks
}

384
kernel/api/setting.go Normal file
View file

@ -0,0 +1,384 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"fmt"
"net/http"
"strings"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/conf"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/util"
)
func setAccount(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
param, err := gulu.JSON.MarshalJSON(arg)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
account := &conf.Account{}
if err = gulu.JSON.UnmarshalJSON(param, account); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
model.Conf.Account = account
model.Conf.Save()
ret.Data = model.Conf.Account
}
func setEditor(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
param, err := gulu.JSON.MarshalJSON(arg)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
oldGenerateHistoryInterval := model.Conf.Editor.GenerateHistoryInterval
editor := conf.NewEditor()
if err = gulu.JSON.UnmarshalJSON(param, editor); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
if "" == editor.PlantUMLServePath {
editor.PlantUMLServePath = "https://www.plantuml.com/plantuml/svg/~1"
}
model.Conf.Editor = editor
model.Conf.Save()
if oldGenerateHistoryInterval != model.Conf.Editor.GenerateHistoryInterval {
model.ChangeHistoryTick(editor.GenerateHistoryInterval)
}
ret.Data = model.Conf.Editor
}
func setExport(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
param, err := gulu.JSON.MarshalJSON(arg)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
export := &conf.Export{}
if err = gulu.JSON.UnmarshalJSON(param, export); nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
if "" != export.PandocBin {
if !util.IsValidPandocBin(export.PandocBin) {
ret.Code = -1
ret.Msg = fmt.Sprintf(model.Conf.Language(117), export.PandocBin)
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
}
model.Conf.Export = export
model.Conf.Save()
ret.Data = model.Conf.Export
}
func setFiletree(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
param, err := gulu.JSON.MarshalJSON(arg)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
fileTree := &conf.FileTree{}
if err = gulu.JSON.UnmarshalJSON(param, fileTree); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
fileTree.RefCreateSavePath = strings.TrimSpace(fileTree.RefCreateSavePath)
if "" != fileTree.RefCreateSavePath {
if !strings.HasSuffix(fileTree.RefCreateSavePath, "/") {
fileTree.RefCreateSavePath += "/"
}
}
model.Conf.FileTree = fileTree
model.Conf.Save()
ret.Data = model.Conf.FileTree
}
func setSearch(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
param, err := gulu.JSON.MarshalJSON(arg)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
s := &conf.Search{}
if err = gulu.JSON.UnmarshalJSON(param, s); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
if 1 > s.Limit {
s.Limit = 32
}
model.Conf.Search = s
model.Conf.Save()
sql.SetCaseSensitive(s.CaseSensitive)
sql.ClearVirtualRefKeywords()
ret.Data = s
}
func setKeymap(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
param, err := gulu.JSON.MarshalJSON(arg["data"])
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
keymap := &conf.Keymap{}
if err = gulu.JSON.UnmarshalJSON(param, keymap); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
model.Conf.Keymap = keymap
model.Conf.Save()
}
func setAppearance(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
param, err := gulu.JSON.MarshalJSON(arg)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
appearance := &conf.Appearance{}
if err = gulu.JSON.UnmarshalJSON(param, appearance); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
model.Conf.Appearance = appearance
model.Conf.Lang = appearance.Lang
model.Conf.Save()
model.InitAppearance()
ret.Data = model.Conf.Appearance
}
func getCloudUser(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
t := arg["token"]
var token string
if nil != t {
token = t.(string)
}
if err := model.RefreshUser(token); nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
ret.Data = model.Conf.User
}
func logoutCloudUser(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
model.LogoutUser()
}
func login2faCloudUser(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)
code := arg["code"].(string)
data, err := model.Login2fa(token, code)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = data
}
func getCustomCSS(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
themeName := arg["theme"].(string)
customCSS, err := model.ReadCustomCSS(themeName)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
ret.Data = customCSS
}
func setCustomCSS(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
themeName := arg["theme"].(string)
css := arg["css"].(map[string]interface{})
if err := model.WriteCustomCSS(themeName, css); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
}
func setEmoji(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
argEmoji := arg["emoji"].([]interface{})
var emoji []string
for _, ae := range argEmoji {
emoji = append(emoji, ae.(string))
}
model.Conf.Editor.Emoji = emoji
}
func setSearchCaseSensitive(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
caseSensitive := arg["caseSensitive"].(bool)
model.Conf.Search.CaseSensitive = caseSensitive
model.Conf.Save()
sql.SetCaseSensitive(caseSensitive)
}

46
kernel/api/sql.go Normal file
View file

@ -0,0 +1,46 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/util"
)
func SQL(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
stmt := arg["stmt"].(string)
result, err := sql.Query(stmt)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
return
}
ret.Data = result
}

154
kernel/api/sync.go Normal file
View file

@ -0,0 +1,154 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func getSyncDirection(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
cloudDirName := arg["name"].(string)
ret.Code, ret.Msg = model.GetSyncDirection(cloudDirName)
}
func getBootSync(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
if 1 == model.BootSyncSucc {
ret.Code = 1
ret.Msg = model.Conf.Language(17)
return
}
}
func performSync(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
model.SyncData(false, false, true)
}
func performBootSync(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
model.SyncData(true, false, true)
ret.Code = model.BootSyncSucc
}
func listCloudSyncDir(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
syncDirs, hSize, err := model.ListCloudSyncDir()
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
ret.Data = map[string]interface{}{
"syncDirs": syncDirs,
"hSize": hSize,
"checkedSyncDir": model.Conf.Sync.CloudName,
}
}
func removeCloudSyncDir(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
name := arg["name"].(string)
err := model.RemoveCloudSyncDir(name)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
ret.Data = model.Conf.Sync.CloudName
}
func createCloudSyncDir(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
name := arg["name"].(string)
err := model.CreateCloudSyncDir(name)
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
}
func setSyncEnable(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
enabled := arg["enabled"].(bool)
err := model.SetSyncEnable(enabled)
if nil != err {
ret.Code = 1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
}
func setCloudSyncDir(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
name := arg["name"].(string)
model.SetCloudSyncDir(name)
}

397
kernel/api/system.go Normal file
View file

@ -0,0 +1,397 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/conf"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
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 nil != err {
util.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); nil != err {
util.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": "自定义",
}
items := []map[string]interface{}{}
custom["items"] = items
if gulu.File.IsDir(customConfDir) {
model.CustomEmojis = sync.Map{}
customEmojis, err := os.ReadDir(customConfDir)
if nil != err {
util.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 nil != err {
util.LogErrorf("read custom emojis failed: %s", err)
continue
}
for _, subCustomEmoji := range subCustomEmojis {
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,
"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 getConf(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
ret.Data = model.Conf
}
func setUILayout(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
param, err := gulu.JSON.MarshalJSON(arg["layout"])
if nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
uiLayout := &conf.UILayout{}
if err = gulu.JSON.UnmarshalJSON(param, uiLayout); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
model.Conf.UILayout = uiLayout
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)
model.Conf.AccessAuthCode = aac
model.Conf.Save()
session := util.GetSession(c)
session.AccessAuthCode = aac
session.Save(c)
go func() {
time.Sleep(200 * time.Millisecond)
util.ReloadUI()
}()
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 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 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()
util.PushMsg(model.Conf.Language(42), 1000*15)
time.Sleep(time.Second * 3)
}
func setE2EEPasswd(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
var passwd string
mode := int(arg["mode"].(float64))
if 0 == mode { // 使用内建的密码生成
passwd = model.GetBuiltInE2EEPasswd()
} else { // 使用自定义密码
passwd = arg["e2eePasswd"].(string)
passwd = strings.TrimSpace(passwd)
}
if "" == passwd {
ret.Code = -1
ret.Msg = model.Conf.Language(39)
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
newPasswd := util.AESEncrypt(passwd)
if model.Conf.E2EEPasswd == newPasswd {
util.PushMsg(model.Conf.Language(92), 3000)
return
}
util.PushMsg(model.Conf.Language(102), 60*1000)
if err := os.RemoveAll(model.Conf.Backup.GetSaveDir()); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
if err := os.MkdirAll(model.Conf.Backup.GetSaveDir(), 0755); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
if err := os.RemoveAll(model.Conf.Sync.GetSaveDir()); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
if err := os.MkdirAll(model.Conf.Sync.GetSaveDir(), 0755); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
if err := os.RemoveAll(filepath.Join(util.WorkspaceDir, "incremental")); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
if err := os.MkdirAll(filepath.Join(util.WorkspaceDir, "incremental"), 0755); nil != err {
ret.Code = -1
ret.Msg = err.Error()
return
}
time.Sleep(1 * time.Second)
model.Conf.E2EEPasswd = newPasswd
model.Conf.E2EEPasswdMode = mode
model.Conf.Save()
util.PushMsg(model.Conf.Language(92), 3000)
time.Sleep(1 * time.Second)
model.SyncData(false, false, true)
}
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)
}
err := model.Close(force)
if nil != err {
ret.Code = 1
ret.Msg = err.Error() + "<div class=\"fn__space\"></div><button class=\"b3-button b3-button--white\">" + model.Conf.Language(97) + "</button>"
ret.Data = map[string]interface{}{"closeTimeout": 0}
return
}
}

84
kernel/api/tag.go Normal file
View file

@ -0,0 +1,84 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func getTag(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
sortParam := arg["sort"]
sortMode := model.Conf.Tag.Sort
if nil != sortParam {
sortMode = int(sortParam.(float64))
}
model.Conf.Tag.Sort = sortMode
model.Conf.Save()
ret.Data = model.BuildTags()
}
func renameTag(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
oldLabel := arg["oldLabel"].(string)
newLabel := arg["newLabel"].(string)
if err := model.RenameTag(oldLabel, newLabel); nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
}
func removeTag(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
label := arg["label"].(string)
if err := model.RemoveTag(label); nil != err {
ret.Code = -1
ret.Msg = err.Error()
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
}

71
kernel/api/template.go Normal file
View file

@ -0,0 +1,71 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"net/http"
"github.com/88250/gulu"
"github.com/88250/lute/html"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func docSaveAsTemplate(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
id := arg["id"].(string)
overwrite := arg["overwrite"].(bool)
code, err := model.DocSaveAsTemplate(id, overwrite)
if nil != err {
ret.Code = -1
ret.Msg = html.EscapeString(err.Error())
return
}
ret.Code = code
}
func renderTemplate(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
p := arg["path"].(string)
id := arg["id"].(string)
content, err := model.RenderTemplate(p, id)
if nil != err {
ret.Code = -1
ret.Msg = html.EscapeString(err.Error())
return
}
ret.Data = map[string]interface{}{
"path": p,
"content": content,
}
}

108
kernel/api/transaction.go Normal file
View file

@ -0,0 +1,108 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"fmt"
"net/http"
"time"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/util"
)
func performTransactions(c *gin.Context) {
start := time.Now()
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
trans := arg["transactions"]
data, err := gulu.JSON.MarshalJSON(trans)
if nil != err {
ret.Code = -1
ret.Msg = "parses request failed"
return
}
var transactions []*model.Transaction
if err = gulu.JSON.UnmarshalJSON(data, &transactions); nil != err {
ret.Code = -1
ret.Msg = "parses request failed"
return
}
if op := model.IsSetAttrs(&transactions); nil != op {
attrs := map[string]string{}
if err = gulu.JSON.UnmarshalJSON([]byte(op.Data.(string)), &attrs); nil != err {
return
}
err = model.SetBlockAttrs(op.ID, attrs)
} else {
err = model.PerformTransactions(&transactions)
}
if filesys.ErrUnableLockFile == err {
ret.Code = 1
return
}
if model.ErrNotFullyBoot == err {
ret.Code = -1
ret.Msg = fmt.Sprintf(model.Conf.Language(74), int(util.GetBootProgress()))
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
if nil != err {
tx, txErr := sql.BeginTx()
if nil != txErr {
util.LogFatalf("transaction failed: %s", txErr)
return
}
sql.ClearBoxHash(tx)
sql.CommitTx(tx)
util.LogFatalf("transaction failed: %s", err)
return
}
ret.Data = transactions
app := arg["app"].(string)
session := arg["session"].(string)
if model.IsFoldHeading(&transactions) || model.IsUnfoldHeading(&transactions) {
model.WaitForWritingFiles()
}
pushTransactions(app, session, transactions)
elapsed := time.Now().Sub(start).Milliseconds()
c.Header("Server-Timing", fmt.Sprintf("total;dur=%d", elapsed))
}
func pushTransactions(app, session string, transactions []*model.Transaction) {
evt := util.NewCmdResult("transactions", 0, util.PushModeBroadcastExcludeSelf, util.PushModeBroadcastExcludeSelf)
evt.AppId = app
evt.SessionId = session
evt.Data = transactions
util.PushEvent(evt)
}

107
kernel/api/workspace.go Normal file
View file

@ -0,0 +1,107 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 (
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/88250/gulu"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/util"
)
func listWorkspaceDirs(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
userHomeConfDir := filepath.Join(util.HomeDir, ".config", "siyuan")
workspaceConf := filepath.Join(userHomeConfDir, "workspace.json")
data, err := os.ReadFile(workspaceConf)
var workspacePaths []string
if err = gulu.JSON.UnmarshalJSON(data, &workspacePaths); nil != err {
util.LogErrorf("unmarshal workspace conf [%s] failed: %s", workspaceConf, err)
return
}
ret.Data = workspacePaths
}
func setWorkspaceDir(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
path := arg["path"].(string)
if util.WorkspaceDir == path {
ret.Code = -1
ret.Msg = model.Conf.Language(78)
ret.Data = map[string]interface{}{"closeTimeout": 3000}
return
}
if gulu.OS.IsWindows() {
installDir := filepath.Dir(util.WorkingDir)
if strings.HasPrefix(path, installDir) {
ret.Code = -1
ret.Msg = model.Conf.Language(98)
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
}
var workspacePaths []string
workspaceConf := filepath.Join(util.HomeDir, ".config", "siyuan", "workspace.json")
data, err := os.ReadFile(workspaceConf)
if nil != err {
util.LogErrorf("read workspace conf failed: %s", err)
} else {
if err = gulu.JSON.UnmarshalJSON(data, &workspacePaths); nil != err {
util.LogErrorf("unmarshal workspace conf failed: %s", err)
}
}
workspacePaths = append(workspacePaths, path)
workspacePaths = util.RemoveDuplicatedElem(workspacePaths)
workspacePaths = util.RemoveElem(workspacePaths, path)
workspacePaths = append(workspacePaths, path) // 切换的工作空间固定放在最后一个
if data, err = gulu.JSON.MarshalJSON(workspacePaths); nil != err {
msg := fmt.Sprintf("marshal workspace conf [%s] failed: %s", workspaceConf, err)
ret.Code = -1
ret.Msg = msg
return
} else {
if err = gulu.File.WriteFileSafer(workspaceConf, data, 0644); nil != err {
msg := fmt.Sprintf("create workspace conf [%s] failed: %s", workspaceConf, err)
ret.Code = -1
ret.Msg = msg
return
}
}
util.PushMsg(model.Conf.Language(42), 1000*15)
time.Sleep(time.Second * 3)
}

144
kernel/bazaar/icon.go Normal file
View file

@ -0,0 +1,144 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 bazaar
import (
"errors"
"os"
"sort"
"strings"
"sync"
"github.com/dustin/go-humanize"
ants "github.com/panjf2000/ants/v2"
"github.com/siyuan-note/siyuan/kernel/util"
)
type Icon struct {
Author string `json:"author"`
URL string `json:"url"`
Version string `json:"version"`
Name string `json:"name"`
RepoURL string `json:"repoURL"`
RepoHash string `json:"repoHash"`
PreviewURL string `json:"previewURL"`
PreviewURLThumb string `json:"previewURLThumb"`
README string `json:"readme"`
Installed bool `json:"installed"`
Outdated bool `json:"outdated"`
Current bool `json:"current"`
Updated string `json:"updated"`
Stars int `json:"stars"`
OpenIssues int `json:"openIssues"`
Size int64 `json:"size"`
HSize string `json:"hSize"`
HUpdated string `json:"hUpdated"`
Downloads int `json:"downloads"`
}
func Icons(proxyURL string) (icons []*Icon) {
icons = []*Icon{}
result, err := util.GetRhyResult(false, proxyURL)
if nil != err {
return
}
bazaarIndex := getBazaarIndex(proxyURL)
bazaarHash := result["bazaar"].(string)
result = map[string]interface{}{}
request := util.NewBrowserRequest(proxyURL)
u := util.BazaarOSSServer + "/bazaar@" + bazaarHash + "/stage/icons.json"
resp, err := request.SetResult(&result).Get(u)
if nil != err {
util.LogErrorf("get community stage index [%s] failed: %s", u, err)
return
}
if 200 != resp.StatusCode {
util.LogErrorf("get community stage index [%s] failed: %d", u, resp.StatusCode)
return
}
repos := result["repos"].([]interface{})
waitGroup := &sync.WaitGroup{}
lock := &sync.Mutex{}
p, _ := ants.NewPoolWithFunc(2, func(arg interface{}) {
defer waitGroup.Done()
repo := arg.(map[string]interface{})
repoURL := repo["url"].(string)
icon := &Icon{}
innerU := util.BazaarOSSServer + "/package/" + repoURL + "/icon.json"
innerResp, innerErr := util.NewBrowserRequest(proxyURL).SetResult(icon).Get(innerU)
if nil != innerErr {
util.LogErrorf("get bazaar package [%s] failed: %s", repoURL, innerErr)
return
}
if 200 != innerResp.StatusCode {
util.LogErrorf("get bazaar package [%s] failed: %d", innerU, innerResp.StatusCode)
return
}
repoURLHash := strings.Split(repoURL, "@")
icon.RepoURL = "https://github.com/" + repoURLHash[0]
icon.RepoHash = repoURLHash[1]
icon.PreviewURL = util.BazaarOSSServer + "/package/" + repoURL + "/preview.png?imageslim"
icon.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repoURL + "/preview.png?imageView2/2/w/436/h/232"
icon.Updated = repo["updated"].(string)
icon.Stars = int(repo["stars"].(float64))
icon.OpenIssues = int(repo["openIssues"].(float64))
icon.Size = int64(repo["size"].(float64))
icon.HSize = humanize.Bytes(uint64(icon.Size))
icon.HUpdated = formatUpdated(icon.Updated)
pkg := bazaarIndex[strings.Split(repoURL, "@")[0]]
if nil != pkg {
icon.Downloads = pkg.Downloads
}
lock.Lock()
icons = append(icons, icon)
lock.Unlock()
})
for _, repo := range repos {
waitGroup.Add(1)
p.Invoke(repo)
}
waitGroup.Wait()
p.Release()
sort.Slice(icons, func(i, j int) bool { return icons[i].Updated > icons[j].Updated })
return
}
func InstallIcon(repoURL, repoHash, installPath, proxyURL string, chinaCDN bool, systemID string) error {
repoURLHash := repoURL + "@" + repoHash
data, err := downloadPackage(repoURLHash, proxyURL, chinaCDN, true, systemID)
if nil != err {
return err
}
return installPackage(data, installPath)
}
func UninstallIcon(installPath string) error {
if err := os.RemoveAll(installPath); nil != err {
util.LogErrorf("remove icon [%s] failed: %s", installPath, err)
return errors.New("remove community icon failed")
}
//util.Logger.Infof("uninstalled icon [%s]", installPath)
return nil
}

206
kernel/bazaar/package.go Normal file
View file

@ -0,0 +1,206 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 bazaar
import (
"bytes"
"errors"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/88250/gulu"
"github.com/88250/lute"
"github.com/PuerkitoBio/goquery"
"github.com/araddon/dateparse"
"github.com/imroc/req/v3"
"github.com/siyuan-note/siyuan/kernel/util"
textUnicode "golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform"
)
func GetPackageREADME(repoURL, repoHash, proxyURL string, chinaCDN bool, systemID string) (ret string) {
repoURLHash := repoURL + "@" + repoHash
data, err := downloadPackage(repoURLHash+"/README.md", proxyURL, chinaCDN, false, systemID)
if nil != err {
ret = "Load bazaar package's README.md failed: " + err.Error()
return
}
if 2 < len(data) {
if 255 == data[0] && 254 == data[1] {
data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.LittleEndian, textUnicode.ExpectBOM).NewDecoder(), data)
} else if 254 == data[1] && 255 == data[0] {
data, _, err = transform.Bytes(textUnicode.UTF16(textUnicode.BigEndian, textUnicode.ExpectBOM).NewDecoder(), data)
}
}
luteEngine := lute.New()
luteEngine.SetSoftBreak2HardBreak(false)
luteEngine.SetCodeSyntaxHighlight(false)
linkBase := repoURL + "/blob/main/"
luteEngine.SetLinkBase(linkBase)
ret = luteEngine.Md2HTML(string(data))
doc, err := goquery.NewDocumentFromReader(strings.NewReader(ret))
if nil != err {
util.LogErrorf("parse HTML failed: %s", err)
return ret
}
doc.Find("a").Each(func(i int, selection *goquery.Selection) {
if href, ok := selection.Attr("href"); ok && util.IsRelativePath(href) {
selection.SetAttr("href", linkBase+href)
}
})
ret, _ = doc.Find("body").Html()
return
}
func downloadPackage(repoURLHash, proxyURL string, chinaCDN, pushProgress bool, systemID string) (data []byte, err error) {
// repoURLHash: https://github.com/88250/Comfortably-Numb@6286912c381ef3f83e455d06ba4d369c498238dc
pushID := repoURLHash[:strings.LastIndex(repoURLHash, "@")]
repoURLHash = strings.TrimPrefix(repoURLHash, "https://github.com/")
u := util.BazaarOSSFileServer + "/package/" + repoURLHash
if chinaCDN {
u = util.BazaarOSSServer + "/package/" + repoURLHash
}
buf := &bytes.Buffer{}
resp, err := util.NewBrowserDownloadRequest(proxyURL).SetOutput(buf).SetDownloadCallback(func(info req.DownloadInfo) {
if pushProgress {
util.PushDownloadProgress(pushID, float32(info.DownloadedSize)/float32(info.Response.ContentLength))
}
}).Get(u)
if nil != err {
u = util.BazaarOSSServer + "/package/" + repoURLHash
resp, err = util.NewBrowserDownloadRequest(proxyURL).SetOutput(buf).SetDownloadCallback(func(info req.DownloadInfo) {
if pushProgress {
util.PushDownloadProgress(pushID, float32(info.DownloadedSize)/float32(info.Response.ContentLength))
}
}).Get(u)
if nil != err {
util.LogErrorf("get bazaar package [%s] failed: %s", u, err)
return nil, errors.New("get bazaar package failed")
}
}
if 200 != resp.StatusCode {
util.LogErrorf("get bazaar package [%s] failed: %d", u, resp.StatusCode)
return nil, errors.New("get bazaar package failed")
}
data = buf.Bytes()
go incPackageDownloads(repoURLHash, proxyURL, systemID)
return
}
func incPackageDownloads(repoURLHash, proxyURL, systemID string) {
if strings.Contains(repoURLHash, ".md") {
return
}
repo := strings.Split(repoURLHash, "@")[0]
u := util.AliyunServer + "/apis/siyuan/bazaar/addBazaarPackageDownloadCount"
util.NewCloudRequest(proxyURL).SetBody(
map[string]interface{}{
"systemID": systemID,
"repo": repo,
}).Post(u)
}
func installPackage(data []byte, installPath string) (err error) {
dir := filepath.Join(util.TempDir, "bazaar", "package")
if err = os.MkdirAll(dir, 0755); nil != err {
return
}
name := gulu.Rand.String(7)
tmp := filepath.Join(dir, name+".zip")
if err = os.WriteFile(tmp, data, 0644); nil != err {
return
}
unzipPath := filepath.Join(dir, name)
if err = gulu.Zip.Unzip(tmp, unzipPath); nil != err {
util.LogErrorf("write file [%s] failed: %s", installPath, err)
err = errors.New("write file failed")
return
}
dirs, err := os.ReadDir(unzipPath)
if nil != err {
return
}
for _, d := range dirs {
if d.IsDir() && strings.Contains(d.Name(), "-") {
dir = d.Name()
break
}
}
srcPath := filepath.Join(unzipPath, dir)
if err = gulu.File.Copy(srcPath, installPath); nil != err {
return
}
return
}
func formatUpdated(updated string) (ret string) {
t, e := dateparse.ParseIn(updated, time.Now().Location())
if nil == e {
ret = t.Format("2006-01-02")
} else {
if strings.Contains(updated, "T") {
ret = updated[:strings.Index(updated, "T")]
} else {
ret = strings.ReplaceAll(strings.ReplaceAll(updated, "T", ""), "Z", "")
}
}
return
}
type bazaarPackage struct {
Name string `json:"name"`
Downloads int `json:"downloads"`
}
var cachedBazaarIndex = map[string]*bazaarPackage{}
var bazaarIndexCacheTime int64
var bazaarIndexLock = sync.Mutex{}
func getBazaarIndex(proxyURL string) map[string]*bazaarPackage {
bazaarIndexLock.Lock()
defer bazaarIndexLock.Unlock()
now := time.Now().Unix()
if 3600 >= now-bazaarIndexCacheTime {
return cachedBazaarIndex
}
request := util.NewBrowserRequest(proxyURL)
u := util.BazaarStatServer + "/bazaar/index.json"
resp, reqErr := request.SetResult(&cachedBazaarIndex).Get(u)
if nil != reqErr {
util.LogErrorf("get bazaar index [%s] failed: %s", u, reqErr)
return cachedBazaarIndex
}
if 200 != resp.StatusCode {
util.LogErrorf("get bazaar index [%s] failed: %d", u, resp.StatusCode)
return cachedBazaarIndex
}
bazaarIndexCacheTime = now
return cachedBazaarIndex
}

164
kernel/bazaar/template.go Normal file
View file

@ -0,0 +1,164 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 bazaar
import (
"errors"
"os"
"sort"
"strings"
"sync"
"time"
"github.com/dustin/go-humanize"
"github.com/panjf2000/ants/v2"
"github.com/siyuan-note/siyuan/kernel/util"
)
type Template struct {
Author string `json:"author"`
URL string `json:"url"`
Version string `json:"version"`
Name string `json:"name"`
RepoURL string `json:"repoURL"`
RepoHash string `json:"repoHash"`
PreviewURL string `json:"previewURL"`
PreviewURLThumb string `json:"previewURLThumb"`
README string `json:"readme"`
Installed bool `json:"installed"`
Outdated bool `json:"outdated"`
Updated string `json:"updated"`
Stars int `json:"stars"`
OpenIssues int `json:"openIssues"`
Size int64 `json:"size"`
HSize string `json:"hSize"`
HUpdated string `json:"hUpdated"`
Downloads int `json:"downloads"`
}
func Templates(proxyURL string) (templates []*Template) {
templates = []*Template{}
result, err := util.GetRhyResult(false, proxyURL)
if nil != err {
return
}
bazaarIndex := getBazaarIndex(proxyURL)
bazaarHash := result["bazaar"].(string)
result = map[string]interface{}{}
request := util.NewBrowserRequest(proxyURL)
u := util.BazaarOSSServer + "/bazaar@" + bazaarHash + "/stage/templates.json"
resp, reqErr := request.SetResult(&result).Get(u)
if nil != reqErr {
util.LogErrorf("get community stage index [%s] failed: %s", u, reqErr)
return
}
if 200 != resp.StatusCode {
util.LogErrorf("get community stage index [%s] failed: %d", u, resp.StatusCode)
return
}
repos := result["repos"].([]interface{})
waitGroup := &sync.WaitGroup{}
lock := &sync.Mutex{}
p, _ := ants.NewPoolWithFunc(2, func(arg interface{}) {
defer waitGroup.Done()
repo := arg.(map[string]interface{})
repoURL := repo["url"].(string)
template := &Template{}
innerU := util.BazaarOSSServer + "/package/" + repoURL + "/template.json"
innerResp, innerErr := util.NewBrowserRequest(proxyURL).SetResult(template).Get(innerU)
if nil != innerErr {
util.LogErrorf("get community template [%s] failed: %s", repoURL, innerErr)
return
}
if 200 != innerResp.StatusCode {
util.LogErrorf("get bazaar package [%s] failed: %d", innerU, innerResp.StatusCode)
return
}
repoURLHash := strings.Split(repoURL, "@")
template.RepoURL = "https://github.com/" + repoURLHash[0]
template.RepoHash = repoURLHash[1]
template.PreviewURL = util.BazaarOSSServer + "/package/" + repoURL + "/preview.png?imageslim"
template.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repoURL + "/preview.png?imageView2/2/w/436/h/232"
template.Updated = repo["updated"].(string)
template.Stars = int(repo["stars"].(float64))
template.OpenIssues = int(repo["openIssues"].(float64))
template.Size = int64(repo["size"].(float64))
template.HSize = humanize.Bytes(uint64(template.Size))
template.HUpdated = formatUpdated(template.Updated)
pkg := bazaarIndex[strings.Split(repoURL, "@")[0]]
if nil != pkg {
template.Downloads = pkg.Downloads
}
lock.Lock()
templates = append(templates, template)
lock.Unlock()
})
for _, repo := range repos {
waitGroup.Add(1)
p.Invoke(repo)
}
waitGroup.Wait()
p.Release()
templates = filterLegacyTemplates(templates)
sort.Slice(templates, func(i, j int) bool { return templates[i].Updated > templates[j].Updated })
return
}
func InstallTemplate(repoURL, repoHash, installPath, proxyURL string, chinaCDN bool, systemID string) error {
repoURLHash := repoURL + "@" + repoHash
data, err := downloadPackage(repoURLHash, proxyURL, chinaCDN, true, systemID)
if nil != err {
return err
}
return installPackage(data, installPath)
}
func UninstallTemplate(installPath string) error {
if err := os.RemoveAll(installPath); nil != err {
util.LogErrorf("remove template [%s] failed: %s", installPath, err)
return errors.New("remove community template failed")
}
return nil
}
func filterLegacyTemplates(templates []*Template) (ret []*Template) {
verTime, _ := time.Parse("2006-01-02T15:04:05", "2021-05-12T00:00:00")
for _, theme := range templates {
if "" != theme.Updated {
updated := theme.Updated[:len("2006-01-02T15:04:05")]
t, err := time.Parse("2006-01-02T15:04:05", updated)
if nil != err {
util.LogErrorf("convert update time [%s] failed: %s", updated, err)
continue
}
if t.After(verTime) {
ret = append(ret, theme)
}
}
}
return
}

146
kernel/bazaar/theme.go Normal file
View file

@ -0,0 +1,146 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 bazaar
import (
"errors"
"os"
"sort"
"strings"
"sync"
"github.com/dustin/go-humanize"
ants "github.com/panjf2000/ants/v2"
"github.com/siyuan-note/siyuan/kernel/util"
)
type Theme struct {
Author string `json:"author"`
URL string `json:"url"`
Version string `json:"version"`
Modes []string `json:"modes"`
Name string `json:"name"`
RepoURL string `json:"repoURL"`
RepoHash string `json:"repoHash"`
PreviewURL string `json:"previewURL"`
PreviewURLThumb string `json:"previewURLThumb"`
README string `json:"readme"`
Installed bool `json:"installed"`
Outdated bool `json:"outdated"`
Current bool `json:"current"`
Updated string `json:"updated"`
Stars int `json:"stars"`
OpenIssues int `json:"openIssues"`
Size int64 `json:"size"`
HSize string `json:"hSize"`
HUpdated string `json:"hUpdated"`
Downloads int `json:"downloads"`
}
func Themes(proxyURL string) (ret []*Theme) {
ret = []*Theme{}
result, err := util.GetRhyResult(false, proxyURL)
if nil != err {
return
}
bazaarIndex := getBazaarIndex(proxyURL)
bazaarHash := result["bazaar"].(string)
result = map[string]interface{}{}
request := util.NewBrowserRequest(proxyURL)
u := util.BazaarOSSServer + "/bazaar@" + bazaarHash + "/stage/themes.json"
resp, reqErr := request.SetResult(&result).Get(u)
if nil != reqErr {
util.LogErrorf("get community stage index [%s] failed: %s", u, reqErr)
return
}
if 200 != resp.StatusCode {
util.LogErrorf("get community stage index [%s] failed: %d", u, resp.StatusCode)
return
}
repos := result["repos"].([]interface{})
waitGroup := &sync.WaitGroup{}
lock := &sync.Mutex{}
p, _ := ants.NewPoolWithFunc(8, func(arg interface{}) {
defer waitGroup.Done()
repo := arg.(map[string]interface{})
repoURL := repo["url"].(string)
theme := &Theme{}
innerU := util.BazaarOSSServer + "/package/" + repoURL + "/theme.json"
innerResp, innerErr := util.NewBrowserRequest(proxyURL).SetResult(theme).Get(innerU)
if nil != innerErr {
util.LogErrorf("get bazaar package [%s] failed: %s", innerU, innerErr)
return
}
if 200 != innerResp.StatusCode {
util.LogErrorf("get bazaar package [%s] failed: %d", innerU, resp.StatusCode)
return
}
repoURLHash := strings.Split(repoURL, "@")
theme.RepoURL = "https://github.com/" + repoURLHash[0]
theme.RepoHash = repoURLHash[1]
theme.PreviewURL = util.BazaarOSSServer + "/package/" + repoURL + "/preview.png?imageslim"
theme.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repoURL + "/preview.png?imageView2/2/w/436/h/232"
theme.Updated = repo["updated"].(string)
theme.Stars = int(repo["stars"].(float64))
theme.OpenIssues = int(repo["openIssues"].(float64))
theme.Size = int64(repo["size"].(float64))
theme.HSize = humanize.Bytes(uint64(theme.Size))
theme.HUpdated = formatUpdated(theme.Updated)
pkg := bazaarIndex[strings.Split(repoURL, "@")[0]]
if nil != pkg {
theme.Downloads = pkg.Downloads
}
lock.Lock()
ret = append(ret, theme)
lock.Unlock()
})
for _, repo := range repos {
waitGroup.Add(1)
p.Invoke(repo)
}
waitGroup.Wait()
p.Release()
sort.Slice(ret, func(i, j int) bool { return ret[i].Updated > ret[j].Updated })
return
}
func InstallTheme(repoURL, repoHash, installPath, proxyURL string, chinaCDN bool, systemID string) error {
repoURLHash := repoURL + "@" + repoHash
data, err := downloadPackage(repoURLHash, proxyURL, chinaCDN, true, systemID)
if nil != err {
return err
}
return installPackage(data, installPath)
}
func UninstallTheme(installPath string) error {
if err := os.RemoveAll(installPath); nil != err {
util.LogErrorf("remove theme [%s] failed: %s", installPath, err)
return errors.New("remove community theme failed")
}
//util.Logger.Infof("uninstalled theme [%s]", installPath)
return nil
}

145
kernel/bazaar/widget.go Normal file
View file

@ -0,0 +1,145 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 bazaar
import (
"errors"
"os"
"sort"
"strings"
"sync"
"github.com/dustin/go-humanize"
ants "github.com/panjf2000/ants/v2"
"github.com/siyuan-note/siyuan/kernel/util"
)
type Widget struct {
Author string `json:"author"`
URL string `json:"url"`
Version string `json:"version"`
Name string `json:"name"`
RepoURL string `json:"repoURL"`
RepoHash string `json:"repoHash"`
PreviewURL string `json:"previewURL"`
PreviewURLThumb string `json:"previewURLThumb"`
README string `json:"readme"`
Installed bool `json:"installed"`
Outdated bool `json:"outdated"`
Current bool `json:"current"`
Updated string `json:"updated"`
Stars int `json:"stars"`
OpenIssues int `json:"openIssues"`
Size int64 `json:"size"`
HSize string `json:"hSize"`
HUpdated string `json:"hUpdated"`
Downloads int `json:"downloads"`
}
func Widgets(proxyURL string) (widgets []*Widget) {
widgets = []*Widget{}
result, err := util.GetRhyResult(false, proxyURL)
if nil != err {
return
}
bazaarIndex := getBazaarIndex(proxyURL)
bazaarHash := result["bazaar"].(string)
result = map[string]interface{}{}
request := util.NewBrowserRequest(proxyURL)
u := util.BazaarOSSServer + "/bazaar@" + bazaarHash + "/stage/widgets.json"
resp, err := request.SetResult(&result).Get(u)
if nil != err {
util.LogErrorf("get community stage index [%s] failed: %s", u, err)
return
}
if 200 != resp.StatusCode {
util.LogErrorf("get community stage index [%s] failed: %d", u, resp.StatusCode)
return
}
repos := result["repos"].([]interface{})
waitGroup := &sync.WaitGroup{}
lock := &sync.Mutex{}
p, _ := ants.NewPoolWithFunc(8, func(arg interface{}) {
defer waitGroup.Done()
repo := arg.(map[string]interface{})
repoURL := repo["url"].(string)
widget := &Widget{}
innerU := util.BazaarOSSServer + "/package/" + repoURL + "/widget.json"
innerResp, innerErr := util.NewBrowserRequest(proxyURL).SetResult(widget).Get(innerU)
if nil != innerErr {
util.LogErrorf("get bazaar package [%s] failed: %s", repoURL, innerErr)
return
}
if 200 != innerResp.StatusCode {
util.LogErrorf("get bazaar package [%s] failed: %d", innerU, innerResp.StatusCode)
return
}
repoURLHash := strings.Split(repoURL, "@")
widget.RepoURL = "https://github.com/" + repoURLHash[0]
widget.RepoHash = repoURLHash[1]
widget.PreviewURL = util.BazaarOSSServer + "/package/" + repoURL + "/preview.png?imageslim"
widget.PreviewURLThumb = util.BazaarOSSServer + "/package/" + repoURL + "/preview.png?imageView2/2/w/436/h/232"
widget.Updated = repo["updated"].(string)
widget.Stars = int(repo["stars"].(float64))
widget.OpenIssues = int(repo["openIssues"].(float64))
widget.Size = int64(repo["size"].(float64))
widget.HSize = humanize.Bytes(uint64(widget.Size))
widget.HUpdated = formatUpdated(widget.Updated)
pkg := bazaarIndex[strings.Split(repoURL, "@")[0]]
if nil != pkg {
widget.Downloads = pkg.Downloads
}
lock.Lock()
widgets = append(widgets, widget)
lock.Unlock()
})
for _, repo := range repos {
waitGroup.Add(1)
p.Invoke(repo)
}
waitGroup.Wait()
p.Release()
sort.Slice(widgets, func(i, j int) bool { return widgets[i].Updated > widgets[j].Updated })
return
}
func InstallWidget(repoURL, repoHash, installPath, proxyURL string, chinaCDN bool, systemID string) error {
repoURLHash := repoURL + "@" + repoHash
data, err := downloadPackage(repoURLHash, proxyURL, chinaCDN, true, systemID)
if nil != err {
return err
}
return installPackage(data, installPath)
}
func UninstallWidget(installPath string) error {
if err := os.RemoveAll(installPath); nil != err {
util.LogErrorf("remove widget [%s] failed: %s", installPath, err)
return errors.New("remove community widget failed")
}
//util.Logger.Infof("uninstalled widget [%s]", installPath)
return nil
}

77
kernel/cache/ial.go vendored Normal file
View file

@ -0,0 +1,77 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 cache
import (
"strings"
"github.com/88250/lute/util"
"github.com/dgraph-io/ristretto"
)
var docIALCache, _ = ristretto.NewCache(&ristretto.Config{
NumCounters: 100000, // 10W
MaxCost: 1024 * 1024 * 10, // 10MB
BufferItems: 64,
})
func PutDocIAL(p string, ial map[string]string) {
docIALCache.Set(p, ial, 1)
}
func GetDocIAL(p string) (ret map[string]string) {
ial, _ := docIALCache.Get(p)
if nil == ial {
return
}
ret = map[string]string{}
for k, v := range ial.(map[string]string) {
ret[k] = strings.ReplaceAll(v, util.IALValEscNewLine, "\n")
}
return
}
func RemoveDocIAL(p string) {
docIALCache.Del(p)
}
func ClearDocsIAL() {
docIALCache.Clear()
}
var blockIALCache, _ = ristretto.NewCache(&ristretto.Config{
NumCounters: 100000, // 10W
MaxCost: 1024 * 1024 * 10, // 10MB
BufferItems: 64,
})
func PutBlockIAL(id string, ial map[string]string) {
blockIALCache.Set(id, ial, 1)
}
func GetBlockIAL(id string) (ret map[string]string) {
ial, _ := blockIALCache.Get(id)
if nil == ial {
return
}
return ial.(map[string]string)
}
func RemoveBlockIAL(id string) {
blockIALCache.Del(id)
}

39
kernel/cmd/closews.go Normal file
View file

@ -0,0 +1,39 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 cmd
import (
"github.com/siyuan-note/siyuan/kernel/util"
)
type closews struct {
*BaseCmd
}
func (cmd *closews) Exec() {
id, _ := cmd.session.Get("id")
util.ClosePushChan(id.(string))
cmd.Push()
}
func (cmd *closews) Name() string {
return "closews"
}
func (cmd *closews) IsRead() bool {
return true
}

86
kernel/cmd/cmd.go Normal file
View file

@ -0,0 +1,86 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 cmd
import (
"github.com/88250/melody"
"github.com/siyuan-note/siyuan/kernel/util"
)
type Cmd interface {
Name() string
IsRead() bool // 非读即写
Id() float64
Exec()
}
type BaseCmd struct {
id float64
param map[string]interface{}
session *melody.Session
PushPayload *util.Result
}
func (cmd *BaseCmd) Id() float64 {
return cmd.id
}
func (cmd *BaseCmd) Push() {
cmd.PushPayload.Callback = cmd.param["callback"]
appId, _ := cmd.session.Get("app")
cmd.PushPayload.AppId = appId.(string)
sid, _ := cmd.session.Get("id")
cmd.PushPayload.SessionId = sid.(string)
util.PushEvent(cmd.PushPayload)
}
func NewCommand(cmdStr string, cmdId float64, param map[string]interface{}, session *melody.Session) (ret Cmd) {
baseCmd := &BaseCmd{id: cmdId, param: param, session: session}
switch cmdStr {
case "closews":
ret = &closews{baseCmd}
}
if nil == ret {
return
}
pushMode := util.PushModeSingleSelf
if pushModeParam := param["pushMode"]; nil != pushModeParam {
pushMode = util.PushMode(pushModeParam.(float64))
}
reloadPushMode := util.PushModeSingleSelf
if reloadPushModeParam := param["reloadPushMode"]; nil != reloadPushModeParam {
reloadPushMode = util.PushMode(reloadPushModeParam.(float64))
}
baseCmd.PushPayload = util.NewCmdResult(ret.Name(), cmdId, pushMode, reloadPushMode)
appId, _ := baseCmd.session.Get("app")
baseCmd.PushPayload.AppId = appId.(string)
sid, _ := baseCmd.session.Get("id")
baseCmd.PushPayload.SessionId = sid.(string)
return
}
func Exec(cmd Cmd) {
go func() {
//start := time.Now()
defer util.Recover()
cmd.Exec()
//end := time.Now()
//util.Logger.Infof("cmd [%s] exec consumed [%d]ms", cmd.Name(), end.Sub(start).Milliseconds())
}()
}

29
kernel/conf/account.go Normal file
View file

@ -0,0 +1,29 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
type Account struct {
DisplayTitle bool `json:"displayTitle"`
DisplayVIP bool `json:"displayVIP"`
}
func NewAccount() *Account {
return &Account{
DisplayTitle: true,
DisplayVIP: true,
}
}

29
kernel/conf/api.go Normal file
View file

@ -0,0 +1,29 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
import "github.com/88250/gulu"
type API struct {
Token string `json:"token"`
}
func NewAPI() *API {
return &API{
Token: gulu.Rand.String(16),
}
}

51
kernel/conf/appearance.go Normal file
View file

@ -0,0 +1,51 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
type Appearance struct {
Mode int `json:"mode"` // 模式0明亮1暗黑2随日出日落自动切换
DarkThemes []string `json:"darkThemes"` // 暗黑模式外观主题列表
LightThemes []string `json:"lightThemes"` // 明亮模式外观主题列表
ThemeDark string `json:"themeDark"` // 选择的暗黑模式外观主题
ThemeLight string `json:"themeLight"` // 选择的明亮模式外观主题
ThemeVer string `json:"themeVer"` // 选择的主题版本
Icons []string `json:"icons"` // 图标列表
Icon string `json:"icon"` // 选择的图标
IconVer string `json:"iconVer"` // 选择的图标版本
NativeEmoji bool `json:"nativeEmoji"` // 文档图标是否使用系统原生 Emoji
CodeBlockThemeLight string `json:"codeBlockThemeLight"` // 明亮模式下代码块主题
CodeBlockThemeDark string `json:"codeBlockThemeDark"` // 暗黑模式下代码块主题
Lang string `json:"lang"` // 选择的界面语言,同 AppConf.Lang
CustomCSS bool `json:"customCSS"` // 是否启用自定义主题
ThemeJS bool `json:"themeJS"` // 是否启用了主题 JavaScript
CloseButtonBehavior int `json:"closeButtonBehavior"` // 关闭按钮行为0退出1最小化到托盘
}
func NewAppearance() *Appearance {
return &Appearance{
Mode: 0,
ThemeDark: "midnight",
ThemeLight: "daylight",
Icon: "material",
NativeEmoji: true,
CodeBlockThemeLight: "github",
CodeBlockThemeDark: "base16/dracula",
Lang: "en_US",
CustomCSS: false,
CloseButtonBehavior: 0,
}
}

34
kernel/conf/backup.go Normal file
View file

@ -0,0 +1,34 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
import (
"path/filepath"
"github.com/siyuan-note/siyuan/kernel/util"
)
type Backup struct {
}
func NewBackup() *Backup {
return &Backup{}
}
func (b *Backup) GetSaveDir() string {
return filepath.Join(util.WorkspaceDir, "backup")
}

40
kernel/conf/box.go Normal file
View file

@ -0,0 +1,40 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
// BoxConf 维护 .siyuan/conf.json 笔记本配置。
type BoxConf struct {
Name string `json:"name"` // 笔记本名称
Sort int `json:"sort"` // 排序字段
Icon string `json:"icon"` // 图标
Closed bool `json:"closed"` // 是否处于关闭状态
RefCreateSavePath string `json:"refCreateSavePath"` // 块引时新建文档存储文件夹路径
CreateDocNameTemplate string `json:"createDocNameTemplate"` // 新建文档名模板
DailyNoteSavePath string `json:"dailyNoteSavePath"` // 新建日记存储路径
DailyNoteTemplatePath string `json:"dailyNoteTemplatePath"` // 新建日记使用的模板路径
}
func NewBoxConf() *BoxConf {
return &BoxConf{
Name: "Untitled",
Closed: true,
RefCreateSavePath: "",
CreateDocNameTemplate: "",
DailyNoteSavePath: "/daily note/{{now | date \"2006/01\"}}/{{now | date \"2006-01-02\"}}",
DailyNoteTemplatePath: "",
}
}

53
kernel/conf/editor.go Normal file
View file

@ -0,0 +1,53 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
type Editor struct {
FontSize int `json:"fontSize"`
FontFamily string `json:"fontFamily"`
CodeSyntaxHighlightLineNum bool `json:"codeSyntaxHighlightLineNum"`
CodeTabSpaces int `json:"codeTabSpaces"` // 代码块中 Tab 转换空格数,配置为 0 则表示不转换
CodeLineWrap bool `json:"codeLineWrap"` // 代码块是否自动折行
CodeLigatures bool `json:"codeLigatures"` // 代码块是否连字
DisplayBookmarkIcon bool `json:"displayBookmarkIcon"`
DisplayNetImgMark bool `json:"displayNetImgMark"`
GenerateHistoryInterval int `json:"generateHistoryInterval"` // 生成历史时间间隔,单位:分钟
HistoryRetentionDays int `json:"historyRetentionDays"` // 历史保留天数
Emoji []string `json:"emoji"` // 常用表情
VirtualBlockRef bool `json:"virtualBlockRef"` // 是否启用虚拟引用
VirtualBlockRefExclude string `json:"virtualBlockRefExclude"` // 虚拟引用关键字排除列表
BlockRefDynamicAnchorTextMaxLen int `json:"blockRefDynamicAnchorTextMaxLen"` // 块引动态锚文本最大长度
PlantUMLServePath string `json:"plantUMLServePath"` // PlantUML 伺服地址
}
func NewEditor() *Editor {
return &Editor{
FontSize: 16,
CodeSyntaxHighlightLineNum: true,
CodeTabSpaces: 0,
CodeLineWrap: false,
CodeLigatures: false,
DisplayBookmarkIcon: true,
DisplayNetImgMark: false,
GenerateHistoryInterval: 10,
HistoryRetentionDays: 30,
Emoji: []string{},
VirtualBlockRef: false,
BlockRefDynamicAnchorTextMaxLen: 64,
PlantUMLServePath: "https://www.plantuml.com/plantuml/svg/~1",
}
}

45
kernel/conf/export.go Normal file
View file

@ -0,0 +1,45 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
type Export struct {
ParagraphBeginningSpace bool `json:"paragraphBeginningSpace"` // 是否使用中文排版段落开头空两格
AddTitle bool `json:"addTitle"` // 是否添加标题
BlockRefMode int `json:"blockRefMode"` // 内容块引用导出模式2锚文本块链3仅锚文本4块引转脚注0使用原始文本1使用 Blockquote。0 和 1 都已经废弃 https://github.com/siyuan-note/siyuan/issues/3155
BlockEmbedMode int `json:"blockEmbedMode"` // 内容块引用导出模式0使用原始文本1使用 Blockquote
BlockRefTextLeft string `json:"blockRefTextLeft"` // 内容块引用导出锚文本左侧符号,默认留空
BlockRefTextRight string `json:"blockRefTextRight"` // 内容块引用导出锚文本右侧符号,默认留空
TagOpenMarker string `json:"tagOpenMarker"` // 标签开始标记符,默认是 #
TagCloseMarker string `json:"tagCloseMarker"` // 标签结束标记符,默认是 #
FileAnnotationRefMode int `json:"fileAnnotationRefMode"` // 文件标注引用导出模式0文件名 - 页码 - 锚文本1仅锚文本
PandocBin string `json:"pandocBin"` // Pandoc 可执行文件路径
}
func NewExport() *Export {
return &Export{
ParagraphBeginningSpace: false,
AddTitle: true,
BlockRefMode: 4,
BlockEmbedMode: 1,
BlockRefTextLeft: "",
BlockRefTextRight: "",
TagOpenMarker: "#",
TagCloseMarker: "#",
FileAnnotationRefMode: 0,
PandocBin: "",
}
}

43
kernel/conf/filetree.go Normal file
View file

@ -0,0 +1,43 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
import (
"github.com/siyuan-note/siyuan/kernel/util"
)
type FileTree struct {
AlwaysSelectOpenedFile bool `json:"alwaysSelectOpenedFile"` // 是否自动选中当前打开的文件
OpenFilesUseCurrentTab bool `json:"openFilesUseCurrentTab"` // 在当前页签打开文件
RefCreateSavePath string `json:"refCreateSavePath"` // 块引时新建文档存储文件夹路径
CreateDocNameTemplate string `json:"createDocNameTemplate"` // 新建文档名模板
MaxListCount int `json:"maxListCount"` // 最大列出数量
AllowCreateDeeper bool `json:"allowCreateDeeper"` // 允许创建超过 7 层深度的子文档
Sort int `json:"sort"` // 排序方式
}
func NewFileTree() *FileTree {
return &FileTree{
AlwaysSelectOpenedFile: false,
OpenFilesUseCurrentTab: false,
Sort: util.SortModeCustom,
CreateDocNameTemplate: "",
MaxListCount: 512,
AllowCreateDeeper: false,
}
}

99
kernel/conf/graph.go Normal file
View file

@ -0,0 +1,99 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
type Graph struct {
MaxBlocks int `json:"maxBlocks"` // 内容块最大显示数
Local *LocalGraph `json:"local"` // 局部图
Global *GlobalGraph `json:"global"` // 全局图
}
func NewGraph() *Graph {
return &Graph{
MaxBlocks: 1024 * 10,
Local: NewLocalGraph(),
Global: NewGlobalGraph(),
}
}
type LocalGraph struct {
DailyNote bool `json:"dailyNote"`
*TypeFilter `json:"type"`
*D3 `json:"d3"`
}
func NewLocalGraph() *LocalGraph {
return &LocalGraph{
DailyNote: false,
TypeFilter: &TypeFilter{},
D3: newD3(),
}
}
type GlobalGraph struct {
MinRefs int `json:"minRefs"` // 引用次数
DailyNote bool `json:"dailyNote"`
*TypeFilter `json:"type"`
*D3 `json:"d3"`
}
func NewGlobalGraph() *GlobalGraph {
return &GlobalGraph{
MinRefs: 0,
DailyNote: false,
TypeFilter: &TypeFilter{},
D3: newD3(),
}
}
type TypeFilter struct {
Tag bool `json:"tag"`
Paragraph bool `json:"paragraph"`
Heading bool `json:"heading"`
Math bool `json:"math"`
Code bool `json:"code"`
Table bool `json:"table"`
List bool `json:"list"`
ListItem bool `json:"listItem"`
Blockquote bool `json:"blockquote"`
Super bool `json:"super"`
}
type D3 struct {
NodeSize float64 `json:"nodeSize"`
LineWidth float64 `json:"linkWidth"`
LineOpacity float64 `json:"lineOpacity"`
CenterStrength float64 `json:"centerStrength"`
CollideRadius float64 `json:"collideRadius"`
CollideStrength float64 `json:"collideStrength"`
LinkDistance int `json:"linkDistance"`
Arrow bool `json:"arrow"`
}
func newD3() *D3 {
return &D3{
NodeSize: 15.0,
LineWidth: 8,
LineOpacity: 0.36,
CenterStrength: 0.01,
CollideRadius: 600,
CollideStrength: 0.08,
LinkDistance: 400,
Arrow: true,
}
}

22
kernel/conf/lang.go Normal file
View file

@ -0,0 +1,22 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
type Lang struct {
Label string `json:"label"` // 简体中文
Name string `json:"name"` // zh_CN
}

20
kernel/conf/layout.go Normal file
View file

@ -0,0 +1,20 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
type UILayout map[string]interface{} // 界面布局
type Keymap map[string]interface{} // 快捷键

189
kernel/conf/search.go Normal file
View file

@ -0,0 +1,189 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
import (
"bytes"
"strings"
"github.com/88250/lute/ast"
"github.com/siyuan-note/siyuan/kernel/treenode"
)
type Search struct {
Document bool `json:"document"`
Heading bool `json:"heading"`
List bool `json:"list"`
ListItem bool `json:"listItem"`
CodeBlock bool `json:"codeBlock"`
MathBlock bool `json:"mathBlock"`
Table bool `json:"table"`
Blockquote bool `json:"blockquote"`
SuperBlock bool `json:"superBlock"`
Paragraph bool `json:"paragraph"`
HTMLBlock bool `json:"htmlBlock"`
Limit int `json:"limit"`
CaseSensitive bool `json:"caseSensitive"`
Name bool `json:"name"`
Alias bool `json:"alias"`
Memo bool `json:"memo"`
Custom bool `json:"custom"`
BacklinkMentionName bool `json:"backlinkMentionName"`
BacklinkMentionAlias bool `json:"backlinkMentionAlias"`
BacklinkMentionAnchor bool `json:"backlinkMentionAnchor"`
BacklinkMentionDoc bool `json:"backlinkMentionDoc"`
VirtualRefName bool `json:"virtualRefName"`
VirtualRefAlias bool `json:"virtualRefAlias"`
VirtualRefAnchor bool `json:"virtualRefAnchor"`
VirtualRefDoc bool `json:"virtualRefDoc"`
}
func NewSearch() *Search {
return &Search{
Document: true,
Heading: true,
List: true,
ListItem: true,
CodeBlock: true,
MathBlock: true,
Table: true,
Blockquote: true,
SuperBlock: true,
Paragraph: true,
HTMLBlock: true,
Limit: 64,
CaseSensitive: false,
Name: true,
Alias: true,
Memo: true,
Custom: false,
BacklinkMentionName: true,
BacklinkMentionAlias: false,
BacklinkMentionAnchor: true,
BacklinkMentionDoc: true,
VirtualRefName: true,
VirtualRefAlias: false,
VirtualRefAnchor: true,
VirtualRefDoc: true,
}
}
func (s *Search) NAMFilter(keyword string) string {
keyword = strings.TrimSpace(keyword)
buf := bytes.Buffer{}
if s.Name {
buf.WriteString(" OR name LIKE '%" + keyword + "%'")
}
if s.Alias {
buf.WriteString(" OR alias LIKE '%" + keyword + "%'")
}
if s.Memo {
buf.WriteString(" OR memo LIKE '%" + keyword + "%'")
}
if s.Custom {
buf.WriteString(" OR ial LIKE '%=%" + keyword + "%'")
}
return buf.String()
}
func (s *Search) TypeFilter() string {
buf := bytes.Buffer{}
if s.Document {
buf.WriteByte('\'')
buf.WriteString(treenode.TypeAbbr(ast.NodeDocument.String()))
buf.WriteByte('\'')
buf.WriteString(",")
}
if s.Heading {
buf.WriteByte('\'')
buf.WriteString(treenode.TypeAbbr(ast.NodeHeading.String()))
buf.WriteByte('\'')
buf.WriteString(",")
}
if s.List {
buf.WriteByte('\'')
buf.WriteString(treenode.TypeAbbr(ast.NodeList.String()))
buf.WriteByte('\'')
buf.WriteString(",")
}
if s.ListItem {
buf.WriteByte('\'')
buf.WriteString(treenode.TypeAbbr(ast.NodeListItem.String()))
buf.WriteByte('\'')
buf.WriteString(",")
}
if s.CodeBlock {
buf.WriteByte('\'')
buf.WriteString(treenode.TypeAbbr(ast.NodeCodeBlock.String()))
buf.WriteByte('\'')
buf.WriteString(",")
}
if s.MathBlock {
buf.WriteByte('\'')
buf.WriteString(treenode.TypeAbbr(ast.NodeMathBlock.String()))
buf.WriteByte('\'')
buf.WriteString(",")
}
if s.Table {
buf.WriteByte('\'')
buf.WriteString(treenode.TypeAbbr(ast.NodeTable.String()))
buf.WriteByte('\'')
buf.WriteString(",")
}
if s.Blockquote {
buf.WriteByte('\'')
buf.WriteString(treenode.TypeAbbr(ast.NodeBlockquote.String()))
buf.WriteByte('\'')
buf.WriteString(",")
}
if s.SuperBlock {
buf.WriteByte('\'')
buf.WriteString(treenode.TypeAbbr(ast.NodeSuperBlock.String()))
buf.WriteByte('\'')
buf.WriteString(",")
}
if s.Paragraph {
buf.WriteByte('\'')
buf.WriteString(treenode.TypeAbbr(ast.NodeParagraph.String()))
buf.WriteByte('\'')
buf.WriteString(",")
}
if s.HTMLBlock {
buf.WriteByte('\'')
buf.WriteString(treenode.TypeAbbr(ast.NodeHTMLBlock.String()))
buf.WriteByte('\'')
buf.WriteString(",")
}
// 无法搜索到 iframe 块、视频块和音频块 https://github.com/siyuan-note/siyuan/issues/3604
buf.WriteString("'iframe','query_embed','video','audio',")
// 挂件块支持内置属性搜索 https://github.com/siyuan-note/siyuan/issues/4497
buf.WriteString("'widget',")
ret := buf.String()
if "" == ret {
return ret
}
return "(" + ret[:len(ret)-1] + ")"
}

27
kernel/conf/stat.go Normal file
View file

@ -0,0 +1,27 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
type Stat struct {
DocCount int `json:"docCount"` // 总文档计数
}
func NewStat() *Stat {
return &Stat{
DocCount: 0,
}
}

43
kernel/conf/sync.go Normal file
View file

@ -0,0 +1,43 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
import (
"path/filepath"
"github.com/siyuan-note/siyuan/kernel/util"
)
type Sync struct {
CloudName string `json:"cloudName"` // 云端同步目录名称
Enabled bool `json:"enabled"` // 是否开启同步
Uploaded int64 `json:"uploaded"` // 最近上传时间
Downloaded int64 `json:"downloaded"` // 最近下载时间
Synced int64 `json:"synced"` // 最近同步时间
Stat string `json:"stat"` // 最近同步统计信息
}
func NewSync() *Sync {
return &Sync{
CloudName: "main",
Enabled: true,
}
}
func (s *Sync) GetSaveDir() string {
return filepath.Join(util.WorkspaceDir, "sync")
}

61
kernel/conf/system.go Normal file
View file

@ -0,0 +1,61 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
import (
"github.com/siyuan-note/siyuan/kernel/util"
)
type System struct {
ID string `json:"id"`
KernelVersion string `json:"kernelVersion"`
OS string `json:"os"`
Container string `json:"container"` // docker, android, ios, std
IsInsider bool `json:"isInsider"`
HomeDir string `json:"homeDir"`
WorkspaceDir string `json:"workspaceDir"`
AppDir string `json:"appDir"`
ConfDir string `json:"confDir"`
DataDir string `json:"dataDir"`
NetworkServe bool `json:"networkServe"`
NetworkProxy *NetworkProxy `json:"networkProxy"`
UploadErrLog bool `json:"uploadErrLog"`
}
func NewSystem() *System {
return &System{
ID: util.GetDeviceID(),
KernelVersion: util.Ver,
NetworkProxy: &NetworkProxy{},
}
}
type NetworkProxy struct {
Scheme string `json:"scheme"`
Host string `json:"host"`
Port string `json:"port"`
}
func (np *NetworkProxy) String() string {
if "" == np.Scheme {
return ""
}
return np.Scheme + "://" + np.Host + ":" + np.Port
}

31
kernel/conf/tag.go Normal file
View file

@ -0,0 +1,31 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
import (
"github.com/siyuan-note/siyuan/kernel/util"
)
type Tag struct {
Sort int `json:"sort"` // 排序方式
}
func NewTag() *Tag {
return &Tag{
Sort: util.SortModeAlphanumASC,
}
}

44
kernel/conf/user.go Normal file
View file

@ -0,0 +1,44 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 conf
type User struct {
UserId string `json:"userId"`
UserName string `json:"userName"`
UserAvatarURL string `json:"userAvatarURL"`
UserHomeBImgURL string `json:"userHomeBImgURL"`
UserTitles []*UserTitle `json:"userTitles"`
UserIntro string `json:"userIntro"`
UserNickname string `json:"userNickname"`
UserCreateTime string `json:"userCreateTime"`
UserPaymentSum string `json:"userPaymentSum"`
UserSiYuanProExpireTime float64 `json:"userSiYuanProExpireTime"`
UserToken string `json:"userToken"`
UserTokenExpireTime string `json:"userTokenExpireTime"`
UserSiYuanRepoSize float64 `json:"userSiYuanRepoSize"`
UserTrafficUpload float64 `json:"userTrafficUpload"`
UserTrafficDownload float64 `json:"userTrafficDownload"`
UserTrafficTime float64 `json:"userTrafficTime"`
UserSiYuanSubscriptionPlan float64 `json:"userSiYuanSubscriptionPlan"` // -2未订阅-1试用0标准订阅1教育订阅
UserSiYuanSubscriptionStatus float64 `json:"userSiYuanSubscriptionStatus"` // -1未订阅0订阅可用1订阅封禁2订阅过期
}
type UserTitle struct {
Name string `json:"name"`
Desc string `json:"desc"`
Icon string `json:"icon"`
}

238
kernel/filesys/filelock.go Normal file
View file

@ -0,0 +1,238 @@
// SiYuan - Build Your Eternal Digital Garden
// 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/>.
//go:build !android && !ios
package filesys
import (
"errors"
"io"
"os"
"strings"
"sync"
"time"
"github.com/88250/flock"
"github.com/88250/gulu"
"github.com/siyuan-note/siyuan/kernel/util"
)
var ErrUnableLockFile = errors.New("unable to lock file")
var (
fileLocks = sync.Map{}
expiration = 5 * time.Minute
fileReadWriteLock = sync.Mutex{}
)
type LockItem struct {
fl *flock.Flock
expired int64
}
func init() {
go func() {
// 锁定超时自动解锁
for range time.Tick(10 * time.Second) {
fileReadWriteLock.Lock()
now := time.Now().UnixNano()
var expiredKeys []string
fileLocks.Range(func(k, v interface{}) bool {
lockItem := v.(*LockItem)
if now > lockItem.expired {
expiredKeys = append(expiredKeys, k.(string))
}
return true
})
for _, k := range expiredKeys {
if err := unlockFile0(k); nil != err {
util.LogErrorf("unlock file [%s] failed: %s", k, err)
continue
}
//util.LogInfof("released file lock [%s]", k)
}
fileReadWriteLock.Unlock()
}
}()
}
func ReleaseFileLocks(localAbsPath string) {
fileReadWriteLock.Lock()
defer fileReadWriteLock.Unlock()
fileLocks.Range(func(k, v interface{}) bool {
if strings.HasPrefix(k.(string), localAbsPath) {
if err := unlockFile0(k.(string)); nil != err {
util.LogErrorf("unlock file [%s] failed: %s", k, err)
}
}
return true
})
}
func ReleaseAllFileLocks() {
fileReadWriteLock.Lock()
defer fileReadWriteLock.Unlock()
fileLocks.Range(func(k, v interface{}) bool {
if err := unlockFile0(k.(string)); nil != err {
util.LogErrorf("unlock file [%s] failed: %s", k, err)
}
return true
})
}
func NoLockFileRead(filePath string) (data []byte, err error) {
fileReadWriteLock.Lock()
defer fileReadWriteLock.Unlock()
v, ok := fileLocks.Load(filePath)
if !ok {
return os.ReadFile(filePath)
}
lockItem := v.(*LockItem)
handle := lockItem.fl.Fh()
if _, err = handle.Seek(0, io.SeekStart); nil != err {
return
}
return io.ReadAll(handle)
}
func LockFileRead(filePath string) (data []byte, err error) {
fileReadWriteLock.Lock()
defer fileReadWriteLock.Unlock()
if !gulu.File.IsExist(filePath) {
err = os.ErrNotExist
return
}
lock, lockErr := lockFile0(filePath)
if nil != lockErr {
err = lockErr
return
}
handle := lock.Fh()
if _, err = handle.Seek(0, io.SeekStart); nil != err {
return
}
return io.ReadAll(handle)
}
func NoLockFileWrite(filePath string, data []byte) (err error) {
fileReadWriteLock.Lock()
defer fileReadWriteLock.Unlock()
v, ok := fileLocks.Load(filePath)
if !ok {
return os.WriteFile(filePath, data, 0644)
}
lockItem := v.(*LockItem)
handle := lockItem.fl.Fh()
err = gulu.File.WriteFileSaferByHandle(handle, data)
return
}
func LockFileWrite(filePath string, data []byte) (err error) {
fileReadWriteLock.Lock()
defer fileReadWriteLock.Unlock()
lock, lockErr := lockFile0(filePath)
if nil != lockErr {
err = lockErr
return
}
handle := lock.Fh()
err = gulu.File.WriteFileSaferByHandle(handle, data)
return
}
func IsLocked(filePath string) bool {
v, _ := fileLocks.Load(filePath)
if nil == v {
return false
}
return true
}
func UnlockFile(filePath string) (err error) {
fileReadWriteLock.Lock()
defer fileReadWriteLock.Unlock()
return unlockFile0(filePath)
}
func unlockFile0(filePath string) (err error) {
v, _ := fileLocks.Load(filePath)
if nil == v {
return
}
lockItem := v.(*LockItem)
err = lockItem.fl.Unlock()
fileLocks.Delete(filePath)
return
}
func LockFile(filePath string) (err error) {
fileReadWriteLock.Lock()
defer fileReadWriteLock.Unlock()
_, err = lockFile0(filePath)
return
}
func lockFile0(filePath string) (lock *flock.Flock, err error) {
lockItemVal, _ := fileLocks.Load(filePath)
var lockItem *LockItem
if nil == lockItemVal {
lock = flock.New(filePath)
var locked bool
var lockErr error
for i := 0; i < 7; i++ {
locked, lockErr = lock.TryLock()
if nil != lockErr || !locked {
time.Sleep(100 * time.Millisecond)
continue
}
break
}
if nil != lockErr {
util.LogErrorf("lock file [%s] failed: %s", filePath, lockErr)
err = ErrUnableLockFile
return
}
if !locked {
util.LogErrorf("unable to lock file [%s]", filePath)
err = ErrUnableLockFile
return
}
lockItem = &LockItem{fl: lock}
} else {
lockItem = lockItemVal.(*LockItem)
lock = lockItem.fl
}
lockItem.expired = time.Now().Add(expiration).UnixNano()
fileLocks.Store(filePath, lockItem)
return
}

View file

@ -0,0 +1,70 @@
// SiYuan - Build Your Eternal Digital Garden
// 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/>.
//go:build android || ios
// +build android ios
package filesys
import (
"errors"
"os"
"sync"
"github.com/88250/gulu"
)
var ErrUnableLockFile = errors.New("unable to lock file")
func ReleaseFileLocks(boxLocalPath string) {}
func ReleaseAllFileLocks() {}
func NoLockFileRead(filePath string) (data []byte, err error) {
return os.ReadFile(filePath)
}
func LockFileRead(filePath string) (data []byte, err error) {
return os.ReadFile(filePath)
}
func NoLockFileWrite(filePath string, data []byte) (err error) {
return gulu.File.WriteFileSafer(filePath, data, 0644)
}
func LockFileWrite(filePath string, data []byte) (err error) {
return gulu.File.WriteFileSafer(filePath, data, 0644)
}
func LockFile(filePath string) (err error) {
return
}
func UnlockFile(filePath string) (err error) {
return
}
var fileLocks = sync.Map{}
func IsLocked(filePath string) bool {
return false
}
func LockFileReadWrite() {
}
func UnlockFileReadWriteLock() {
}

200
kernel/filesys/tree.go Normal file
View file

@ -0,0 +1,200 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 filesys
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/88250/lute"
"github.com/88250/lute/parse"
"github.com/88250/protyle"
"github.com/siyuan-note/siyuan/kernel/cache"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func LoadTree(boxID, p string, luteEngine *lute.Lute) (ret *parse.Tree, err error) {
filePath := filepath.Join(util.DataDir, boxID, p)
data, err := LockFileRead(filePath)
if nil != err {
return
}
ret = parseJSON2Tree(boxID, p, data, luteEngine)
if nil == ret {
ret = recoverParseJSON2Tree(boxID, p, filePath, luteEngine)
if nil == ret {
return nil, errors.New("parse tree failed")
}
}
ret.Path = p
ret.Root.Path = p
parts := strings.Split(p, "/")
parts = parts[1 : len(parts)-1] // 去掉开头的斜杆和结尾的自己
if 1 > len(parts) {
ret.HPath = "/" + ret.Root.IALAttr("title")
ret.Hash = treenode.NodeHash(ret.Root, ret, luteEngine)
return
}
// 构造 HPath
hPathBuilder := bytes.Buffer{}
hPathBuilder.WriteString("/")
for i, _ := range parts {
var parentPath string
if 0 < i {
parentPath = strings.Join(parts[:i+1], "/")
} else {
parentPath = parts[0]
}
parentPath += ".sy"
parentPath = filepath.Join(util.DataDir, boxID, parentPath)
data, err := LockFileRead(parentPath)
if nil != err {
hPathBuilder.WriteString("Untitled/")
continue
}
parentTree, err := protyle.ParseJSONWithoutFix(luteEngine, data)
if nil != err {
hPathBuilder.WriteString("Untitled/")
continue
}
hPathBuilder.WriteString(parentTree.Root.IALAttr("title"))
hPathBuilder.WriteString("/")
}
hPathBuilder.WriteString(ret.Root.IALAttr("title"))
ret.HPath = hPathBuilder.String()
ret.Hash = treenode.NodeHash(ret.Root, ret, luteEngine)
return
}
func WriteTree(tree *parse.Tree) (err error) {
luteEngine := util.NewLute() // 不关注用户的自定义解析渲染选项
if nil == tree.Root.FirstChild {
newP := protyle.NewParagraph()
tree.Root.AppendChild(newP)
tree.Root.SetIALAttr("updated", util.TimeFromID(newP.ID))
}
renderer := protyle.NewJSONRenderer(tree, luteEngine.RenderOptions)
output := renderer.Render()
// .sy 文档数据使用格式化好的 JSON 而非单行 JSON
buf := bytes.Buffer{}
buf.Grow(4096)
if err = json.Indent(&buf, output, "", "\t"); nil != err {
return
}
output = buf.Bytes()
filePath := filepath.Join(util.DataDir, tree.Box, tree.Path)
if err = os.MkdirAll(filepath.Dir(filePath), 0755); nil != err {
return
}
if err = LockFileWrite(filePath, output); nil != err {
msg := fmt.Sprintf("write data [%s] failed: %s", filePath, err)
util.LogErrorf(msg)
return errors.New(msg)
}
docIAL := parse.IAL2MapUnEsc(tree.Root.KramdownIAL)
cache.PutDocIAL(tree.Path, docIAL)
return
}
func recoverParseJSON2Tree(boxID, p, filePath string, luteEngine *lute.Lute) (ret *parse.Tree) {
// 尝试从临时文件恢复
tmp := util.LatestTmpFile(filePath)
if "" == tmp {
util.LogWarnf("recover tree [%s] not found tmp", filePath)
return
}
stat, err := os.Stat(filePath)
if nil != err {
util.LogErrorf("stat tmp [%s] failed: %s", tmp, err)
return
}
if stat.ModTime().Before(time.Now().Add(-time.Hour * 24)) {
util.LogWarnf("tmp [%s] is too old, remove it", tmp)
os.RemoveAll(tmp)
return
}
data, err := NoLockFileRead(tmp)
if nil != err {
util.LogErrorf("recover tree read from tmp [%s] failed: %s", tmp, err)
return
}
if err = NoLockFileWrite(filePath, data); nil != err {
util.LogErrorf("recover tree write [%s] from tmp [%s] failed: %s", filePath, tmp, err)
return
}
ret = parseJSON2Tree(boxID, p, data, luteEngine)
if nil == ret {
util.LogErrorf("recover tree from tmp [%s] parse failed, remove it", tmp)
os.RemoveAll(tmp)
return
}
util.LogInfof("recovered tree [%s] from [%s]", filePath, tmp)
os.RemoveAll(tmp)
return
}
func parseJSON2Tree(boxID, p string, jsonData []byte, luteEngine *lute.Lute) (ret *parse.Tree) {
var err error
var needFix bool
ret, needFix, err = protyle.ParseJSON(luteEngine, jsonData)
if nil != err {
util.LogErrorf("parse json [%s] to tree failed: %s", boxID+p, err)
return
}
ret.Box = boxID
ret.Path = p
if needFix {
renderer := protyle.NewJSONRenderer(ret, luteEngine.RenderOptions)
output := renderer.Render()
buf := bytes.Buffer{}
buf.Grow(4096)
if err = json.Indent(&buf, output, "", "\t"); nil != err {
return
}
output = buf.Bytes()
filePath := filepath.Join(util.DataDir, ret.Box, ret.Path)
if err = os.MkdirAll(filepath.Dir(filePath), 0755); nil != err {
return
}
if err = LockFileWrite(filePath, output); nil != err {
msg := fmt.Sprintf("write data [%s] failed: %s", filePath, err)
util.LogErrorf(msg)
}
}
return
}

108
kernel/filesys/workspace.go Normal file
View file

@ -0,0 +1,108 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 filesys
import (
"os"
"path/filepath"
"sync"
"github.com/88250/gulu"
"github.com/siyuan-note/siyuan/kernel/util"
)
type DataConf struct {
Updated int64 `json:"updated"` // 最近一次数据更新时间
SyncVer int64 `json:"syncVer"` // 同步版本号
Device string `json:"device"` // 设备 ID
}
var incWorkspaceDataVerLock = sync.Mutex{}
func IncWorkspaceDataVer(inc bool, systemID string) {
incWorkspaceDataVerLock.Lock()
defer incWorkspaceDataVerLock.Unlock()
confPath := filepath.Join(util.DataDir, ".siyuan")
os.MkdirAll(confPath, 0755)
confPath = filepath.Join(confPath, "conf.json")
var data []byte
var err error
now := util.CurrentTimeMillis()
conf := &DataConf{Updated: now, Device: systemID}
if !gulu.File.IsExist(confPath) {
data, _ = gulu.JSON.MarshalIndentJSON(conf, "", " ")
if err = LockFileWrite(confPath, data); nil != err {
util.LogErrorf("save data conf [%s] failed: %s", confPath, err)
}
t := util.Millisecond2Time(now)
if err = os.Chtimes(confPath, t, t); nil != err {
util.LogErrorf("change file [%s] mod time failed: %s", confPath, err)
}
return
}
data, err = LockFileRead(confPath)
if nil != err {
data, err = recoverFrom(confPath)
if nil != err {
return
}
}
if err = gulu.JSON.UnmarshalJSON(data, conf); nil != err {
data, err = recoverFrom(confPath)
if nil != err {
return
}
if err = gulu.JSON.UnmarshalJSON(data, conf); nil != err {
util.LogErrorf("parse data conf [%s] failed: %s", confPath, err)
}
}
conf.Updated = now
conf.Device = systemID
if inc {
conf.SyncVer++
}
data, _ = gulu.JSON.MarshalIndentJSON(conf, "", " ")
if err = LockFileWrite(confPath, data); nil != err {
util.LogErrorf("save data conf [%s] failed: %s", confPath, err)
return
}
}
func recoverFrom(confPath string) (data []byte, err error) {
// 尝试从临时文件恢复
tmp := util.LatestTmpFile(confPath)
if "" == tmp {
util.LogErrorf("read data conf [%s] failed: %s", confPath, err)
return
}
data, err = NoLockFileRead(tmp)
if nil != err {
util.LogErrorf("read temp data conf [%s] failed: %s", tmp, err)
return
}
util.LogInfof("recovered file [%s] from [%s]", confPath, tmp)
os.RemoveAll(tmp)
return
}

110
kernel/go.mod Normal file
View file

@ -0,0 +1,110 @@
module github.com/siyuan-note/siyuan/kernel
go 1.18
require (
github.com/88250/clipboard v0.1.5
github.com/88250/css v0.1.2
github.com/88250/flock v0.8.2
github.com/88250/gulu v1.2.1
github.com/88250/lute v1.7.4-0.20220525011519-3148f42c174b
github.com/88250/melody v0.0.0-20201115062536-c0b3394adcd1
github.com/88250/pdfcpu v0.3.13
github.com/88250/protyle v0.0.0-20220519012506-0a2c8dc24397
github.com/88250/vitess-sqlparser v0.0.0-20210205111146-56a2ded2aba1
github.com/ConradIrwin/font v0.0.0-20210318200717-ce8d41cc0732
github.com/Masterminds/sprig/v3 v3.2.2
github.com/PuerkitoBio/goquery v1.8.0
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be
github.com/denisbrodbeck/machineid v1.0.1
github.com/dgraph-io/ristretto v0.1.0
github.com/dustin/go-humanize v1.0.0
github.com/emirpasic/gods v1.18.1
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb
github.com/flopp/go-findfont v0.1.0
github.com/fsnotify/fsnotify v1.5.4
github.com/getsentry/sentry-go v0.13.0
github.com/gin-contrib/cors v1.3.1
github.com/gin-contrib/gzip v0.0.5
github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.7.7
github.com/imroc/req/v3 v3.11.3
github.com/jinzhu/copier v0.3.5
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/mitchellh/go-ps v1.0.0
github.com/mssola/user_agent v0.5.3
github.com/panjf2000/ants/v2 v2.5.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/qiniu/go-sdk/v7 v7.12.1
github.com/radovskyb/watcher v1.0.7
github.com/siyuan-note/encryption v0.0.0-20210811062758-4d08f2d31e37
github.com/vmihailenco/msgpack/v5 v5.3.5
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9
golang.org/x/mobile v0.0.0-20220307220422-55113b94f09c
golang.org/x/text v0.3.7
)
require (
dmitri.shuralyov.com/font/woff2 v0.0.0-20180220214647-957792cbbdab // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.0 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 // indirect
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/errors v0.0.0-20220331221717-b38fca44723b // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-zglob v0.0.3 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
golang.org/x/mod v0.5.1 // indirect
golang.org/x/net v0.0.0-20220524220425-1d687d428aca // indirect
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/tools v0.1.8 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
replace github.com/mattn/go-sqlite3 => github.com/88250/go-sqlite3 v1.14.13-0.20220412041952-88c3aaa8595e
//replace github.com/88250/lute => D:\gogogo\src\github.com\88250\lute
//replace github.com/88250/enumfonts => D:\88250\enumfonts
//replace github.com/88250/pdfcpu => D:\88250\pdfcpu
//replace github.com/88250/protyle => D:\88250\protyle
//replace github.com/88250/gulu => D:\88250\gulu
//replace github.com/88250/melody => D:\88250\melody
//replace github.com/mattn/go-sqlite3 => D:\88250\go-sqlite3

849
kernel/go.sum Normal file
View file

@ -0,0 +1,849 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
dmitri.shuralyov.com/font/woff2 v0.0.0-20180220214647-957792cbbdab h1:Ew70NL+wL6v9looOiJJthlqA41VzoJS+q9AyjHJe6/g=
dmitri.shuralyov.com/font/woff2 v0.0.0-20180220214647-957792cbbdab/go.mod h1:FvHgTMJanm43G7B3MVSjS/jim5ytVqAJNAOpRhnuHJc=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/88250/clipboard v0.1.5 h1:V/mCiSrjwmIiJwvchGTs+W2ozdINxk7y7KgHNTSzlCI=
github.com/88250/clipboard v0.1.5/go.mod h1:bNLJx4L8cF6fEgiXMPVrK1Iidnaff8BTkktTNtefcks=
github.com/88250/css v0.1.2 h1:+AADhEwWoGZFbUjqIsBcdnq2xfj8fDFDAGRXhBUhUY8=
github.com/88250/css v0.1.2/go.mod h1:XfcZHQ0YuUb9VncVBurQfVyw1ZQicsB5Gc9N7BK3/ig=
github.com/88250/flock v0.8.2 h1:LLbRJw3hoYfjD4g7DiYsYcTCCFTxm8icn/WepLlxIg0=
github.com/88250/flock v0.8.2/go.mod h1:k+PZxETAUe4vLZx3R39ykvQCIlwHhc7AI2P2NUQV6zw=
github.com/88250/go-sqlite3 v1.14.13-0.20220412041952-88c3aaa8595e h1:uXi4QLKI/mswcXuzD+wBjJMkj1C3hK5Tgl3hF6MJpbo=
github.com/88250/go-sqlite3 v1.14.13-0.20220412041952-88c3aaa8595e/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/88250/gulu v1.2.0/go.mod h1:ZhEJ98UjR2y7j2toGj4/b+1rRELcZFQAPq/Yjyin2yY=
github.com/88250/gulu v1.2.1 h1:omRzVo2IToCL1sclmAy3Xq81vAW7nY/vZN1FopAvJtg=
github.com/88250/gulu v1.2.1/go.mod h1:I1qBzsksFL2ciGSuqDE7R3XW4BUMrfDgOvSXEk7FsAI=
github.com/88250/lute v1.7.4-0.20220426011157-34c9bfa2e148/go.mod h1:Bdu9LRNjQhtL3TftbtpjIWTwDVAXoS7AD8QsZQPk7zo=
github.com/88250/lute v1.7.4-0.20220525011519-3148f42c174b h1:UwGrvVWNkXpyv8FkazWeV0brdmhM7FpWE3qMfqF1K+4=
github.com/88250/lute v1.7.4-0.20220525011519-3148f42c174b/go.mod h1:Bdu9LRNjQhtL3TftbtpjIWTwDVAXoS7AD8QsZQPk7zo=
github.com/88250/melody v0.0.0-20201115062536-c0b3394adcd1 h1:9Cb+iN639vUI2OcIBc+4oGwml9/0J6bL6dWNb8Al+1s=
github.com/88250/melody v0.0.0-20201115062536-c0b3394adcd1/go.mod h1:jH6MMPr8G7AMzaVmWHXZQiB1DKO3giWbcWZ7UoJ1teI=
github.com/88250/pdfcpu v0.3.13 h1:touMWMZkCGalMIbEg9bxYp7rETM+zwb9hXjwhqi4I7Q=
github.com/88250/pdfcpu v0.3.13/go.mod h1:S5YT38L/GCjVjmB4PB84PymA1qfopjEhfhTNQilLpv4=
github.com/88250/protyle v0.0.0-20220519012506-0a2c8dc24397 h1:vFP83UlxlY0ug3nc8bX4nODqjVWvb+gaMb4ULwBc4CA=
github.com/88250/protyle v0.0.0-20220519012506-0a2c8dc24397/go.mod h1:sbEh005LkR8vNhi+O5ww6rgDJtiP8OFyYMZcg69Vt+M=
github.com/88250/vitess-sqlparser v0.0.0-20210205111146-56a2ded2aba1 h1:48T899JQDwyyRu9yXHePYlPdHtpJfrJEUGBMH3SMBWY=
github.com/88250/vitess-sqlparser v0.0.0-20210205111146-56a2ded2aba1/go.mod h1:U3pckKQIgxxkmZjV5yXQjHdGxQK0o/vEZeZ6cQsxfHw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/ConradIrwin/font v0.0.0-20210318200717-ce8d41cc0732 h1:0EDePskeF4vNFCk70ATaFHQzjmwXsk+VImnMJttecNU=
github.com/ConradIrwin/font v0.0.0-20210318200717-ce8d41cc0732/go.mod h1:krTLO7JWu6g8RMxG8sl+T1Hf8W93XQacBKJmqFZ2MFY=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q=
github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb h1:IT4JYU7k4ikYg1SCxNI1/Tieq/NFvh6dzLdgi7eu0tM=
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb/go.mod h1:bH6Xx7IW64qjjJq8M2u4dxNaBiDfKK+z/3eGDpXEQhc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU=
github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA=
github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk=
github.com/gin-contrib/gzip v0.0.5 h1:mhnVU32YnnBh2LPH2iqRqsA/eR7SAqRaD388jL2s/j0=
github.com/gin-contrib/gzip v0.0.5/go.mod h1:OPIK6HR0Um2vNmBUTlayD7qle4yVVRZT0PyhdUigrKk=
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20220221023154-0b2280d3ff96/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk=
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 h1:1yY/RQWNSBjJe2GDCIYoLmpWVidrooriUr4QS/zaATQ=
github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk=
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 h1:o1wMw7uTNyA58IlEdDpxIrtFHTgnvYzA8sCQz8luv94=
github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7/go.mod h1:WkUxfS2JUu3qPo6tRld7ISb8HiC0gVSU91kooBMDVok=
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
github.com/imroc/req/v3 v3.11.3 h1:3KmVX29c2fdF9XcRZ95/7fymisG/EXcUE7IwE6u4sNk=
github.com/imroc/req/v3 v3.11.3/go.mod h1:G6fkq27P+JcTcgRVxecxY+amHN1xFl8W81eLCfJ151M=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/errors v0.0.0-20170703010042-c7d06af17c68/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/errors v0.0.0-20220331221717-b38fca44723b h1:AxFeSQJfcm2O3ov1wqAkTKYFsnMw2g1B4PkYujfAdkY=
github.com/juju/errors v0.0.0-20220331221717-b38fca44723b/go.mod h1:jMGj9DWF/qbo91ODcfJq6z/RYc3FX3taCBZMCcpI4Ls=
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/juju/testing v0.0.0-20191001232224-ce9dec17d28b/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-zglob v0.0.3 h1:6Ry4EYsScDyt5di4OI6xw1bYhOqfE5S33Z1OPy+d+To=
github.com/mattn/go-zglob v0.0.3/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mssola/user_agent v0.5.3 h1:lBRPML9mdFuIZgI2cmlQ+atbpJdLdeVl2IDodjBR578=
github.com/mssola/user_agent v0.5.3/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/panjf2000/ants/v2 v2.5.0 h1:1rWGWSnxCsQBga+nQbA4/iY6VMeNoOIAM0ZWh9u3q2Q=
github.com/panjf2000/ants/v2 v2.5.0/go.mod h1:cU93usDlihJZ5CfRGNDYsiBYvoilLvBF5Qp/BT2GNRE=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
github.com/qiniu/go-sdk/v7 v7.12.1 h1:FZG5dhs2MZBV/mHVhmHnsgsQ+j1gSE0RqIoA2WwEDwY=
github.com/qiniu/go-sdk/v7 v7.12.1/go.mod h1:btsaOc8CA3hdVloULfFdDgDc+g4f3TDZEFsDY0BLE+w=
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/gofontwoff v0.0.0-20181114050219-180f79e6909d h1:lvCTyBbr36+tqMccdGMwuEU+hjux/zL6xSmf5S9ITaA=
github.com/shurcooL/gofontwoff v0.0.0-20181114050219-180f79e6909d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/siyuan-note/encryption v0.0.0-20210811062758-4d08f2d31e37 h1:WvJU9uRS7kaaqnNShIMMtR2Yf8duGmXYJXYGg69EXBs=
github.com/siyuan-note/encryption v0.0.0-20210811062758-4d08f2d31e37/go.mod h1:hWBdT3FZEzWvIbZpXYJvkSBH2+Z4GvYcOpKpXcZC+zg=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190823064033-3a9bac650e44/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20220307220422-55113b94f09c h1:9J0m/JcA5YXYbamDhF5I3T7cJnR7V75OCLnMCPb5gl4=
golang.org/x/mobile v0.0.0-20220307220422-55113b94f09c/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/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-20201209123823-ac852fbbde11/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/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220524220425-1d687d428aca h1:xTaFYiPROfpPhqrfTIDXj0ri1SpfueYT951s4bAuDO8=
golang.org/x/net v0.0.0-20220524220425-1d687d428aca/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211020174200-9d6173849985/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.0.0-20180302201248-b7ef84aaf62a/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

53
kernel/main.go Normal file
View file

@ -0,0 +1,53 @@
// SiYuan - Build Your Eternal Digital Garden
// 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/>.
//go:build !mobile
package main
import (
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/server"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func main() {
util.Boot()
model.InitConf()
go server.Serve(false)
model.InitAppearance()
sql.InitDatabase(false)
sql.SetCaseSensitive(model.Conf.Search.CaseSensitive)
model.SyncData(true, false, false)
model.InitBoxes()
go model.AutoGenerateDocHistory()
go model.AutoSync()
go model.AutoStat()
go model.HookResident()
util.SetBooted()
util.ClearPushProgress(100)
go model.AutoRefreshUser()
go model.AutoFlushTx()
go sql.AutoFlushTreeQueue()
go treenode.AutoFlushBlockTree()
model.WatchAssets()
model.HandleSignal()
}

90
kernel/mobile/kernel.go Normal file
View file

@ -0,0 +1,90 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 mobile
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/siyuan-note/siyuan/kernel/model"
"github.com/siyuan-note/siyuan/kernel/server"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
_ "golang.org/x/mobile/bind"
)
func StartKernelFast(container, appDir, workspaceDir, nativeLibDir, privateDataDir, localIP string) {
go server.Serve(true)
}
func StartKernel(container, appDir, workspaceDir, nativeLibDir, privateDataDir, timezoneID, localIPs, lang string) {
SetTimezone(container, appDir, timezoneID)
util.Mode = "prod"
util.LocalIPs = strings.Split(localIPs, ",")
util.BootMobile(container, appDir, workspaceDir, nativeLibDir, privateDataDir, lang)
model.InitConf()
go server.Serve(false)
go func() {
model.InitAppearance()
sql.InitDatabase(false)
sql.SetCaseSensitive(model.Conf.Search.CaseSensitive)
model.SyncData(true, false, false)
model.InitBoxes()
go model.AutoGenerateDocHistory()
go model.AutoSync()
go model.AutoStat()
util.SetBooted()
util.ClearPushProgress(100)
go model.AutoRefreshUser()
go model.AutoFlushTx()
go sql.AutoFlushTreeQueue()
go treenode.AutoFlushBlockTree()
}()
}
func Language(num int) string {
return model.Conf.Language(num)
}
func ShowMsg(msg string, timeout int) {
util.PushMsg(msg, timeout)
}
func IsHttpServing() bool {
return util.HttpServing
}
func SetTimezone(container, appDir, timezoneID string) {
if "ios" == container {
os.Setenv("ZONEINFO", filepath.Join(appDir, "app", "zoneinfo.zip"))
}
z, err := time.LoadLocation(strings.TrimSpace(timezoneID))
if err != nil {
fmt.Printf("load location failed: %s\n", err)
time.Local = time.FixedZone("CST", 8*3600)
return
}
time.Local = z
}

345
kernel/model/appearance.go Normal file
View file

@ -0,0 +1,345 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/88250/gulu"
"github.com/fsnotify/fsnotify"
"github.com/siyuan-note/siyuan/kernel/util"
)
func InitAppearance() {
util.SetBootDetails("Initializing appearance...")
if err := os.Mkdir(util.AppearancePath, 0755); nil != err && !os.IsExist(err) {
util.LogFatalf("create appearance folder [%s] failed: %s", util.AppearancePath, err)
}
unloadThemes()
from := filepath.Join(util.WorkingDir, "appearance")
if err := gulu.File.Copy(from, util.AppearancePath); nil != err {
util.LogFatalf("copy appearance resources from [%s] to [%s] failed: %s", from, util.AppearancePath, err)
}
loadThemes()
if !gulu.Str.Contains(Conf.Appearance.ThemeDark, Conf.Appearance.DarkThemes) {
Conf.Appearance.ThemeDark = "midnight"
Conf.Appearance.ThemeJS = false
}
if !gulu.Str.Contains(Conf.Appearance.ThemeLight, Conf.Appearance.LightThemes) {
Conf.Appearance.ThemeLight = "daylight"
Conf.Appearance.ThemeJS = false
}
loadIcons()
if !gulu.Str.Contains(Conf.Appearance.Icon, Conf.Appearance.Icons) {
Conf.Appearance.Icon = "material"
}
Conf.Save()
}
var themeWatchers = sync.Map{} // [string]*fsnotify.Watcher{}
func closeThemeWatchers() {
themeWatchers.Range(func(key, value interface{}) bool {
if err := value.(*fsnotify.Watcher).Close(); nil != err {
util.LogErrorf("close file watcher failed: %s", err)
}
return true
})
}
func unloadThemes() {
if !gulu.File.IsDir(util.ThemesPath) {
return
}
dir, err := os.Open(util.ThemesPath)
if nil != err {
util.LogErrorf("open appearance themes folder [%s] failed: %s", util.ThemesPath, err)
return
}
themeDirs, err := dir.Readdir(-1)
if nil != err {
util.LogErrorf("read appearance themes folder failed: %s", err)
return
}
dir.Close()
for _, themeDir := range themeDirs {
if !themeDir.IsDir() {
continue
}
unwatchTheme(filepath.Join(util.ThemesPath, themeDir.Name()))
}
}
func loadThemes() {
dir, err := os.Open(util.ThemesPath)
if nil != err {
util.LogFatalf("open appearance themes folder [%s] failed: %s", util.ThemesPath, err)
}
themeDirs, err := dir.Readdir(-1)
if nil != err {
util.LogFatalf("read appearance themes folder failed: %s", err)
}
dir.Close()
Conf.Appearance.DarkThemes = nil
Conf.Appearance.LightThemes = nil
for _, themeDir := range themeDirs {
if !themeDir.IsDir() {
continue
}
name := themeDir.Name()
themeConf, err := themeJSON(name)
if nil != err || nil == themeConf {
continue
}
modes := themeConf["modes"].([]interface{})
for _, mode := range modes {
if "dark" == mode {
Conf.Appearance.DarkThemes = append(Conf.Appearance.DarkThemes, name)
} else if "light" == mode {
Conf.Appearance.LightThemes = append(Conf.Appearance.LightThemes, name)
}
}
if 0 == Conf.Appearance.Mode {
if Conf.Appearance.ThemeLight == name {
Conf.Appearance.ThemeVer = themeConf["version"].(string)
Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js"))
}
} else {
if Conf.Appearance.ThemeDark == name {
Conf.Appearance.ThemeVer = themeConf["version"].(string)
Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(util.ThemesPath, name, "theme.js"))
}
}
go watchTheme(filepath.Join(util.ThemesPath, name))
}
}
func themeJSON(themeName string) (ret map[string]interface{}, err error) {
p := filepath.Join(util.ThemesPath, themeName, "theme.json")
if !gulu.File.IsExist(p) {
err = os.ErrNotExist
return
}
data, err := os.ReadFile(p)
if nil != err {
util.LogErrorf("read theme.json [%s] failed: %s", p, err)
return
}
if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err {
util.LogErrorf("parse theme.json [%s] failed: %s", p, err)
return
}
if 5 > len(ret) {
util.LogWarnf("invalid theme.json [%s]", p)
return nil, errors.New("invalid theme.json")
}
return
}
func iconJSON(iconName string) (ret map[string]interface{}, err error) {
p := filepath.Join(util.IconsPath, iconName, "icon.json")
if !gulu.File.IsExist(p) {
err = os.ErrNotExist
return
}
data, err := os.ReadFile(p)
if nil != err {
util.LogErrorf("read icon.json [%s] failed: %s", p, err)
return
}
if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err {
util.LogErrorf("parse icon.json [%s] failed: %s", p, err)
return
}
if 4 > len(ret) {
util.LogWarnf("invalid icon.json [%s]", p)
return nil, errors.New("invalid icon.json")
}
return
}
func templateJSON(templateName string) (ret map[string]interface{}, err error) {
p := filepath.Join(util.DataDir, "templates", templateName, "template.json")
if !gulu.File.IsExist(p) {
err = os.ErrNotExist
return
}
data, err := os.ReadFile(p)
if nil != err {
util.LogErrorf("read template.json [%s] failed: %s", p, err)
return
}
if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err {
util.LogErrorf("parse template.json [%s] failed: %s", p, err)
return
}
if 4 > len(ret) {
util.LogWarnf("invalid template.json [%s]", p)
return nil, errors.New("invalid template.json")
}
return
}
func widgetJSON(widgetName string) (ret map[string]interface{}, err error) {
p := filepath.Join(util.DataDir, "widgets", widgetName, "widget.json")
if !gulu.File.IsExist(p) {
err = os.ErrNotExist
return
}
data, err := os.ReadFile(p)
if nil != err {
util.LogErrorf("read widget.json [%s] failed: %s", p, err)
return
}
if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err {
util.LogErrorf("parse widget.json [%s] failed: %s", p, err)
return
}
if 4 > len(ret) {
util.LogWarnf("invalid widget.json [%s]", p)
return nil, errors.New("invalid widget.json")
}
return
}
func loadIcons() {
dir, err := os.Open(util.IconsPath)
if nil != err {
util.LogFatalf("open appearance icons folder [%s] failed: %s", util.IconsPath, err)
}
iconDirs, err := dir.Readdir(-1)
if nil != err {
util.LogFatalf("read appearance icons folder failed: %s", err)
}
dir.Close()
Conf.Appearance.Icons = nil
for _, iconDir := range iconDirs {
if !iconDir.IsDir() {
continue
}
name := iconDir.Name()
iconConf, err := iconJSON(name)
if nil != err || nil == iconConf {
continue
}
Conf.Appearance.Icons = append(Conf.Appearance.Icons, name)
if Conf.Appearance.Icon == name {
Conf.Appearance.IconVer = iconConf["version"].(string)
}
}
}
func unwatchTheme(folder string) {
val, _ := themeWatchers.Load(folder)
if nil != val {
themeWatcher := val.(*fsnotify.Watcher)
themeWatcher.Close()
}
}
func watchTheme(folder string) {
val, _ := themeWatchers.Load(folder)
var themeWatcher *fsnotify.Watcher
if nil != val {
themeWatcher = val.(*fsnotify.Watcher)
themeWatcher.Close()
}
var err error
if themeWatcher, err = fsnotify.NewWatcher(); nil != err {
util.LogErrorf("add theme file watcher for folder [%s] failed: %s", folder, err)
return
}
themeWatchers.Store(folder, themeWatcher)
done := make(chan bool)
go func() {
for {
select {
case event, ok := <-themeWatcher.Events:
if !ok {
return
}
//util.LogInfof(event.String())
if event.Op&fsnotify.Write == fsnotify.Write &&
(strings.HasSuffix(event.Name, "theme.css") || strings.HasSuffix(event.Name, "custom.css")) {
var themeName string
if themeName = isCurrentUseTheme(event.Name); "" == themeName {
break
}
if strings.HasSuffix(event.Name, "theme.css") {
util.BroadcastByType("main", "refreshtheme", 0, "", map[string]interface{}{
"theme": "/appearance/themes/" + themeName + "/theme.css?" + fmt.Sprintf("%d", time.Now().Unix()),
})
break
}
if strings.HasSuffix(event.Name, "custom.css") {
util.BroadcastByType("main", "refreshtheme", 0, "", map[string]interface{}{
"theme": "/appearance/themes/" + themeName + "/custom.css?" + fmt.Sprintf("%d", time.Now().Unix()),
})
break
}
}
case err, ok := <-themeWatcher.Errors:
if !ok {
return
}
util.LogErrorf("watch theme file failed: %s", err)
}
}
}()
//util.LogInfof("add file watcher [%s]", folder)
if err := themeWatcher.Add(folder); err != nil {
util.LogErrorf("add theme files watcher for folder [%s] failed: %s", folder, err)
}
<-done
}
func isCurrentUseTheme(themePath string) string {
themeName := filepath.Base(filepath.Dir(themePath))
if 0 == Conf.Appearance.Mode { // 明亮
if Conf.Appearance.ThemeLight == themeName {
return themeName
}
} else if 1 == Conf.Appearance.Mode { // 暗黑
if Conf.Appearance.ThemeDark == themeName {
return themeName
}
}
return ""
}

677
kernel/model/assets.go Normal file
View file

@ -0,0 +1,677 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"mime"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strings"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/search"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func DocImageAssets(rootID string) (ret []string, err error) {
tree, err := loadTreeByBlockID(rootID)
if nil != err {
return
}
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeImage == n.Type {
linkDest := n.ChildByType(ast.NodeLinkDest)
dest := linkDest.Tokens
ret = append(ret, gulu.Str.FromBytes(dest))
}
return ast.WalkContinue
})
return
}
func NetImg2LocalAssets(rootID string) (err error) {
tree, err := loadTreeByBlockID(rootID)
if nil != err {
return
}
var files int
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeImage == n.Type {
linkDest := n.ChildByType(ast.NodeLinkDest)
dest := linkDest.Tokens
if !sql.IsAssetLinkDest(dest) && (bytes.HasPrefix(bytes.ToLower(dest), []byte("https://")) || bytes.HasPrefix(bytes.ToLower(dest), []byte("http://"))) {
u := string(dest)
util.PushMsg(fmt.Sprintf(Conf.Language(119), u), 15000)
request := util.NewBrowserRequest(Conf.System.NetworkProxy.String())
resp, reqErr := request.Get(u)
if nil != reqErr {
util.LogErrorf("download net img [%s] failed: %s", u, reqErr)
return ast.WalkSkipChildren
}
if 200 != resp.StatusCode {
util.LogErrorf("download net img [%s] failed: %d", u, resp.StatusCode)
return ast.WalkSkipChildren
}
data, repErr := resp.ToBytes()
if nil != repErr {
util.LogErrorf("download net img [%s] failed: %s", u, repErr)
return ast.WalkSkipChildren
}
var name string
if strings.Contains(u, "?") {
name = u[:strings.Index(u, "?")]
name = path.Base(name)
} else {
name = path.Base(u)
}
name, _ = url.PathUnescape(name)
ext := path.Ext(name)
if "" == ext {
contentType := resp.Header.Get("Content-Type")
exts, _ := mime.ExtensionsByType(contentType)
if 0 < len(exts) {
ext = exts[0]
}
}
name = strings.TrimSuffix(name, ext)
name = gulu.Str.SubStr(name, 64)
name = util.FilterFileName(name)
name = "net-img-" + name + "-" + ast.NewNodeID() + ext
writePath := filepath.Join(util.DataDir, "assets", name)
if err = gulu.File.WriteFileSafer(writePath, data, 0644); nil != err {
util.LogErrorf("write downloaded net img [%s] to local assets [%s] failed: %s", u, writePath, err)
return ast.WalkSkipChildren
}
linkDest.Tokens = []byte("assets/" + name)
files++
}
return ast.WalkSkipChildren
}
return ast.WalkContinue
})
if 0 < files {
util.PushMsg(Conf.Language(113), 5000)
if err = writeJSONQueue(tree); nil != err {
return
}
sql.WaitForWritingDatabase()
util.PushMsg(fmt.Sprintf(Conf.Language(120), files), 5000)
} else {
util.PushMsg(Conf.Language(121), 3000)
}
return
}
type Asset struct {
HName string `json:"hName"`
Name string `json:"name"`
Path string `json:"path"`
}
func SearchAssetsByName(keyword string) (ret []*Asset) {
ret = []*Asset{}
sqlAssets := sql.QueryAssetsByName(keyword)
for _, sqlAsset := range sqlAssets {
hName := util.RemoveID(sqlAsset.Name)
_, hName = search.MarkText(hName, keyword, 64, Conf.Search.CaseSensitive)
asset := &Asset{
HName: hName,
Name: sqlAsset.Name,
Path: sqlAsset.Path,
}
ret = append(ret, asset)
}
return
}
func GetAssetAbsPath(relativePath string) (absPath string, err error) {
relativePath = strings.TrimSpace(relativePath)
notebooks, err := ListNotebooks()
if nil != err {
err = errors.New(Conf.Language(0))
return
}
// 在笔记本下搜索
for _, notebook := range notebooks {
notebookAbsPath := filepath.Join(util.DataDir, notebook.ID)
filepath.Walk(notebookAbsPath, func(path string, info fs.FileInfo, _ error) error {
if isSkipFile(info.Name()) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
if p := filepath.ToSlash(path); strings.HasSuffix(p, relativePath) {
if gulu.File.IsExist(path) {
absPath = path
return io.EOF
}
}
return nil
})
if "" != absPath {
return
}
}
// 在全局 assets 路径下搜索
p := filepath.Join(util.DataDir, relativePath)
if gulu.File.IsExist(p) {
absPath = p
return
}
return "", errors.New(fmt.Sprintf(Conf.Language(12), relativePath))
}
func UploadAssets2Cloud(rootID string) (err error) {
if !IsSubscriber() {
return
}
sqlAssets := sql.QueryRootBlockAssets(rootID)
err = uploadCloud(sqlAssets)
return
}
func uploadCloud(sqlAssets []*sql.Asset) (err error) {
syncedAssets := readWorkspaceAssets()
var unSyncAssets []string
for _, sqlAsset := range sqlAssets {
if !gulu.Str.Contains(sqlAsset.Path, syncedAssets) && strings.Contains(sqlAsset.Path, "assets/") {
unSyncAssets = append(unSyncAssets, sqlAsset.Path)
}
}
if 1 > len(unSyncAssets) {
return
}
var uploadAbsAssets []string
for _, asset := range unSyncAssets {
var absPath string
absPath, err = GetAssetAbsPath(asset)
if nil != err {
util.LogWarnf("get asset [%s] abs path failed: %s", asset, err)
return
}
if "" == absPath {
util.LogErrorf("not found asset [%s]", asset)
err = errors.New(fmt.Sprintf(Conf.Language(12), asset))
return
}
uploadAbsAssets = append(uploadAbsAssets, absPath)
}
if 1 > len(uploadAbsAssets) {
return
}
uploadAbsAssets = util.RemoveDuplicatedElem(uploadAbsAssets)
util.LogInfof("uploading [%d] assets", len(uploadAbsAssets))
if loadErr := LoadUploadToken(); nil != loadErr {
util.PushMsg(loadErr.Error(), 5000)
return
}
var completedUploadAssets []string
for _, absAsset := range uploadAbsAssets {
if fi, statErr := os.Stat(absAsset); nil != statErr {
util.LogErrorf("stat file [%s] failed: %s", absAsset, statErr)
return statErr
} else if util.CloudSingleFileMaxSizeLimit/10 <= fi.Size() {
util.LogWarnf("file [%s] larger than 10MB, ignore uploading it", absAsset)
continue
}
requestResult := gulu.Ret.NewResult()
request := util.NewCloudFileRequest2m(Conf.System.NetworkProxy.String())
resp, reqErr := request.
SetResult(requestResult).
SetFile("file[]", absAsset).
SetCookies(&http.Cookie{Name: "symphony", Value: uploadToken}).
Post(util.AliyunServer + "/apis/siyuan/upload?ver=" + util.Ver)
if nil != reqErr {
util.LogErrorf("upload assets failed: %s", reqErr)
return ErrFailedToConnectCloudServer
}
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
if 0 != requestResult.Code {
util.LogErrorf("upload assets failed: %s", requestResult.Msg)
err = errors.New(fmt.Sprintf(Conf.Language(94), requestResult.Msg))
return
}
absAsset = filepath.ToSlash(absAsset)
relAsset := absAsset[strings.Index(absAsset, "assets/"):]
completedUploadAssets = append(completedUploadAssets, relAsset)
util.LogInfof("uploaded asset [%s]", relAsset)
}
if 0 < len(completedUploadAssets) {
syncedAssets = readWorkspaceAssets()
util.LogInfof("uploaded [%d] assets", len(completedUploadAssets))
for _, completedSyncAsset := range completedUploadAssets {
syncedAssets = append(syncedAssets, completedSyncAsset)
}
saveWorkspaceAssets(syncedAssets)
}
return
}
func readWorkspaceAssets() (ret []string) {
ret = []string{}
confDir := filepath.Join(util.DataDir, "assets", ".siyuan")
if err := os.MkdirAll(confDir, 0755); nil != err {
util.LogErrorf("create assets conf dir [%s] failed: %s", confDir, err)
return
}
confPath := filepath.Join(confDir, "assets.json")
if !gulu.File.IsExist(confPath) {
return
}
data, err := os.ReadFile(confPath)
if nil != err {
util.LogErrorf("read assets conf failed: %s", err)
return
}
if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err {
util.LogErrorf("parse assets conf failed: %s, re-init it", err)
return
}
return
}
func saveWorkspaceAssets(assets []string) {
confDir := filepath.Join(util.DataDir, "assets", ".siyuan")
if err := os.MkdirAll(confDir, 0755); nil != err {
util.LogErrorf("create assets conf dir [%s] failed: %s", confDir, err)
return
}
confPath := filepath.Join(confDir, "assets.json")
assets = util.RemoveDuplicatedElem(assets)
sort.Strings(assets)
data, err := gulu.JSON.MarshalIndentJSON(assets, "", " ")
if nil != err {
util.LogErrorf("create assets conf failed: %s", err)
return
}
if err = gulu.File.WriteFileSafer(confPath, data, 0644); nil != err {
util.LogErrorf("write assets conf failed: %s", err)
return
}
}
func RemoveUnusedAssets() (ret []string) {
util.PushMsg(Conf.Language(100), 30*1000)
defer util.PushMsg(Conf.Language(99), 3000)
ret = []string{}
unusedAssets := UnusedAssets()
historyDir, err := util.GetHistoryDir("delete")
if nil != err {
util.LogErrorf("get history dir failed: %s", err)
return
}
for _, p := range unusedAssets {
historyPath := filepath.Join(historyDir, p)
if p = filepath.Join(util.DataDir, p); gulu.File.IsExist(p) {
if err = gulu.File.Copy(p, historyPath); nil != err {
return
}
}
}
for _, unusedAsset := range unusedAssets {
if unusedAsset = filepath.Join(util.DataDir, unusedAsset); gulu.File.IsExist(unusedAsset) {
if err := os.RemoveAll(unusedAsset); nil != err {
util.LogErrorf("remove unused asset [%s] failed: %s", unusedAsset, err)
}
}
ret = append(ret, unusedAsset)
}
if 0 < len(ret) {
IncWorkspaceDataVer()
}
return
}
func RemoveUnusedAsset(p string) (ret string) {
p = filepath.Join(util.DataDir, p)
if !gulu.File.IsExist(p) {
return p
}
historyDir, err := util.GetHistoryDir("delete")
if nil != err {
util.LogErrorf("get history dir failed: %s", err)
return
}
newP := strings.TrimPrefix(p, util.DataDir)
historyPath := filepath.Join(historyDir, newP)
if err = gulu.File.Copy(p, historyPath); nil != err {
return
}
if err = os.RemoveAll(p); nil != err {
util.LogErrorf("remove unused asset [%s] failed: %s", p, err)
}
ret = p
IncWorkspaceDataVer()
return
}
func UnusedAssets() (ret []string) {
ret = []string{}
assetsPathMap, err := allAssetAbsPaths()
if nil != err {
return
}
linkDestMap := map[string]bool{}
notebooks, err := ListNotebooks()
if nil != err {
return
}
for _, notebook := range notebooks {
notebookAbsPath := filepath.Join(util.DataDir, notebook.ID)
trees := loadTrees(notebookAbsPath)
dests := map[string]bool{}
for _, tree := range trees {
for _, d := range assetsLinkDestsInTree(tree) {
dests[d] = true
}
if titleImgPath := treenode.GetDocTitleImgPath(tree.Root); "" != titleImgPath {
// 题头图计入
if !sql.IsAssetLinkDest([]byte(titleImgPath)) {
continue
}
dests[titleImgPath] = true
}
}
var linkDestFolderPaths, linkDestFilePaths []string
for dest, _ := range dests {
if !strings.HasPrefix(dest, "assets/") {
continue
}
if "" == assetsPathMap[dest] {
continue
}
if strings.HasSuffix(dest, "/") {
linkDestFolderPaths = append(linkDestFolderPaths, dest)
} else {
linkDestFilePaths = append(linkDestFilePaths, dest)
}
}
// 排除文件夹链接
var toRemoves []string
for asset, _ := range assetsPathMap {
for _, linkDestFolder := range linkDestFolderPaths {
if strings.HasPrefix(asset, linkDestFolder) {
toRemoves = append(toRemoves, asset)
}
}
for _, linkDestPath := range linkDestFilePaths {
if strings.HasPrefix(linkDestPath, asset) {
toRemoves = append(toRemoves, asset)
}
}
}
for _, toRemove := range toRemoves {
delete(assetsPathMap, toRemove)
}
for _, dest := range linkDestFilePaths {
linkDestMap[dest] = true
}
}
// 排除文件注解
var toRemoves []string
for asset, _ := range assetsPathMap {
if strings.HasSuffix(asset, ".sya") {
toRemoves = append(toRemoves, asset)
}
}
for _, toRemove := range toRemoves {
delete(assetsPathMap, toRemove)
}
for _, assetAbsPath := range assetsPathMap {
if _, ok := linkDestMap[assetAbsPath]; ok {
continue
}
var p string
if strings.HasPrefix(filepath.Join(util.DataDir, "assets"), assetAbsPath) {
p = assetAbsPath[strings.Index(assetAbsPath, "assets"):]
} else {
p = strings.TrimPrefix(assetAbsPath, util.DataDir)[1:]
}
p = filepath.ToSlash(p)
ret = append(ret, p)
}
sort.Strings(ret)
return
}
func assetsLinkDestsInTree(tree *parse.Tree) (ret []string) {
ret = []string{}
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
// 修改以下代码时需要同时修改 database 构造行级元素实现,增加必要的类型
if !entering || (ast.NodeLinkDest != n.Type && ast.NodeHTMLBlock != n.Type && ast.NodeInlineHTML != n.Type &&
ast.NodeIFrame != n.Type && ast.NodeWidget != n.Type && ast.NodeAudio != n.Type && ast.NodeVideo != n.Type) {
return ast.WalkContinue
}
if ast.NodeLinkDest == n.Type {
if !isRelativePath(n.Tokens) {
return ast.WalkContinue
}
dest := strings.TrimSpace(string(n.Tokens))
ret = append(ret, dest)
} else {
if ast.NodeWidget == n.Type {
dataAssets := n.IALAttr("data-assets")
if "" == dataAssets || !isRelativePath([]byte(dataAssets)) {
return ast.WalkContinue
}
ret = append(ret, dataAssets)
} else { // HTMLBlock/InlineHTML/IFrame/Audio/Video
if index := bytes.Index(n.Tokens, []byte("src=\"")); 0 < index {
src := n.Tokens[index+len("src=\""):]
src = src[:bytes.Index(src, []byte("\""))]
if !isRelativePath(src) {
return ast.WalkContinue
}
dest := strings.TrimSpace(string(src))
ret = append(ret, dest)
}
}
}
return ast.WalkContinue
})
return
}
func isRelativePath(dest []byte) bool {
if 1 > len(dest) {
return false
}
if '/' == dest[0] {
return false
}
return !bytes.Contains(dest, []byte(":"))
}
// allAssetAbsPaths 返回 asset 相对路径assets/xxx到绝对路径F:\SiYuan\data\assets\xxx的映射。
func allAssetAbsPaths() (assetsAbsPathMap map[string]string, err error) {
notebooks, err := ListNotebooks()
if nil != err {
return
}
assetsAbsPathMap = map[string]string{}
// 笔记本 assets
for _, notebook := range notebooks {
notebookAbsPath := filepath.Join(util.DataDir, notebook.ID)
filepath.Walk(notebookAbsPath, func(path string, info fs.FileInfo, err error) error {
if notebookAbsPath == path {
return nil
}
if isSkipFile(info.Name()) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
if info.IsDir() && "assets" == info.Name() {
filepath.Walk(path, func(assetPath string, info fs.FileInfo, err error) error {
if path == assetPath {
return nil
}
if isSkipFile(info.Name()) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
relPath := filepath.ToSlash(assetPath)
relPath = relPath[strings.Index(relPath, "assets/"):]
if info.IsDir() {
relPath += "/"
}
assetsAbsPathMap[relPath] = assetPath
return nil
})
return filepath.SkipDir
}
return nil
})
}
// 全局 assets
assets := filepath.Join(util.DataDir, "assets")
filepath.Walk(assets, func(assetPath string, info fs.FileInfo, err error) error {
if assets == assetPath {
return nil
}
if isSkipFile(info.Name()) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
relPath := filepath.ToSlash(assetPath)
relPath = relPath[strings.Index(relPath, "assets/"):]
if info.IsDir() {
relPath += "/"
}
assetsAbsPathMap[relPath] = assetPath
return nil
})
return
}
// copyBoxAssetsToDataAssets 将笔记本路径下所有(包括子文档)的 assets 复制一份到 data/assets 中。
func copyBoxAssetsToDataAssets(boxID string) {
boxLocalPath := filepath.Join(util.DataDir, boxID)
copyAssetsToDataAssets(boxLocalPath)
}
// copyDocAssetsToDataAssets 将文档路径下所有(包括子文档)的 assets 复制一份到 data/assets 中。
func copyDocAssetsToDataAssets(boxID, parentDocPath string) {
boxLocalPath := filepath.Join(util.DataDir, boxID)
parentDocDirAbsPath := filepath.Dir(filepath.Join(boxLocalPath, parentDocPath))
copyAssetsToDataAssets(parentDocDirAbsPath)
}
func copyAssetsToDataAssets(rootPath string) {
filesys.ReleaseFileLocks(rootPath)
var assetsDirPaths []string
filepath.Walk(rootPath, func(path string, info fs.FileInfo, err error) error {
if rootPath == path || nil == info {
return nil
}
isDir := info.IsDir()
name := info.Name()
if isSkipFile(name) {
if isDir {
return filepath.SkipDir
}
return nil
}
if "assets" == name && isDir {
assetsDirPaths = append(assetsDirPaths, path)
}
return nil
})
dataAssetsPath := filepath.Join(util.DataDir, "assets")
for _, assetsDirPath := range assetsDirPaths {
if err := gulu.File.Copy(assetsDirPath, dataAssetsPath); nil != err {
util.LogErrorf("copy tree assets from [%s] to [%s] failed: %s", assetsDirPaths, dataAssetsPath, err)
}
}
}

View file

@ -0,0 +1,95 @@
// SiYuan - Build Your Eternal Digital Garden
// 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/>.
//go:build !darwin
package model
import (
"path/filepath"
"time"
"github.com/fsnotify/fsnotify"
"github.com/siyuan-note/siyuan/kernel/util"
)
var assetsWatcher *fsnotify.Watcher
func WatchAssets() {
if "android" == util.Container {
return
}
go func() {
watchAssets()
}()
}
func watchAssets() {
assetsDir := filepath.Join(util.DataDir, "assets")
if nil != assetsWatcher {
assetsWatcher.Close()
}
var err error
if assetsWatcher, err = fsnotify.NewWatcher(); nil != err {
util.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err)
return
}
go func() {
var (
timer *time.Timer
lastEvent fsnotify.Event
)
timer = time.NewTimer(100 * time.Millisecond)
<-timer.C // timer should be expired at first
for {
select {
case event, ok := <-assetsWatcher.Events:
if !ok {
return
}
lastEvent = event
timer.Reset(time.Millisecond * 100)
case err, ok := <-assetsWatcher.Errors:
if !ok {
return
}
util.LogErrorf("watch assets failed: %s", err)
case <-timer.C:
//util.LogInfof("assets changed: %s", lastEvent)
if lastEvent.Op&fsnotify.Write == fsnotify.Write {
// 外部修改已有资源文件后纳入云端同步 https://github.com/siyuan-note/siyuan/issues/4694
IncWorkspaceDataVer()
}
}
}
}()
if err = assetsWatcher.Add(assetsDir); err != nil {
util.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err)
}
//util.LogInfof("added file watcher [%s]", assetsDir)
}
func CloseWatchAssets() {
if nil != assetsWatcher {
assetsWatcher.Close()
}
}

View file

@ -0,0 +1,88 @@
// SiYuan - Build Your Eternal Digital Garden
// 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/>.
//go:build darwin
package model
import (
"path/filepath"
"time"
"github.com/radovskyb/watcher"
"github.com/siyuan-note/siyuan/kernel/util"
)
var assetsWatcher *watcher.Watcher
func WatchAssets() {
if "iOS" == util.Container {
return
}
go func() {
watchAssets()
}()
}
func watchAssets() {
if nil != assetsWatcher {
assetsWatcher.Close()
}
assetsWatcher = watcher.New()
assetsDir := filepath.Join(util.DataDir, "assets")
go func() {
for {
select {
case event, ok := <-assetsWatcher.Event:
if !ok {
return
}
//util.LogInfof("assets changed: %s", event)
if watcher.Write == event.Op {
IncWorkspaceDataVer()
}
case err, ok := <-assetsWatcher.Error:
if !ok {
return
}
util.LogErrorf("watch assets failed: %s", err)
case <-assetsWatcher.Closed:
return
}
}
}()
if err := assetsWatcher.Add(assetsDir); nil != err {
util.LogErrorf("add assets watcher for folder [%s] failed: %s", assetsDir, err)
return
}
//util.LogInfof("added file watcher [%s]", assetsDir)
if err := assetsWatcher.Start(10 * time.Second); nil != err {
util.LogErrorf("start assets watcher for folder [%s] failed: %s", assetsDir, err)
return
}
}
func CloseWatchAssets() {
if nil != assetsWatcher {
assetsWatcher.Close()
}
}

509
kernel/model/backlink.go Normal file
View file

@ -0,0 +1,509 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"fmt"
"path"
"regexp"
"sort"
"strconv"
"strings"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/emirpasic/gods/sets/hashset"
"github.com/siyuan-note/siyuan/kernel/search"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func RefreshBacklink(id string) {
WaitForWritingFiles()
tx, err := sql.BeginTx()
if nil != err {
return
}
defer sql.CommitTx(tx)
refs := sql.QueryRefsByDefID(id, false)
trees := map[string]*parse.Tree{}
for _, ref := range refs {
tree := trees[ref.RootID]
if nil == tree {
tree, err = loadTreeByBlockID(ref.RootID)
if nil != err {
util.LogErrorf("refresh tree refs failed: %s", err)
continue
}
trees[ref.RootID] = tree
sql.UpsertRefs(tx, tree)
}
}
}
func CreateBacklink(defID, refID, refText string, isDynamic bool) (refRootID string, err error) {
refTree, err := loadTreeByBlockID(refID)
if nil != err {
return "", err
}
refNode := treenode.GetNodeInTree(refTree, refID)
if nil == refNode {
return
}
refRootID = refTree.Root.ID
defBlockTree := treenode.GetBlockTree(defID)
if nil == defBlockTree {
return
}
defRoot := sql.GetBlock(defBlockTree.RootID)
if nil == defRoot {
return
}
refTextLower := strings.ToLower(refText)
defBlock := sql.QueryBlockByNameOrAlias(defRoot.ID, refText)
if nil == defBlock {
if strings.ToLower(defRoot.Content) == refTextLower {
// 如果命名别名没有命中,但文档名和提及关键字匹配,则使用文档作为定义块
defBlock = defRoot
}
if nil == defBlock {
// 使用锚文本进行搜索,取第一个匹配的定义块
if defIDs := sql.QueryBlockDefIDsByRefText(refTextLower, nil); 0 < len(defIDs) {
if defBlock = sql.GetBlock(defIDs[0]); nil != defBlock {
goto OK
}
}
}
if nil == defBlock {
defBlock = sql.GetBlock(defBlockTree.ID)
}
if nil == defBlock {
return
}
if strings.ToLower(defBlock.Content) != refTextLower {
return
}
}
OK:
luteEngine := NewLute()
found := false
var toRemove []*ast.Node
ast.Walk(refNode, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeText != n.Type {
return ast.WalkContinue
}
text := gulu.Str.FromBytes(n.Tokens)
re := regexp.MustCompile("(?i)" + refText)
if strings.Contains(strings.ToLower(text), refTextLower) {
if isDynamic {
text = re.ReplaceAllString(text, "(("+defBlock.ID+" '"+refText+"'))")
} else {
text = re.ReplaceAllString(text, "(("+defBlock.ID+" \""+refText+"\"))")
}
found = true
subTree := parse.Inline("", []byte(text), luteEngine.ParseOptions)
var toInsert []*ast.Node
for newNode := subTree.Root.FirstChild.FirstChild; nil != newNode; newNode = newNode.Next {
toInsert = append(toInsert, newNode)
}
for _, insert := range toInsert {
n.InsertBefore(insert)
}
toRemove = append(toRemove, n)
}
return ast.WalkContinue
})
for _, n := range toRemove {
n.Unlink()
}
if found {
refTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
if err = indexWriteJSONQueue(refTree); nil != err {
return "", err
}
IncWorkspaceDataVer()
}
sql.WaitForWritingDatabase()
return
}
func BuildTreeBacklink(id, keyword, mentionKeyword string, beforeLen int) (boxID string, linkPaths, mentionPaths []*Path, linkRefsCount, mentionsCount int) {
linkPaths = []*Path{}
mentionPaths = []*Path{}
sqlBlock := sql.GetBlock(id)
if nil == sqlBlock {
return
}
rootID := sqlBlock.RootID
boxID = sqlBlock.Box
var links []*Block
refs := sql.QueryRefsByDefID(id, true)
// 为了减少查询,组装好 IDs 后一次查出
defSQLBlockIDs, refSQLBlockIDs := map[string]bool{}, map[string]bool{}
var queryBlockIDs []string
for _, ref := range refs {
defSQLBlockIDs[ref.DefBlockID] = true
refSQLBlockIDs[ref.BlockID] = true
queryBlockIDs = append(queryBlockIDs, ref.DefBlockID)
queryBlockIDs = append(queryBlockIDs, ref.BlockID)
}
querySQLBlocks := sql.GetBlocks(queryBlockIDs)
defSQLBlocksCache := map[string]*sql.Block{}
for _, defSQLBlock := range querySQLBlocks {
if nil != defSQLBlock && defSQLBlockIDs[defSQLBlock.ID] {
defSQLBlocksCache[defSQLBlock.ID] = defSQLBlock
}
}
refSQLBlocksCache := map[string]*sql.Block{}
for _, refSQLBlock := range querySQLBlocks {
if nil != refSQLBlock && refSQLBlockIDs[refSQLBlock.ID] {
refSQLBlocksCache[refSQLBlock.ID] = refSQLBlock
}
}
excludeBacklinkIDs := hashset.New()
for _, ref := range refs {
defSQLBlock := defSQLBlocksCache[(ref.DefBlockID)]
if nil == defSQLBlock {
continue
}
refSQLBlock := refSQLBlocksCache[ref.BlockID]
if nil == refSQLBlock {
continue
}
refBlock := fromSQLBlock(refSQLBlock, "", beforeLen)
if rootID == refBlock.RootID { // 排除当前文档内引用提及
excludeBacklinkIDs.Add(refBlock.RootID, refBlock.ID)
}
defBlock := fromSQLBlock(defSQLBlock, "", beforeLen)
if defBlock.RootID == rootID { // 当前文档的定义块
links = append(links, defBlock)
if ref.DefBlockID == defBlock.ID {
defBlock.Refs = append(defBlock.Refs, refBlock)
}
}
}
for _, link := range links {
for _, ref := range link.Refs {
excludeBacklinkIDs.Add(ref.RootID, ref.ID)
}
linkRefsCount += len(link.Refs)
}
var linkRefs []*Block
processedParagraphs := hashset.New()
var paragraphParentIDs []string
for _, link := range links {
for _, ref := range link.Refs {
if "NodeParagraph" == ref.Type {
paragraphParentIDs = append(paragraphParentIDs, ref.ParentID)
}
}
}
paragraphParents := sql.GetBlocks(paragraphParentIDs)
for _, p := range paragraphParents {
if "i" == p.Type {
linkRefs = append(linkRefs, fromSQLBlock(p, keyword, beforeLen))
processedParagraphs.Add(p.ID)
}
}
for _, link := range links {
for _, ref := range link.Refs {
if "NodeParagraph" == ref.Type {
if processedParagraphs.Contains(ref.ParentID) {
continue
}
}
ref.DefID = link.ID
ref.DefPath = link.Path
content := ref.Content
if "" != keyword {
_, content = search.MarkText(content, keyword, beforeLen, Conf.Search.CaseSensitive)
ref.Content = content
}
linkRefs = append(linkRefs, ref)
}
}
linkPaths = toSubTree(linkRefs, keyword)
mentions := buildTreeBackmention(sqlBlock, linkRefs, mentionKeyword, excludeBacklinkIDs, beforeLen)
mentionsCount = len(mentions)
mentionPaths = toFlatTree(mentions, 0, "backlink")
return
}
func buildTreeBackmention(defSQLBlock *sql.Block, refBlocks []*Block, keyword string, excludeBacklinkIDs *hashset.Set, beforeLen int) (ret []*Block) {
ret = []*Block{}
var names, aliases []string
var fName, rootID string
if "d" == defSQLBlock.Type {
if Conf.Search.BacklinkMentionName {
names = sql.QueryBlockNamesByRootID(defSQLBlock.ID)
}
if Conf.Search.BacklinkMentionAlias {
aliases = sql.QueryBlockAliases(defSQLBlock.ID)
}
if Conf.Search.BacklinkMentionDoc {
fName = path.Base(defSQLBlock.HPath)
}
rootID = defSQLBlock.ID
} else {
if Conf.Search.BacklinkMentionName {
if "" != defSQLBlock.Name {
names = append(names, defSQLBlock.Name)
}
}
if Conf.Search.BacklinkMentionAlias {
if "" != defSQLBlock.Alias {
aliases = strings.Split(defSQLBlock.Alias, ",")
}
}
root := treenode.GetBlockTree(defSQLBlock.RootID)
rootID = root.ID
}
set := hashset.New()
for _, name := range names {
set.Add(name)
}
for _, alias := range aliases {
set.Add(alias)
}
if "" != fName {
set.Add(fName)
}
if Conf.Search.BacklinkMentionAnchor {
for _, refBlock := range refBlocks {
refs := sql.QueryRefsByDefIDRefID(refBlock.DefID, refBlock.ID)
for _, ref := range refs {
set.Add(ref.Content)
}
}
}
var mentionKeywords []string
for _, v := range set.Values() {
mentionKeywords = append(mentionKeywords, v.(string))
}
ret = searchBackmention(mentionKeywords, keyword, excludeBacklinkIDs, rootID, beforeLen)
return
}
func searchBackmention(mentionKeywords []string, keyword string, excludeBacklinkIDs *hashset.Set, rootID string, beforeLen int) (ret []*Block) {
ret = []*Block{}
if 1 > len(mentionKeywords) {
return
}
sort.SliceStable(mentionKeywords, func(i, j int) bool {
return len(mentionKeywords[i]) < len(mentionKeywords[j])
})
table := "blocks_fts" // 大小写敏感
if !Conf.Search.CaseSensitive {
table = "blocks_fts_case_insensitive"
}
buf := bytes.Buffer{}
buf.WriteString("SELECT * FROM " + table + " WHERE " + table + " MATCH '{content}:(")
for i, mentionKeyword := range mentionKeywords {
if 511 < i { // 提及搜索最大限制 https://github.com/siyuan-note/siyuan/issues/3715
util.PushMsg(fmt.Sprintf(Conf.Language(38), len(mentionKeywords)), 5000)
mentionKeyword = strings.ReplaceAll(mentionKeyword, "\"", "\"\"")
buf.WriteString("\"" + mentionKeyword + "\"")
break
}
mentionKeyword = strings.ReplaceAll(mentionKeyword, "\"", "\"\"")
buf.WriteString("\"" + mentionKeyword + "\"")
if i < len(mentionKeywords)-1 {
buf.WriteString(" OR ")
}
}
buf.WriteString(")'")
if "" != keyword {
buf.WriteString(" AND MATCH '{content}:'")
buf.WriteString("\"" + keyword + "\"")
keyword = strings.ReplaceAll(keyword, "\"", "\"\"")
}
buf.WriteString(" AND root_id != '" + rootID + "'") // 不在定义块所在文档中搜索
buf.WriteString(" AND type IN ('d', 'h', 'p', 't')")
buf.WriteString(" ORDER BY id DESC LIMIT " + strconv.Itoa(Conf.Search.Limit))
query := buf.String()
sqlBlocks := sql.SelectBlocksRawStmt(query, Conf.Search.Limit)
blocks := fromSQLBlocks(&sqlBlocks, strings.Join(mentionKeywords, search.TermSep), beforeLen)
// 排除链接文本 https://github.com/siyuan-note/siyuan/issues/1542
luteEngine := NewLute()
var tmp []*Block
for _, b := range blocks {
tree := parse.Parse("", gulu.Str.ToBytes(b.Markdown), luteEngine.ParseOptions)
if nil == tree {
continue
}
textBuf := &bytes.Buffer{}
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || n.IsBlock() {
return ast.WalkContinue
}
if ast.NodeText == n.Type || ast.NodeLinkText == n.Type {
textBuf.Write(n.Tokens)
}
return ast.WalkContinue
})
text := textBuf.String()
text = strings.ToLower(text)
var contain bool
for _, mentionKeyword := range mentionKeywords {
if strings.Contains(text, strings.ToLower(mentionKeyword)) {
contain = true
break
}
}
if contain {
tmp = append(tmp, b)
}
}
blocks = tmp
mentionBlockMap := map[string]*Block{}
for _, block := range blocks {
mentionBlockMap[block.ID] = block
refText := getContainStr(block.Content, mentionKeywords)
block.RefText = refText
}
for _, mentionBlock := range mentionBlockMap {
if !excludeBacklinkIDs.Contains(mentionBlock.ID) {
ret = append(ret, mentionBlock)
}
}
sort.SliceStable(ret, func(i, j int) bool {
return ret[i].ID > ret[j].ID
})
return
}
func getContainStr(str string, strs []string) string {
str = strings.ToLower(str)
for _, s := range strs {
if strings.Contains(str, strings.ToLower(s)) {
return s
}
}
return ""
}
// buildFullLinks 构建正向和反向链接列表。
// forwardlinks正向链接关系 refs
// backlinks反向链接关系 defs
func buildFullLinks(condition string) (forwardlinks, backlinks []*Block) {
forwardlinks, backlinks = []*Block{}, []*Block{}
defs := buildDefsAndRefs(condition)
backlinks = append(backlinks, defs...)
for _, def := range defs {
for _, ref := range def.Refs {
forwardlinks = append(forwardlinks, ref)
}
}
return
}
func buildDefsAndRefs(condition string) (defBlocks []*Block) {
defBlockMap := map[string]*Block{}
refBlockMap := map[string]*Block{}
defRefs := sql.DefRefs(condition)
// 将 sql block 转为 block
for _, row := range defRefs {
for def, ref := range row {
if nil == ref {
continue
}
refBlock := refBlockMap[ref.ID]
if nil == refBlock {
refBlock = fromSQLBlock(ref, "", 0)
refBlockMap[ref.ID] = refBlock
}
// ref 块自己也需要作为定义块,否则图上没有节点
if defBlock := defBlockMap[ref.ID]; nil == defBlock {
defBlockMap[ref.ID] = refBlock
}
if defBlock := defBlockMap[def.ID]; nil == defBlock {
defBlock = fromSQLBlock(def, "", 0)
defBlockMap[def.ID] = defBlock
}
}
}
// 组装 block.Defs 和 block.Refs 字段
for _, row := range defRefs {
for def, ref := range row {
if nil == ref {
defBlock := fromSQLBlock(def, "", 0)
defBlockMap[def.ID] = defBlock
continue
}
refBlock := refBlockMap[ref.ID]
defBlock := defBlockMap[def.ID]
if refBlock.ID == defBlock.ID { // 自引用
continue
}
refBlock.Defs = append(refBlock.Defs, defBlock)
defBlock.Refs = append(defBlock.Refs, refBlock)
}
}
for _, def := range defBlockMap {
defBlocks = append(defBlocks, def)
}
return
}

601
kernel/model/backup.go Normal file
View file

@ -0,0 +1,601 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
"github.com/88250/gulu"
"github.com/dustin/go-humanize"
"github.com/siyuan-note/encryption"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/util"
)
type Backup struct {
Size int64 `json:"size"`
HSize string `json:"hSize"`
Updated string `json:"updated"`
SaveDir string `json:"saveDir"` // 本地备份数据存放目录路径
}
type Sync struct {
Size int64 `json:"size"`
HSize string `json:"hSize"`
Updated string `json:"updated"`
CloudName string `json:"cloudName"` // 云端同步数据存放目录名
SaveDir string `json:"saveDir"` // 本地同步数据存放目录路径
}
func RemoveCloudBackup() (err error) {
err = removeCloudDirPath("backup")
return
}
func getCloudAvailableBackupSize() (size int64, err error) {
var sync map[string]interface{}
var assetSize int64
sync, _, assetSize, err = getCloudSpaceOSS()
if nil != err {
return
}
var syncSize int64
if nil != sync {
syncSize = int64(sync["size"].(float64))
}
size = int64(Conf.User.UserSiYuanRepoSize) - syncSize - assetSize
return
}
func GetCloudSpace() (s *Sync, b *Backup, hSize, hAssetSize, hTotalSize string, err error) {
var sync, backup map[string]interface{}
var assetSize int64
sync, backup, assetSize, err = getCloudSpaceOSS()
if nil != err {
return nil, nil, "", "", "", errors.New(Conf.Language(30) + " " + err.Error())
}
var totalSize, syncSize, backupSize int64
var syncUpdated, backupUpdated string
if nil != sync {
syncSize = int64(sync["size"].(float64))
syncUpdated = sync["updated"].(string)
}
s = &Sync{
Size: syncSize,
HSize: humanize.Bytes(uint64(syncSize)),
Updated: syncUpdated,
}
if nil != backup {
backupSize = int64(backup["size"].(float64))
backupUpdated = backup["updated"].(string)
}
b = &Backup{
Size: backupSize,
HSize: humanize.Bytes(uint64(backupSize)),
Updated: backupUpdated,
}
totalSize = syncSize + backupSize + assetSize
hAssetSize = humanize.Bytes(uint64(assetSize))
hSize = humanize.Bytes(uint64(totalSize))
hTotalSize = byteCountSI(int64(Conf.User.UserSiYuanRepoSize))
return
}
func byteCountSI(b int64) string {
const unit = 1000
if b < unit {
return fmt.Sprintf("%d B", b)
}
div, exp := int64(unit), 0
for n := b / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp])
}
func GetLocalBackup() (ret *Backup, err error) {
backupDir := Conf.Backup.GetSaveDir()
if err = os.MkdirAll(backupDir, 0755); nil != err {
return
}
backup, err := os.Stat(backupDir)
ret = &Backup{
Updated: backup.ModTime().Format("2006-01-02 15:04:05"),
SaveDir: Conf.Backup.GetSaveDir(),
}
return
}
func RecoverLocalBackup() (err error) {
if "" == Conf.E2EEPasswd {
return errors.New(Conf.Language(11))
}
data := util.AESDecrypt(Conf.E2EEPasswd)
data, _ = hex.DecodeString(string(data))
passwd := string(data)
syncLock.Lock()
defer syncLock.Unlock()
CloseWatchAssets()
defer WatchAssets()
// 使用备份恢复时自动暂停同步,避免刚刚恢复后的数据又被同步覆盖 https://github.com/siyuan-note/siyuan/issues/4773
syncEnabled := Conf.Sync.Enabled
Conf.Sync.Enabled = false
Conf.Save()
filesys.ReleaseAllFileLocks()
util.PushEndlessProgress(Conf.Language(63))
util.LogInfof("starting recovery...")
start := time.Now()
decryptedDataDir, err := decryptDataDir(passwd)
if nil != err {
return
}
newDataDir := filepath.Join(util.WorkspaceDir, "data.new")
os.RemoveAll(newDataDir)
if err = os.MkdirAll(newDataDir, 0755); nil != err {
util.ClearPushProgress(100)
return
}
if err = stableCopy(decryptedDataDir, newDataDir); nil != err {
util.ClearPushProgress(100)
return
}
oldDataDir := filepath.Join(util.WorkspaceDir, "data.old")
if err = os.RemoveAll(oldDataDir); nil != err {
util.ClearPushProgress(100)
return
}
// 备份恢复时生成历史 https://github.com/siyuan-note/siyuan/issues/4752
if gulu.File.IsExist(util.DataDir) {
var historyDir string
historyDir, err = util.GetHistoryDir("backup")
if nil != err {
util.LogErrorf("get history dir failed: %s", err)
util.ClearPushProgress(100)
return
}
var dirs []os.DirEntry
dirs, err = os.ReadDir(util.DataDir)
if nil != err {
util.LogErrorf("read dir [%s] failed: %s", util.DataDir, err)
util.ClearPushProgress(100)
return
}
for _, dir := range dirs {
from := filepath.Join(util.DataDir, dir.Name())
to := filepath.Join(historyDir, dir.Name())
if err = os.Rename(from, to); nil != err {
util.LogErrorf("rename [%s] to [%s] failed: %s", from, to, err)
util.ClearPushProgress(100)
return
}
}
}
if gulu.File.IsExist(util.DataDir) {
if err = os.RemoveAll(util.DataDir); nil != err {
util.LogErrorf("remove [%s] failed: %s", util.DataDir, err)
util.ClearPushProgress(100)
return
}
}
if err = os.Rename(newDataDir, util.DataDir); nil != err {
util.ClearPushProgress(100)
util.LogErrorf("rename data dir from [%s] to [%s] failed: %s", newDataDir, util.DataDir, err)
return
}
elapsed := time.Now().Sub(start).Seconds()
size, _ := util.SizeOfDirectory(util.DataDir, false)
sizeStr := humanize.Bytes(uint64(size))
util.LogInfof("recovered backup [size=%s] in [%.2fs]", sizeStr, elapsed)
util.PushEndlessProgress(Conf.Language(62))
time.Sleep(2 * time.Second)
refreshFileTree()
if syncEnabled {
func() {
time.Sleep(5 * time.Second)
util.PushMsg(Conf.Language(134), 7000)
}()
}
return
}
func CreateLocalBackup() (err error) {
if "" == Conf.E2EEPasswd {
return errors.New(Conf.Language(11))
}
defer util.ClearPushProgress(100)
util.PushEndlessProgress(Conf.Language(22))
WaitForWritingFiles()
syncLock.Lock()
defer syncLock.Unlock()
filesys.ReleaseAllFileLocks()
util.LogInfof("creating backup...")
start := time.Now()
data := util.AESDecrypt(Conf.E2EEPasswd)
data, _ = hex.DecodeString(string(data))
passwd := string(data)
encryptedDataDir, err := encryptDataDir(passwd)
if nil != err {
util.LogErrorf("encrypt data dir failed: %s", err)
err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
return
}
newBackupDir := Conf.Backup.GetSaveDir() + ".new"
os.RemoveAll(newBackupDir)
if err = os.MkdirAll(newBackupDir, 0755); nil != err {
err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
return
}
if err = stableCopy(encryptedDataDir, newBackupDir); nil != err {
util.LogErrorf("copy encrypted data dir from [%s] to [%s] failed: %s", encryptedDataDir, newBackupDir, err)
err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
return
}
err = genCloudIndex(newBackupDir, map[string]bool{})
if nil != err {
return
}
conf := map[string]interface{}{"updated": time.Now().UnixMilli()}
data, err = gulu.JSON.MarshalJSON(conf)
if nil != err {
util.LogErrorf("marshal backup conf.json failed: %s", err)
} else {
confPath := filepath.Join(newBackupDir, "conf.json")
if err = os.WriteFile(confPath, data, 0644); nil != err {
util.LogErrorf("write backup conf.json [%s] failed: %s", confPath, err)
}
}
oldBackupDir := Conf.Backup.GetSaveDir() + ".old"
os.RemoveAll(oldBackupDir)
backupDir := Conf.Backup.GetSaveDir()
if gulu.File.IsExist(backupDir) {
if err = os.Rename(backupDir, oldBackupDir); nil != err {
util.LogErrorf("rename backup dir from [%s] to [%s] failed: %s", backupDir, oldBackupDir, err)
err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
return
}
}
if err = os.Rename(newBackupDir, backupDir); nil != err {
util.LogErrorf("rename backup dir from [%s] to [%s] failed: %s", newBackupDir, backupDir, err)
err = errors.New(fmt.Sprintf(Conf.Language(23), formatErrorMsg(err)))
return
}
os.RemoveAll(oldBackupDir)
elapsed := time.Now().Sub(start).Seconds()
size, _ := util.SizeOfDirectory(backupDir, false)
sizeStr := humanize.Bytes(uint64(size))
util.LogInfof("created backup [size=%s] in [%.2fs]", sizeStr, elapsed)
util.PushEndlessProgress(Conf.Language(21))
time.Sleep(2 * time.Second)
return
}
func DownloadBackup() (err error) {
syncLock.Lock()
defer syncLock.Unlock()
// 使用索引文件进行解密验证 https://github.com/siyuan-note/siyuan/issues/3789
var tmpFetchedFiles int
var tmpTransferSize uint64
err = ossDownload0(util.TempDir+"/backup", "backup", "/"+pathJSON, &tmpFetchedFiles, &tmpTransferSize, false)
if nil != err {
return
}
data, err := os.ReadFile(filepath.Join(util.TempDir, "/backup/"+pathJSON))
if nil != err {
return
}
passwdData, _ := hex.DecodeString(string(util.AESDecrypt(Conf.E2EEPasswd)))
passwd := string(passwdData)
data, err = encryption.AESGCMDecryptBinBytes(data, passwd)
if nil != err {
err = errors.New(Conf.Language(28))
return
}
localDirPath := Conf.Backup.GetSaveDir()
util.PushEndlessProgress(Conf.Language(68))
start := time.Now()
fetchedFiles, transferSize, err := ossDownload(localDirPath, "backup", false)
if nil == err {
elapsed := time.Now().Sub(start).Seconds()
util.LogInfof("downloaded backup [fetchedFiles=%d, transferSize=%s] in [%.2fs]", fetchedFiles, humanize.Bytes(transferSize), elapsed)
util.PushEndlessProgress(Conf.Language(69))
}
return
}
func UploadBackup() (err error) {
defer util.ClearPushProgress(100)
if err = checkUploadBackup(); nil != err {
return
}
syncLock.Lock()
defer syncLock.Unlock()
localDirPath := Conf.Backup.GetSaveDir()
util.PushEndlessProgress(Conf.Language(61))
util.LogInfof("uploading backup...")
start := time.Now()
wroteFiles, transferSize, err := ossUpload(localDirPath, "backup", "", false)
if nil == err {
elapsed := time.Now().Sub(start).Seconds()
util.LogInfof("uploaded backup [wroteFiles=%d, transferSize=%s] in [%.2fs]", wroteFiles, humanize.Bytes(transferSize), elapsed)
util.PushEndlessProgress(Conf.Language(41))
time.Sleep(2 * time.Second)
return
}
err = errors.New(formatErrorMsg(err))
return
}
var pathJSON = fmt.Sprintf("%x", md5.Sum([]byte("paths.json"))) // 6952277a5a37c17aa6a7c6d86cd507b1
func encryptDataDir(passwd string) (encryptedDataDir string, err error) {
encryptedDataDir = filepath.Join(util.WorkspaceDir, "incremental", "backup-encrypt")
if err = os.RemoveAll(encryptedDataDir); nil != err {
return
}
if err = os.MkdirAll(encryptedDataDir, 0755); nil != err {
return
}
ctime := map[string]time.Time{}
metaJSON := map[string]string{}
filepath.Walk(util.DataDir, func(path string, info fs.FileInfo, _ error) error {
if util.DataDir == path {
return nil
}
if isCloudSkipFile(path, info) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
plainP := strings.TrimPrefix(path, util.DataDir+string(os.PathSeparator))
p := plainP
parts := strings.Split(p, string(os.PathSeparator))
buf := bytes.Buffer{}
for i, part := range parts {
buf.WriteString(fmt.Sprintf("%x", sha256.Sum256([]byte(part)))[:7])
if i < len(parts)-1 {
buf.WriteString(string(os.PathSeparator))
}
}
p = buf.String()
metaJSON[filepath.ToSlash(p)] = filepath.ToSlash(plainP)
p = encryptedDataDir + string(os.PathSeparator) + p
if info.IsDir() {
if err = os.MkdirAll(p, 0755); nil != err {
return io.EOF
}
if fi, err0 := os.Stat(path); nil == err0 {
ctime[p] = fi.ModTime()
}
} else {
if err = os.MkdirAll(filepath.Dir(p), 0755); nil != err {
return io.EOF
}
f, err0 := os.Create(p)
if nil != err0 {
util.LogErrorf("create file [%s] failed: %s", p, err0)
err = err0
return io.EOF
}
data, err0 := os.ReadFile(path)
if nil != err0 {
util.LogErrorf("read file [%s] failed: %s", path, err0)
err = err0
return io.EOF
}
data, err0 = encryption.AESGCMEncryptBinBytes(data, passwd)
if nil != err0 {
util.LogErrorf("encrypt file [%s] failed: %s", path, err0)
err = errors.New("encrypt file failed")
return io.EOF
}
if _, err0 = f.Write(data); nil != err0 {
util.LogErrorf("write file [%s] failed: %s", p, err0)
err = err0
return io.EOF
}
if err0 = f.Close(); nil != err0 {
util.LogErrorf("close file [%s] failed: %s", p, err0)
err = err0
return io.EOF
}
fi, err0 := os.Stat(path)
if nil != err0 {
util.LogErrorf("stat file [%s] failed: %s", path, err0)
err = err0
return io.EOF
}
ctime[p] = fi.ModTime()
}
return nil
})
if nil != err {
return
}
for p, t := range ctime {
if err = os.Chtimes(p, t, t); nil != err {
return
}
}
// 检查文件是否全部已经编入索引
err = filepath.Walk(encryptedDataDir, func(path string, info fs.FileInfo, _ error) error {
if encryptedDataDir == path {
return nil
}
path = strings.TrimPrefix(path, encryptedDataDir+string(os.PathSeparator))
path = filepath.ToSlash(path)
if _, ok := metaJSON[path]; !ok {
util.LogErrorf("not found backup path in meta [%s]", path)
return errors.New(Conf.Language(27))
}
return nil
})
if nil != err {
return
}
data, err := gulu.JSON.MarshalJSON(metaJSON)
if nil != err {
return
}
data, err = encryption.AESGCMEncryptBinBytes(data, passwd)
if nil != err {
return "", errors.New("encrypt file failed")
}
meta := filepath.Join(encryptedDataDir, pathJSON)
if err = gulu.File.WriteFileSafer(meta, data, 0644); nil != err {
return
}
return
}
func decryptDataDir(passwd string) (decryptedDataDir string, err error) {
decryptedDataDir = filepath.Join(util.WorkspaceDir, "incremental", "backup-decrypt")
if err = os.RemoveAll(decryptedDataDir); nil != err {
return
}
backupDir := Conf.Backup.GetSaveDir()
meta := filepath.Join(backupDir, pathJSON)
data, err := os.ReadFile(meta)
if nil != err {
return
}
data, err = encryption.AESGCMDecryptBinBytes(data, passwd)
if nil != err {
return "", errors.New(Conf.Language(40))
}
metaJSON := map[string]string{}
if err = gulu.JSON.UnmarshalJSON(data, &metaJSON); nil != err {
return
}
modTimes := map[string]time.Time{}
err = filepath.Walk(backupDir, func(path string, info fs.FileInfo, _ error) error {
if backupDir == path || pathJSON == info.Name() || strings.HasSuffix(info.Name(), ".json") {
return nil
}
encryptedP := strings.TrimPrefix(path, backupDir+string(os.PathSeparator))
encryptedP = filepath.ToSlash(encryptedP)
plainP := filepath.Join(decryptedDataDir, metaJSON[encryptedP])
plainP = filepath.FromSlash(plainP)
if info.IsDir() {
if err = os.MkdirAll(plainP, 0755); nil != err {
return io.EOF
}
} else {
if err = os.MkdirAll(filepath.Dir(plainP), 0755); nil != err {
return io.EOF
}
var err0 error
data, err0 = os.ReadFile(path)
if nil != err0 {
util.LogErrorf("read file [%s] failed: %s", path, err0)
err = err0
return io.EOF
}
data, err0 = encryption.AESGCMDecryptBinBytes(data, passwd)
if nil != err0 {
util.LogErrorf("decrypt file [%s] failed: %s", path, err0)
err = errors.New(Conf.Language(40))
return io.EOF
}
if err0 = os.WriteFile(plainP, data, 0644); nil != err0 {
util.LogErrorf("write file [%s] failed: %s", plainP, err0)
err = err0
return io.EOF
}
}
fi, err0 := os.Stat(path)
if nil != err0 {
util.LogErrorf("stat file [%s] failed: %s", path, err0)
err = err0
return io.EOF
}
modTimes[plainP] = fi.ModTime()
return nil
})
for plainP, modTime := range modTimes {
if err = os.Chtimes(plainP, modTime, modTime); nil != err {
return
}
}
return
}

222
kernel/model/bazzar.go Normal file
View file

@ -0,0 +1,222 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"errors"
"fmt"
"path/filepath"
"github.com/88250/gulu"
"github.com/siyuan-note/siyuan/kernel/util"
"github.com/siyuan-note/siyuan/kernel/bazaar"
)
func GetPackageREADME(repoURL, repoHash string) (ret string) {
ret = bazaar.GetPackageREADME(repoURL, repoHash, Conf.System.NetworkProxy.String(), IsSubscriber(), Conf.System.ID)
return
}
func BazaarWidgets() (widgets []*bazaar.Widget) {
widgets = bazaar.Widgets(Conf.System.NetworkProxy.String())
for _, widget := range widgets {
widget.Installed = gulu.File.IsDir(filepath.Join(util.DataDir, "widgets", widget.Name))
if widget.Installed {
if widget.Installed {
if widgetConf, err := widgetJSON(widget.Name); nil == err && nil != widget {
if widget.Version != widgetConf["version"].(string) {
widget.Outdated = true
}
}
}
}
}
return
}
func InstallBazaarWidget(repoURL, repoHash, widgetName string) error {
syncLock.Lock()
defer syncLock.Unlock()
installPath := filepath.Join(util.DataDir, "widgets", widgetName)
err := bazaar.InstallWidget(repoURL, repoHash, installPath, Conf.System.NetworkProxy.String(), IsSubscriber(), Conf.System.ID)
if nil != err {
return errors.New(fmt.Sprintf(Conf.Language(46), widgetName))
}
return nil
}
func UninstallBazaarWidget(widgetName string) error {
syncLock.Lock()
defer syncLock.Unlock()
installPath := filepath.Join(util.DataDir, "widgets", widgetName)
err := bazaar.UninstallWidget(installPath)
if nil != err {
return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
}
return nil
}
func BazaarIcons() (icons []*bazaar.Icon) {
icons = bazaar.Icons(Conf.System.NetworkProxy.String())
for _, installed := range Conf.Appearance.Icons {
for _, icon := range icons {
if installed == icon.Name {
icon.Installed = true
if themeConf, err := iconJSON(icon.Name); nil == err {
if icon.Version != themeConf["version"].(string) {
icon.Outdated = true
}
}
}
icon.Current = icon.Name == Conf.Appearance.Icon
}
}
return
}
func InstallBazaarIcon(repoURL, repoHash, iconName string) error {
syncLock.Lock()
defer syncLock.Unlock()
installPath := filepath.Join(util.IconsPath, iconName)
err := bazaar.InstallIcon(repoURL, repoHash, installPath, Conf.System.NetworkProxy.String(), IsSubscriber(), Conf.System.ID)
if nil != err {
return errors.New(fmt.Sprintf(Conf.Language(46), iconName))
}
Conf.Appearance.Icon = iconName
Conf.Save()
InitAppearance()
return nil
}
func UninstallBazaarIcon(iconName string) error {
syncLock.Lock()
defer syncLock.Unlock()
installPath := filepath.Join(util.IconsPath, iconName)
err := bazaar.UninstallIcon(installPath)
if nil != err {
return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
}
InitAppearance()
return nil
}
func BazaarThemes() (ret []*bazaar.Theme) {
ret = bazaar.Themes(Conf.System.NetworkProxy.String())
installs := Conf.Appearance.DarkThemes
installs = append(installs, Conf.Appearance.LightThemes...)
for _, installed := range installs {
for _, theme := range ret {
if installed == theme.Name {
theme.Installed = true
if themeConf, err := themeJSON(theme.Name); nil == err {
theme.Outdated = theme.Version != themeConf["version"].(string)
}
theme.Current = theme.Name == Conf.Appearance.ThemeDark || theme.Name == Conf.Appearance.ThemeLight
}
}
}
return
}
func InstallBazaarTheme(repoURL, repoHash, themeName string, mode int, update bool) error {
syncLock.Lock()
defer syncLock.Unlock()
closeThemeWatchers()
installPath := filepath.Join(util.ThemesPath, themeName)
err := bazaar.InstallTheme(repoURL, repoHash, installPath, Conf.System.NetworkProxy.String(), IsSubscriber(), Conf.System.ID)
if nil != err {
return errors.New(fmt.Sprintf(Conf.Language(46), themeName))
}
if !update {
// 更新主题后不需要对该主题进行切换 https://github.com/siyuan-note/siyuan/issues/4966
if 0 == mode {
Conf.Appearance.ThemeLight = themeName
} else {
Conf.Appearance.ThemeDark = themeName
}
Conf.Appearance.Mode = mode
Conf.Appearance.ThemeJS = gulu.File.IsExist(filepath.Join(installPath, "theme.js"))
Conf.Save()
}
InitAppearance()
return nil
}
func UninstallBazaarTheme(themeName string) error {
syncLock.Lock()
defer syncLock.Unlock()
closeThemeWatchers()
installPath := filepath.Join(util.ThemesPath, themeName)
err := bazaar.UninstallTheme(installPath)
if nil != err {
return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
}
InitAppearance()
return nil
}
func BazaarTemplates() (templates []*bazaar.Template) {
templates = bazaar.Templates(Conf.System.NetworkProxy.String())
for _, template := range templates {
template.Installed = gulu.File.IsExist(filepath.Join(util.DataDir, "templates", template.Name))
if template.Installed {
if themeConf, err := templateJSON(template.Name); nil == err && nil != themeConf {
if template.Version != themeConf["version"].(string) {
template.Outdated = true
}
}
}
}
return
}
func InstallBazaarTemplate(repoURL, repoHash, templateName string) error {
syncLock.Lock()
defer syncLock.Unlock()
installPath := filepath.Join(util.DataDir, "templates", templateName)
err := bazaar.InstallTemplate(repoURL, repoHash, installPath, Conf.System.NetworkProxy.String(), IsSubscriber(), Conf.System.ID)
if nil != err {
return errors.New(fmt.Sprintf(Conf.Language(46), templateName))
}
return nil
}
func UninstallBazaarTemplate(templateName string) error {
syncLock.Lock()
defer syncLock.Unlock()
installPath := filepath.Join(util.DataDir, "templates", templateName)
err := bazaar.UninstallTemplate(installPath)
if nil != err {
return errors.New(fmt.Sprintf(Conf.Language(47), err.Error()))
}
return nil
}

179
kernel/model/block.go Normal file
View file

@ -0,0 +1,179 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"github.com/88250/lute"
"github.com/88250/lute/ast"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
)
// Block 描述了内容块。
type Block struct {
Box string `json:"box"`
Path string `json:"path"`
HPath string `json:"hPath"`
ID string `json:"id"`
RootID string `json:"rootID"`
ParentID string `json:"parentID"`
Name string `json:"name"`
Alias string `json:"alias"`
Memo string `json:"memo"`
Tag string `json:"tag"`
Content string `json:"content"`
FContent string `json:"fcontent"`
Markdown string `json:"markdown"`
Folded bool `json:"folded"`
Type string `json:"type"`
SubType string `json:"subType"`
RefText string `json:"refText"`
Defs []*Block `json:"-"` // 当前块引用了这些块,避免序列化 JSON 时产生循环引用
Refs []*Block `json:"refs"` // 当前块被这些块引用
DefID string `json:"defID"`
DefPath string `json:"defPath"`
IAL map[string]string `json:"ial"`
Children []*Block `json:"children"`
Depth int `json:"depth"`
Count int `json:"count"`
}
func (block *Block) IsContainerBlock() bool {
switch block.Type {
case "NodeDocument", "NodeBlockquote", "NodeList", "NodeListItem", "NodeSuperBlock":
return true
}
return false
}
type Path struct {
ID string `json:"id"` // 块 ID
Box string `json:"box"` // 块 Box
Name string `json:"name"` // 当前路径
Full string `json:"full"` // 全路径
Type string `json:"type"` // "path"
NodeType string `json:"nodeType"` // 节点类型
SubType string `json:"subType"` // 节点子类型
Blocks []*Block `json:"blocks"` // 子块节点
Children []*Path `json:"children"` // 子路径节点
Depth int `json:"depth"` // 层级深度
Count int `json:"count"` // 子块计数
}
func RecentUpdatedBlocks() (ret []*Block) {
ret = []*Block{}
sqlBlocks := sql.QueryRecentUpdatedBlocks()
if 1 > len(sqlBlocks) {
return
}
ret = fromSQLBlocks(&sqlBlocks, "", 0)
return
}
func GetBlockDOM(id string) (ret string) {
if "" == id {
return
}
tree, err := loadTreeByBlockID(id)
if nil != err {
return
}
node := treenode.GetNodeInTree(tree, id)
luteEngine := NewLute()
ret = lute.RenderNodeBlockDOM(node, luteEngine.ParseOptions, luteEngine.RenderOptions)
return
}
func GetBlock(id string) (ret *Block, err error) {
ret, err = getBlock(id)
return
}
func getBlock(id string) (ret *Block, err error) {
if "" == id {
return
}
tree, err := loadTreeByBlockID(id)
if nil != err {
return
}
node := treenode.GetNodeInTree(tree, id)
sqlBlock := sql.BuildBlockFromNode(node, tree)
if nil == sqlBlock {
return
}
ret = fromSQLBlock(sqlBlock, "", 0)
return
}
func getBlockRendered(id string, headingMode int) (ret *Block) {
tree, _ := loadTreeByBlockID(id)
if nil == tree {
return
}
def := treenode.GetNodeInTree(tree, id)
if nil == def {
return
}
var unlinks, nodes []*ast.Node
ast.Walk(def, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeHeading == n.Type {
if "1" == n.IALAttr("fold") {
children := treenode.FoldedHeadingChildren(n)
for _, c := range children {
unlinks = append(unlinks, c)
}
}
}
return ast.WalkContinue
})
for _, n := range unlinks {
n.Unlink()
}
nodes = append(nodes, def)
if 0 == headingMode && ast.NodeHeading == def.Type && "1" != def.IALAttr("fold") {
children := treenode.HeadingChildren(def)
for _, c := range children {
if "1" == c.IALAttr("heading-fold") {
// 嵌入块包含折叠标题时不应该显示其下方块 https://github.com/siyuan-note/siyuan/issues/4765
continue
}
nodes = append(nodes, c)
}
}
b := treenode.GetBlockTree(def.ID)
if nil == b {
return
}
luteEngine := NewLute()
luteEngine.RenderOptions.ProtyleContenteditable = false // 不可编辑
dom := renderBlockDOMByNodes(nodes, luteEngine)
ret = &Block{Box: def.Box, Path: def.Path, HPath: b.HPath, ID: def.ID, Type: def.Type.String(), Content: dom}
return
}

186
kernel/model/blockial.go Normal file
View file

@ -0,0 +1,186 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"errors"
"fmt"
"time"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/html"
"github.com/88250/lute/lex"
"github.com/88250/lute/parse"
"github.com/araddon/dateparse"
"github.com/siyuan-note/siyuan/kernel/cache"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func SetBlockReminder(id string, timed string) (err error) {
if !IsSubscriber() {
if "ios" == util.Container {
return errors.New(Conf.Language(122))
}
return errors.New(Conf.Language(29))
}
var timedMills int64
if "0" != timed {
t, e := dateparse.ParseIn(timed, time.Now().Location())
if nil != e {
return e
}
timedMills = t.UnixMilli()
}
attrs := GetBlockAttrs(id) // 获取属性是会等待树写入
tree, err := loadTreeByBlockID(id)
if nil != err {
return
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
return errors.New(fmt.Sprintf(Conf.Language(15), id))
}
if ast.NodeDocument != node.Type && node.IsContainerBlock() {
node = treenode.FirstLeafBlock(node)
}
content := treenode.NodeStaticContent(node)
content = gulu.Str.SubStr(content, 128)
err = SetCloudBlockReminder(id, content, timedMills)
if nil != err {
return
}
attrName := "custom-reminder-wechat"
if "0" == timed {
delete(attrs, attrName)
old := node.IALAttr(attrName)
oldTimedMills, e := dateparse.ParseIn(old, time.Now().Location())
if nil == e {
util.PushMsg(fmt.Sprintf(Conf.Language(109), oldTimedMills.Format("2006-01-02 15:04")), 3000)
}
node.RemoveIALAttr(attrName)
} else {
attrs[attrName] = timed
node.SetIALAttr(attrName, timed)
util.PushMsg(fmt.Sprintf(Conf.Language(101), time.UnixMilli(timedMills).Format("2006-01-02 15:04")), 5000)
}
if err = indexWriteJSONQueue(tree); nil != err {
return
}
IncWorkspaceDataVer()
cache.PutBlockIAL(id, attrs)
return
}
func SetBlockAttrs(id string, nameValues map[string]string) (err error) {
WaitForWritingFiles()
tree, err := loadTreeByBlockID(id)
if nil != err {
return err
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
return errors.New(fmt.Sprintf(Conf.Language(15), id))
}
for name, _ := range nameValues {
for i := 0; i < len(name); i++ {
if !lex.IsASCIILetterNumHyphen(name[i]) {
return errors.New(fmt.Sprintf(Conf.Language(25), id))
}
}
}
for name, value := range nameValues {
if "" == value {
node.RemoveIALAttr(name)
} else {
node.SetIALAttr(name, html.EscapeAttrVal(value))
}
}
if err = indexWriteJSONQueue(tree); nil != err {
return
}
IncWorkspaceDataVer()
cache.PutBlockIAL(id, parse.IAL2Map(node.KramdownIAL))
return
}
func ResetBlockAttrs(id string, nameValues map[string]string) (err error) {
tree, err := loadTreeByBlockID(id)
if nil != err {
return err
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
return errors.New(fmt.Sprintf(Conf.Language(15), id))
}
for name, _ := range nameValues {
for i := 0; i < len(name); i++ {
if !lex.IsASCIILetterNumHyphen(name[i]) {
return errors.New(fmt.Sprintf(Conf.Language(25), id))
}
}
}
node.ClearIALAttrs()
for name, value := range nameValues {
if "" != value {
node.SetIALAttr(name, value)
}
}
if err = indexWriteJSONQueue(tree); nil != err {
return
}
IncWorkspaceDataVer()
cache.RemoveBlockIAL(id)
return
}
func GetBlockAttrs(id string) (ret map[string]string) {
ret = map[string]string{}
if cached := cache.GetBlockIAL(id); nil != cached {
ret = cached
return
}
WaitForWritingFiles()
tree, err := loadTreeByBlockID(id)
if nil != err {
return
}
node := treenode.GetNodeInTree(tree, id)
for _, kv := range node.KramdownIAL {
ret[kv[0]] = html.UnescapeAttrVal(kv[1])
}
cache.PutBlockIAL(id, ret)
return
}

249
kernel/model/blockinfo.go Normal file
View file

@ -0,0 +1,249 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"os"
"path"
"path/filepath"
"sort"
"strings"
"unicode/utf8"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/html"
"github.com/88250/lute/parse"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
type BlockInfo struct {
ID string `json:"id"`
Name string `json:"name"`
RefCount int `json:"refCount"`
SubFileCount int `json:"subFileCount"`
RefIDs []string `json:"refIDs"`
IAL map[string]string `json:"ial"`
Icon string `json:"icon"`
}
func GetDocInfo(rootID string) (ret *BlockInfo) {
WaitForWritingFiles()
tree, err := loadTreeByBlockID(rootID)
if nil != err {
util.LogErrorf("load tree by block id failed: %s", err)
return
}
title := tree.Root.IALAttr("title")
ret = &BlockInfo{ID: rootID, Name: title}
ret.IAL = parse.IAL2Map(tree.Root.KramdownIAL)
ret.RefIDs, _ = sql.QueryRefIDsByDefID(rootID, false)
ret.RefCount = len(ret.RefIDs)
var subFileCount int
boxLocalPath := filepath.Join(util.DataDir, tree.Box)
subFiles, err := os.ReadDir(filepath.Join(boxLocalPath, strings.TrimSuffix(tree.Path, ".sy")))
if nil == err {
for _, subFile := range subFiles {
if strings.HasSuffix(subFile.Name(), ".sy") {
subFileCount++
}
}
}
ret.SubFileCount = subFileCount
ret.Icon = tree.Root.IALAttr("icon")
return
}
func GetBlockRefText(id string) string {
WaitForWritingFiles()
bt := treenode.GetBlockTree(id)
if nil == bt {
return ErrBlockNotFound.Error()
}
tree, err := loadTreeByBlockID(id)
if nil != err {
return ErrTreeNotFound.Error()
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
return ErrBlockNotFound.Error()
}
if name := node.IALAttr("name"); "" != name {
return name
}
switch node.Type {
case ast.NodeBlockQueryEmbed:
return "Query Embed Block..."
case ast.NodeIFrame:
return "IFrame..."
case ast.NodeThematicBreak:
return "Thematic Break..."
case ast.NodeVideo:
return "Video..."
case ast.NodeAudio:
return "Audio..."
}
if ast.NodeDocument != node.Type && node.IsContainerBlock() {
node = treenode.FirstLeafBlock(node)
}
ret := renderBlockText(node)
if Conf.Editor.BlockRefDynamicAnchorTextMaxLen < utf8.RuneCountInString(ret) {
ret = gulu.Str.SubStr(ret, Conf.Editor.BlockRefDynamicAnchorTextMaxLen) + "..."
}
return ret
}
func GetBlockRefIDs(id string) (refIDs, refTexts, defIDs []string) {
refIDs = []string{}
bt := treenode.GetBlockTree(id)
if nil == bt {
return
}
isDoc := bt.ID == bt.RootID
refIDs, refTexts = sql.QueryRefIDsByDefID(id, isDoc)
if isDoc {
defIDs = sql.QueryChildDefIDsByRootDefID(id)
} else {
defIDs = append(defIDs, id)
}
return
}
func GetBlockRefIDsByFileAnnotationID(id string) (refIDs, refTexts []string) {
refIDs, refTexts = sql.QueryRefIDsByAnnotationID(id)
return
}
func GetBlockDefIDsByRefText(refText string, excludeIDs []string) (ret []string) {
ret = sql.QueryBlockDefIDsByRefText(refText, excludeIDs)
sort.Sort(sort.Reverse(sort.StringSlice(ret)))
return
}
type BlockPath struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
SubType string `json:"subType"`
Children []*BlockPath `json:"children"`
}
func BuildBlockBreadcrumb(id string) (ret []*BlockPath, err error) {
ret = []*BlockPath{}
tree, err := loadTreeByBlockID(id)
if nil == tree {
err = nil
return
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
return
}
ret = buildBlockBreadcrumb(node)
return
}
func buildBlockBreadcrumb(node *ast.Node) (ret []*BlockPath) {
ret = []*BlockPath{}
if nil == node {
return
}
box := Conf.Box(node.Box)
if nil == box {
return
}
headingLevel := 16
maxNameLen := 1024
boxName := box.Name
var hPath string
baseBlock := treenode.GetBlockTreeRootByPath(node.Box, node.Path)
if nil != baseBlock {
hPath = baseBlock.HPath
}
for parent := node; nil != parent; parent = parent.Parent {
if "" == parent.ID {
continue
}
id := parent.ID
name := html.EscapeHTMLStr(parent.IALAttr("name"))
if ast.NodeDocument == parent.Type {
name = html.EscapeHTMLStr(path.Join(boxName, hPath))
} else {
if "" == name {
if ast.NodeListItem == parent.Type {
name = gulu.Str.SubStr(renderBlockText(parent.FirstChild), maxNameLen)
} else {
name = gulu.Str.SubStr(renderBlockText(parent), maxNameLen)
}
}
if ast.NodeHeading == parent.Type {
headingLevel = parent.HeadingLevel
}
}
add := true
if ast.NodeList == parent.Type || ast.NodeSuperBlock == parent.Type || ast.NodeBlockquote == parent.Type {
add = false
}
if ast.NodeParagraph == parent.Type && nil != parent.Parent && ast.NodeListItem == parent.Parent.Type && nil == parent.Next && nil == parent.Previous {
add = false
}
if ast.NodeListItem == parent.Type {
if "" == name {
name = gulu.Str.SubStr(renderBlockText(parent.FirstChild), maxNameLen)
}
}
if add {
ret = append([]*BlockPath{{
ID: id,
Name: name,
Type: parent.Type.String(),
SubType: treenode.SubTypeAbbr(parent),
}}, ret...)
}
for prev := parent.Previous; nil != prev; prev = prev.Previous {
if ast.NodeHeading == prev.Type && headingLevel > prev.HeadingLevel {
name = gulu.Str.SubStr(renderBlockText(prev), maxNameLen)
ret = append([]*BlockPath{{
ID: prev.ID,
Name: name,
Type: prev.Type.String(),
SubType: treenode.SubTypeAbbr(prev),
}}, ret...)
headingLevel = prev.HeadingLevel
}
}
}
return
}

138
kernel/model/bookmark.go Normal file
View file

@ -0,0 +1,138 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"errors"
"fmt"
"sort"
"strings"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func RenameBookmark(oldBookmark, newBookmark string) (err error) {
if treenode.ContainsMarker(newBookmark) {
return errors.New(Conf.Language(112))
}
newBookmark = strings.TrimSpace(newBookmark)
if "" == newBookmark {
return errors.New(Conf.Language(126))
}
if oldBookmark == newBookmark {
return
}
util.PushEndlessProgress(Conf.Language(110))
bookmarks := sql.QueryBookmarkBlocksByKeyword(oldBookmark)
treeBlocks := map[string][]string{}
for _, tag := range bookmarks {
if blocks, ok := treeBlocks[tag.RootID]; !ok {
treeBlocks[tag.RootID] = []string{tag.ID}
} else {
treeBlocks[tag.RootID] = append(blocks, tag.ID)
}
}
for treeID, blocks := range treeBlocks {
util.PushEndlessProgress("[" + treeID + "]")
tree, e := loadTreeByBlockID(treeID)
if nil != e {
util.ClearPushProgress(100)
return e
}
for _, blockID := range blocks {
node := treenode.GetNodeInTree(tree, blockID)
if nil == node {
continue
}
if bookmarkAttrVal := node.IALAttr("bookmark"); bookmarkAttrVal == oldBookmark {
node.SetIALAttr("bookmark", newBookmark)
}
}
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(111), tree.Root.IALAttr("title")))
if err = writeJSONQueue(tree); nil != err {
util.ClearPushProgress(100)
return
}
util.RandomSleep(50, 150)
}
util.PushEndlessProgress(Conf.Language(113))
sql.WaitForWritingDatabase()
util.ReloadUI()
return
}
type BookmarkLabel string
type BookmarkBlocks []*Block
type Bookmark struct {
Name BookmarkLabel `json:"name"`
Blocks []*Block `json:"blocks"`
Type string `json:"type"` // "bookmark"
Depth int `json:"depth"`
Count int `json:"count"`
}
type Bookmarks []*Bookmark
func (s Bookmarks) Len() int { return len(s) }
func (s Bookmarks) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s Bookmarks) Less(i, j int) bool { return s[i].Name < s[j].Name }
func BookmarkLabels() (ret []string) {
ret = sql.QueryBookmarkLabels()
return
}
func BuildBookmark() (ret *Bookmarks) {
WaitForWritingFiles()
sql.WaitForWritingDatabase()
ret = &Bookmarks{}
sqlBlocks := sql.QueryBookmarkBlocks()
labelBlocks := map[BookmarkLabel]BookmarkBlocks{}
blocks := fromSQLBlocks(&sqlBlocks, "", 0)
for _, block := range blocks {
label := BookmarkLabel(block.IAL["bookmark"])
if bs, ok := labelBlocks[label]; ok {
bs = append(bs, block)
labelBlocks[label] = bs
} else {
labelBlocks[label] = []*Block{block}
}
}
for label, bs := range labelBlocks {
for _, b := range bs {
b.Depth = 1
}
*ret = append(*ret, &Bookmark{Name: label, Blocks: bs, Type: "bookmark", Count: len(bs)})
}
sort.Sort(ret)
return
}

537
kernel/model/box.go Normal file
View file

@ -0,0 +1,537 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"sort"
"strings"
"time"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/facette/natsort"
"github.com/siyuan-note/siyuan/kernel/conf"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
// Box 笔记本。
type Box struct {
ID string `json:"id"`
Name string `json:"name"`
Icon string `json:"icon"`
Sort int `json:"sort"`
Closed bool `json:"closed"`
historyGenerated int64 // 最近一次历史生成时间
}
func AutoStat() {
for range time.Tick(10 * time.Minute) {
autoStat()
}
}
func autoStat() {
Conf.Stat.DocCount = sql.CountAllDoc()
Conf.Save()
}
func ListNotebooks() (ret []*Box, err error) {
ret = []*Box{}
dirs, err := os.ReadDir(util.DataDir)
if nil != err {
util.LogErrorf("read dir [%s] failed: %s", util.DataDir, err)
return ret, err
}
for _, dir := range dirs {
if util.IsReservedFilename(dir.Name()) {
continue
}
if !dir.IsDir() {
continue
}
if !util.IsIDPattern(dir.Name()) {
continue
}
boxConf := conf.NewBoxConf()
boxConfPath := filepath.Join(util.DataDir, dir.Name(), ".siyuan", "conf.json")
if !gulu.File.IsExist(boxConfPath) {
if isUserGuide(dir.Name()) {
filesys.ReleaseAllFileLocks()
os.RemoveAll(filepath.Join(util.DataDir, dir.Name()))
util.LogWarnf("not found user guid box conf [%s], removed it", boxConfPath)
continue
}
util.LogWarnf("not found box conf [%s], recreate it", boxConfPath)
} else {
data, readErr := filesys.NoLockFileRead(boxConfPath)
if nil != readErr {
util.LogErrorf("read box conf [%s] failed: %s", boxConfPath, readErr)
continue
}
if readErr = gulu.JSON.UnmarshalJSON(data, boxConf); nil != readErr {
util.LogErrorf("parse box conf [%s] failed: %s", boxConfPath, readErr)
continue
}
}
id := dir.Name()
ret = append(ret, &Box{
ID: id,
Name: boxConf.Name,
Icon: boxConf.Icon,
Sort: boxConf.Sort,
Closed: boxConf.Closed,
})
}
switch Conf.FileTree.Sort {
case util.SortModeNameASC:
sort.Slice(ret, func(i, j int) bool {
return util.PinYinCompare(util.RemoveEmoji(ret[i].Name), util.RemoveEmoji(ret[j].Name))
})
case util.SortModeNameDESC:
sort.Slice(ret, func(i, j int) bool {
return util.PinYinCompare(util.RemoveEmoji(ret[j].Name), util.RemoveEmoji(ret[i].Name))
})
case util.SortModeUpdatedASC:
case util.SortModeUpdatedDESC:
case util.SortModeAlphanumASC:
sort.Slice(ret, func(i, j int) bool {
return natsort.Compare(util.RemoveEmoji(ret[i].Name), util.RemoveEmoji(ret[j].Name))
})
case util.SortModeAlphanumDESC:
sort.Slice(ret, func(i, j int) bool {
return natsort.Compare(util.RemoveEmoji(ret[j].Name), util.RemoveEmoji(ret[i].Name))
})
case util.SortModeCustom:
sort.Slice(ret, func(i, j int) bool { return ret[i].Sort < ret[j].Sort })
case util.SortModeRefCountASC:
case util.SortModeRefCountDESC:
case util.SortModeCreatedASC:
sort.Slice(ret, func(i, j int) bool { return natsort.Compare(ret[j].ID, ret[i].ID) })
case util.SortModeCreatedDESC:
sort.Slice(ret, func(i, j int) bool { return natsort.Compare(ret[j].ID, ret[i].ID) })
}
return
}
func (box *Box) GetConf() (ret *conf.BoxConf) {
ret = conf.NewBoxConf()
confPath := filepath.Join(util.DataDir, box.ID, ".siyuan/conf.json")
if !gulu.File.IsExist(confPath) {
return
}
data, err := filesys.LockFileRead(confPath)
if nil != err {
util.LogErrorf("read box conf [%s] failed: %s", confPath, err)
return
}
if err = gulu.JSON.UnmarshalJSON(data, ret); nil != err {
util.LogErrorf("parse box conf [%s] failed: %s", confPath, err)
return
}
return
}
func (box *Box) SaveConf(conf *conf.BoxConf) {
confPath := filepath.Join(util.DataDir, box.ID, ".siyuan/conf.json")
newData, err := gulu.JSON.MarshalIndentJSON(conf, "", " ")
if nil != err {
util.LogErrorf("marshal box conf [%s] failed: %s", confPath, err)
return
}
oldData, err := filesys.NoLockFileRead(confPath)
if nil != err {
box.saveConf0(newData)
return
}
if bytes.Equal(newData, oldData) {
return
}
box.saveConf0(newData)
}
func (box *Box) saveConf0(data []byte) {
confPath := filepath.Join(util.DataDir, box.ID, ".siyuan/conf.json")
if err := os.MkdirAll(filepath.Join(util.DataDir, box.ID, ".siyuan"), 0755); nil != err {
util.LogErrorf("save box conf [%s] failed: %s", confPath, err)
}
if err := filesys.LockFileWrite(confPath, data); nil != err {
util.LogErrorf("save box conf [%s] failed: %s", confPath, err)
}
}
func (box *Box) Ls(p string) (ret []*FileInfo, totals int, err error) {
boxLocalPath := filepath.Join(util.DataDir, box.ID)
if strings.HasSuffix(p, ".sy") {
dir := strings.TrimSuffix(p, ".sy")
absDir := filepath.Join(boxLocalPath, dir)
if gulu.File.IsDir(absDir) {
p = dir
} else {
return
}
}
files, err := ioutil.ReadDir(filepath.Join(util.DataDir, box.ID, p))
if nil != err {
return
}
for _, f := range files {
if util.IsReservedFilename(f.Name()) {
continue
}
totals += 1
fi := &FileInfo{}
fi.name = f.Name()
fi.isdir = f.IsDir()
fi.size = f.Size()
fPath := path.Join(p, f.Name())
if f.IsDir() {
fPath += "/"
}
fi.path = fPath
ret = append(ret, fi)
}
return
}
func (box *Box) Stat(p string) (ret *FileInfo) {
absPath := filepath.Join(util.DataDir, box.ID, p)
info, err := os.Stat(absPath)
if nil != err {
if !os.IsNotExist(err) {
util.LogErrorf("stat [%s] failed: %s", absPath, err)
}
return
}
ret = &FileInfo{
path: p,
name: info.Name(),
size: info.Size(),
isdir: info.IsDir(),
}
return
}
func (box *Box) Exist(p string) bool {
return gulu.File.IsExist(filepath.Join(util.DataDir, box.ID, p))
}
func (box *Box) Mkdir(path string) error {
if err := os.Mkdir(filepath.Join(util.DataDir, box.ID, path), 0755); nil != err {
msg := fmt.Sprintf(Conf.Language(6), box.Name, path, err)
util.LogErrorf("mkdir [path=%s] in box [%s] failed: %s", path, box.ID, err)
return errors.New(msg)
}
IncWorkspaceDataVer()
return nil
}
func (box *Box) MkdirAll(path string) error {
if err := os.MkdirAll(filepath.Join(util.DataDir, box.ID, path), 0755); nil != err {
msg := fmt.Sprintf(Conf.Language(6), box.Name, path, err)
util.LogErrorf("mkdir all [path=%s] in box [%s] failed: %s", path, box.ID, err)
return errors.New(msg)
}
IncWorkspaceDataVer()
return nil
}
func (box *Box) Move(oldPath, newPath string) error {
boxLocalPath := filepath.Join(util.DataDir, box.ID)
fromPath := filepath.Join(boxLocalPath, oldPath)
toPath := filepath.Join(boxLocalPath, newPath)
filesys.ReleaseFileLocks(fromPath)
if err := os.Rename(fromPath, toPath); nil != err {
msg := fmt.Sprintf(Conf.Language(5), box.Name, fromPath, err)
util.LogErrorf("move [path=%s] in box [%s] failed: %s", fromPath, box.Name, err)
return errors.New(msg)
}
if oldDir := path.Dir(oldPath); util.IsIDPattern(path.Base(oldDir)) {
fromDir := filepath.Join(boxLocalPath, oldDir)
if util.IsEmptyDir(fromDir) {
os.Remove(fromDir)
}
}
IncWorkspaceDataVer()
return nil
}
func (box *Box) Remove(path string) error {
boxLocalPath := filepath.Join(util.DataDir, box.ID)
filePath := filepath.Join(boxLocalPath, path)
filesys.ReleaseFileLocks(filePath)
if err := os.RemoveAll(filePath); nil != err {
msg := fmt.Sprintf(Conf.Language(7), box.Name, path, err)
util.LogErrorf("remove [path=%s] in box [%s] failed: %s", path, box.ID, err)
return errors.New(msg)
}
IncWorkspaceDataVer()
return nil
}
func (box *Box) Unindex() {
tx, err := sql.BeginTx()
if nil != err {
return
}
sql.RemoveBoxHash(tx, box.ID)
sql.DeleteByBoxTx(tx, box.ID)
sql.CommitTx(tx)
filesys.ReleaseFileLocks(filepath.Join(util.DataDir, box.ID))
treenode.RemoveBlockTreesByBoxID(box.ID)
}
func (box *Box) ListFiles(path string) (ret []*FileInfo) {
fis, _, err := box.Ls(path)
if nil != err {
return
}
box.listFiles(&fis, &ret)
return
}
func (box *Box) listFiles(files, ret *[]*FileInfo) {
for _, file := range *files {
if file.isdir {
fis, _, err := box.Ls(file.path)
if nil == err {
box.listFiles(&fis, ret)
}
*ret = append(*ret, file)
} else {
*ret = append(*ret, file)
}
}
return
}
func isSkipFile(filename string) bool {
return strings.HasPrefix(filename, ".") || "node_modules" == filename || "dist" == filename || "target" == filename
}
func checkUploadBackup() (err error) {
if !IsSubscriber() {
if "ios" == util.Container {
return errors.New(Conf.Language(122))
}
return errors.New(Conf.Language(29))
}
backupDir := Conf.Backup.GetSaveDir()
backupSize, err := util.SizeOfDirectory(backupDir, false)
if nil != err {
return
}
cloudAvailableBackupSize, err := getCloudAvailableBackupSize()
if nil != err {
return
}
if cloudAvailableBackupSize < backupSize {
return errors.New(fmt.Sprintf(Conf.Language(43), byteCountSI(int64(Conf.User.UserSiYuanRepoSize))))
}
return nil
}
func (box *Box) renameSubTrees(tree *parse.Tree) {
subFiles := box.ListFiles(tree.Path)
totals := len(subFiles) + 3
showProgress := 64 < totals
for i, subFile := range subFiles {
if !strings.HasSuffix(subFile.path, ".sy") {
continue
}
subTree, err := LoadTree(box.ID, subFile.path) // LoadTree 会重新构造 HPath
if nil != err {
continue
}
sql.UpsertTreeQueue(subTree)
if showProgress {
msg := fmt.Sprintf(Conf.Language(107), subTree.HPath)
util.PushProgress(util.PushProgressCodeProgressed, i, totals, msg)
}
}
if showProgress {
util.ClearPushProgress(totals)
}
}
func moveTree(tree *parse.Tree) {
treenode.SetBlockTreePath(tree)
sql.UpsertTreeQueue(tree)
box := Conf.Box(tree.Box)
subFiles := box.ListFiles(tree.Path)
totals := len(subFiles) + 5
showProgress := 64 < totals
for i, subFile := range subFiles {
if !strings.HasSuffix(subFile.path, ".sy") {
continue
}
subTree, err := LoadTree(box.ID, subFile.path)
if nil != err {
continue
}
treenode.SetBlockTreePath(subTree)
sql.UpsertTreeQueue(subTree)
if showProgress {
msg := fmt.Sprintf(Conf.Language(107), subTree.HPath)
util.PushProgress(util.PushProgressCodeProgressed, i, totals, msg)
}
}
if showProgress {
util.ClearPushProgress(totals)
}
}
func parseKTree(kramdown []byte) (ret *parse.Tree) {
luteEngine := NewLute()
ret = parse.Parse("", kramdown, luteEngine.ParseOptions)
ast.Walk(ret.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if treenode.IsEmptyBlockIAL(n) {
// 空段落保留
p := &ast.Node{Type: ast.NodeParagraph}
p.KramdownIAL = parse.Tokens2IAL(n.Tokens)
p.ID = p.IALAttr("id")
n.InsertBefore(p)
return ast.WalkContinue
}
id := n.IALAttr("id")
if "" == id {
n.SetIALAttr("id", n.ID)
}
if "" == n.IALAttr("id") && (ast.NodeParagraph == n.Type || ast.NodeList == n.Type || ast.NodeListItem == n.Type || ast.NodeBlockquote == n.Type ||
ast.NodeMathBlock == n.Type || ast.NodeCodeBlock == n.Type || ast.NodeHeading == n.Type || ast.NodeTable == n.Type || ast.NodeThematicBreak == n.Type ||
ast.NodeYamlFrontMatter == n.Type || ast.NodeBlockQueryEmbed == n.Type || ast.NodeSuperBlock == n.Type ||
ast.NodeHTMLBlock == n.Type || ast.NodeIFrame == n.Type || ast.NodeWidget == n.Type || ast.NodeAudio == n.Type || ast.NodeVideo == n.Type) {
n.ID = ast.NewNodeID()
n.KramdownIAL = [][]string{{"id", n.ID}}
n.InsertAfter(&ast.Node{Type: ast.NodeKramdownBlockIAL, Tokens: []byte("{: id=\"" + n.ID + "\"}")})
n.SetIALAttr("updated", util.TimeFromID(n.ID))
}
if "" == n.ID && 0 < len(n.KramdownIAL) && ast.NodeDocument != n.Type {
n.ID = n.IALAttr("id")
}
return ast.WalkContinue
})
ret.Root.KramdownIAL = parse.Tokens2IAL(ret.Root.LastChild.Tokens)
return
}
func RefreshFileTree() {
WaitForWritingFiles()
syncLock.Lock()
defer syncLock.Unlock()
refreshFileTree()
}
func refreshFileTree() {
if err := sql.InitDatabase(true); nil != err {
util.PushErrMsg(Conf.Language(85), 5000)
return
}
util.PushEndlessProgress(Conf.Language(35))
openedBoxes := Conf.GetOpenedBoxes()
for _, openedBox := range openedBoxes {
openedBox.Index(true)
}
IndexRefs()
// 缓存根一级的文档树展开
for _, openedBox := range openedBoxes {
ListDocTree(openedBox.ID, "/", Conf.FileTree.Sort)
}
treenode.SaveBlockTree()
util.PushEndlessProgress(Conf.Language(58))
go func() {
time.Sleep(1 * time.Second)
util.ReloadUI()
}()
}
func ChangeBoxSort(boxIDs []string) {
for i, boxID := range boxIDs {
box := &Box{ID: boxID}
boxConf := box.GetConf()
boxConf.Sort = i + 1
box.SaveConf(boxConf)
}
}
func SetBoxIcon(boxID, icon string) {
box := &Box{ID: boxID}
boxConf := box.GetConf()
boxConf.Icon = icon
box.SaveConf(boxConf)
}
func (box *Box) UpdateHistoryGenerated() {
boxLatestHistoryTime[box.ID] = time.Now()
}
func LockFileByBlockID(id string) (locked bool, filePath string) {
bt := treenode.GetBlockTree(id)
if nil == bt {
return
}
p := filepath.Join(util.DataDir, bt.BoxID, bt.Path)
if !gulu.File.IsExist(p) {
return true, ""
}
return nil == filesys.LockFile(p), p
}

561
kernel/model/conf.go Normal file
View file

@ -0,0 +1,561 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"crypto/sha256"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/88250/gulu"
"github.com/88250/lute"
humanize "github.com/dustin/go-humanize"
"github.com/getsentry/sentry-go"
"github.com/siyuan-note/siyuan/kernel/conf"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
var Conf *AppConf
// AppConf 维护应用元数据,保存在 ~/.siyuan/conf.json。
type AppConf struct {
LogLevel string `json:"logLevel"` // 日志级别Off, Trace, Debug, Info, Warn, Error, Fatal
Appearance *conf.Appearance `json:"appearance"` // 外观
Langs []*conf.Lang `json:"langs"` // 界面语言列表
Lang string `json:"lang"` // 选择的界面语言,同 Appearance.Lang
FileTree *conf.FileTree `json:"fileTree"` // 文档面板
Tag *conf.Tag `json:"tag"` // 标签面板
Editor *conf.Editor `json:"editor"` // 编辑器配置
Export *conf.Export `json:"export"` // 导出配置
Graph *conf.Graph `json:"graph"` // 关系图配置
UILayout *conf.UILayout `json:"uiLayout"` // 界面布局
UserData string `json:"userData"` // 社区用户信息,对 User 加密存储
User *conf.User `json:"-"` // 社区用户内存结构,不持久化
Account *conf.Account `json:"account"` // 帐号配置
ReadOnly bool `json:"readonly"` // 是否是只读
LocalIPs []string `json:"localIPs"` // 本地 IP 列表
AccessAuthCode string `json:"accessAuthCode"` // 访问授权码
E2EEPasswd string `json:"e2eePasswd"` // 端到端加密密码,用于备份和同步
E2EEPasswdMode int `json:"e2eePasswdMode"` // 端到端加密密码生成方式0自动1自定义
System *conf.System `json:"system"` // 系统
Keymap *conf.Keymap `json:"keymap"` // 快捷键
Backup *conf.Backup `json:"backup"` // 备份配置
Sync *conf.Sync `json:"sync"` // 同步配置
Search *conf.Search `json:"search"` // 搜索配置
Stat *conf.Stat `json:"stat"` // 统计
Api *conf.API `json:"api"` // API
Newbie bool `json:"newbie"` // 是否是安装后第一次启动
}
func InitConf() {
initLang()
windowStateConf := filepath.Join(util.ConfDir, "windowState.json")
if !gulu.File.IsExist(windowStateConf) {
if err := gulu.File.WriteFileSafer(windowStateConf, []byte("{}"), 0644); nil != err {
util.LogErrorf("create [windowState.json] failed: %s", err)
}
}
Conf = &AppConf{LogLevel: "debug", Lang: util.Lang}
confPath := filepath.Join(util.ConfDir, "conf.json")
if gulu.File.IsExist(confPath) {
data, err := os.ReadFile(confPath)
if nil != err {
util.LogErrorf("load conf [%s] failed: %s", confPath, err)
}
err = gulu.JSON.UnmarshalJSON(data, Conf)
if err != nil {
util.LogErrorf("parse conf [%s] failed: %s", confPath, err)
}
}
Conf.Langs = loadLangs()
if nil == Conf.Appearance {
Conf.Appearance = conf.NewAppearance()
}
var langOK bool
for _, l := range Conf.Langs {
if Conf.Lang == l.Name {
langOK = true
break
}
}
if !langOK {
Conf.Lang = "en_US"
}
Conf.Appearance.Lang = Conf.Lang
if nil == Conf.UILayout {
Conf.UILayout = &conf.UILayout{}
}
if nil == Conf.Keymap {
Conf.Keymap = &conf.Keymap{}
}
if "" == Conf.Appearance.CodeBlockThemeDark {
Conf.Appearance.CodeBlockThemeDark = "dracula"
}
if "" == Conf.Appearance.CodeBlockThemeLight {
Conf.Appearance.CodeBlockThemeLight = "github"
}
if nil == Conf.FileTree {
Conf.FileTree = conf.NewFileTree()
}
if 1 > Conf.FileTree.MaxListCount {
Conf.FileTree.MaxListCount = 512
}
if nil == Conf.Tag {
Conf.Tag = conf.NewTag()
}
if nil == Conf.Editor {
Conf.Editor = conf.NewEditor()
}
if 1 > len(Conf.Editor.Emoji) {
Conf.Editor.Emoji = []string{}
}
if 1 > Conf.Editor.BlockRefDynamicAnchorTextMaxLen {
Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 64
}
if 5120 < Conf.Editor.BlockRefDynamicAnchorTextMaxLen {
Conf.Editor.BlockRefDynamicAnchorTextMaxLen = 5120
}
if nil == Conf.Export {
Conf.Export = conf.NewExport()
}
if 0 == Conf.Export.BlockRefMode || 1 == Conf.Export.BlockRefMode {
// 废弃导出选项引用块转换为原始块和引述块 https://github.com/siyuan-note/siyuan/issues/3155
Conf.Export.BlockRefMode = 4 // 改为脚注
}
if 9 > Conf.Editor.FontSize || 72 < Conf.Editor.FontSize {
Conf.Editor.FontSize = 16
}
if "" == Conf.Editor.PlantUMLServePath {
Conf.Editor.PlantUMLServePath = "https://www.plantuml.com/plantuml/svg/~1"
}
if nil == Conf.Graph || nil == Conf.Graph.Local || nil == Conf.Graph.Global {
Conf.Graph = conf.NewGraph()
}
if nil == Conf.System {
Conf.System = conf.NewSystem()
} else {
Conf.System.KernelVersion = util.Ver
Conf.System.IsInsider = util.IsInsider
}
if nil == Conf.System.NetworkProxy {
Conf.System.NetworkProxy = &conf.NetworkProxy{}
}
if "" != Conf.System.NetworkProxy.Scheme {
util.LogInfof("using network proxy [%s]", Conf.System.NetworkProxy.String())
}
if "" == Conf.System.ID {
Conf.System.ID = util.GetDeviceID()
}
if "std" == util.Container {
Conf.System.ID = util.GetDeviceID()
}
Conf.System.AppDir = util.WorkingDir
Conf.System.ConfDir = util.ConfDir
Conf.System.HomeDir = util.HomeDir
Conf.System.WorkspaceDir = util.WorkspaceDir
Conf.System.DataDir = util.DataDir
Conf.System.Container = util.Container
util.UserAgent = util.UserAgent + " " + util.Container
Conf.System.OS = runtime.GOOS
Conf.Newbie = util.IsNewbie
if "" != Conf.UserData {
Conf.User = loadUserFromConf()
}
if nil == Conf.Account {
Conf.Account = conf.NewAccount()
}
if nil == Conf.Backup {
Conf.Backup = conf.NewBackup()
}
if !gulu.File.IsExist(Conf.Backup.GetSaveDir()) {
if err := os.MkdirAll(Conf.Backup.GetSaveDir(), 0755); nil != err {
util.LogErrorf("create backup dir [%s] failed: %s", Conf.Backup.GetSaveDir(), err)
}
}
if nil == Conf.Sync {
Conf.Sync = conf.NewSync()
}
if !gulu.File.IsExist(Conf.Sync.GetSaveDir()) {
if err := os.MkdirAll(Conf.Sync.GetSaveDir(), 0755); nil != err {
util.LogErrorf("create sync dir [%s] failed: %s", Conf.Sync.GetSaveDir(), err)
}
}
if nil == Conf.Api {
Conf.Api = conf.NewAPI()
}
if 1440 < Conf.Editor.GenerateHistoryInterval {
Conf.Editor.GenerateHistoryInterval = 1440
}
if 1 > Conf.Editor.HistoryRetentionDays {
Conf.Editor.HistoryRetentionDays = 7
}
if nil == Conf.Search {
Conf.Search = conf.NewSearch()
}
if nil == Conf.Stat {
Conf.Stat = conf.NewStat()
}
Conf.ReadOnly = util.ReadOnly
if "" != util.AccessAuthCode {
Conf.AccessAuthCode = util.AccessAuthCode
}
Conf.E2EEPasswdMode = 0
if !isBuiltInE2EEPasswd() {
Conf.E2EEPasswdMode = 1
}
Conf.LocalIPs = util.GetLocalIPs()
Conf.Save()
util.SetLogLevel(Conf.LogLevel)
if Conf.System.UploadErrLog {
util.LogInfof("user has enabled [Automatically upload error messages and diagnostic data]")
sentry.Init(sentry.ClientOptions{
Dsn: "https://bdff135f14654ae58a054adeceb2c308@o1173696.ingest.sentry.io/6269178",
Release: util.Ver,
Environment: util.Mode,
})
}
}
var langs = map[string]map[int]string{}
var timeLangs = map[string]map[string]interface{}{}
func initLang() {
p := filepath.Join(util.WorkingDir, "appearance", "langs")
dir, err := os.Open(p)
if nil != err {
util.LogFatalf("open language configuration folder [%s] failed: %s", p, err)
}
defer dir.Close()
langNames, err := dir.Readdirnames(-1)
if nil != err {
util.LogFatalf("list language configuration folder [%s] failed: %s", p, err)
}
for _, langName := range langNames {
jsonPath := filepath.Join(p, langName)
data, err := os.ReadFile(jsonPath)
if nil != err {
util.LogErrorf("read language configuration [%s] failed: %s", jsonPath, err)
continue
}
langMap := map[string]interface{}{}
if err := gulu.JSON.UnmarshalJSON(data, &langMap); nil != err {
util.LogErrorf("parse language configuration failed [%s] failed: %s", jsonPath, err)
continue
}
kernelMap := map[int]string{}
label := langMap["_label"].(string)
kernelLangs := langMap["_kernel"].(map[string]interface{})
for k, v := range kernelLangs {
num, err := strconv.Atoi(k)
if nil != err {
util.LogErrorf("parse language configuration [%s] item [%d] failed [%s] failed: %s", p, num, err)
continue
}
kernelMap[num] = v.(string)
}
kernelMap[-1] = label
name := langName[:strings.LastIndex(langName, ".")]
langs[name] = kernelMap
timeLangs[name] = langMap["_time"].(map[string]interface{})
}
}
func loadLangs() (ret []*conf.Lang) {
for name, langMap := range langs {
lang := &conf.Lang{Label: langMap[-1], Name: name}
ret = append(ret, lang)
}
sort.Slice(ret, func(i, j int) bool {
return ret[i].Name < ret[j].Name
})
return
}
var exitLock = sync.Mutex{}
func Close(force bool) (err error) {
exitLock.Lock()
defer exitLock.Unlock()
treenode.CloseBlockTree()
util.PushMsg(Conf.Language(95), 10000*60)
WaitForWritingFiles()
if !force {
SyncData(false, true, false)
if 0 != ExitSyncSucc {
err = errors.New(Conf.Language(96))
return
}
}
//util.UIProcessIDs.Range(func(key, _ interface{}) bool {
// pid := key.(string)
// util.Kill(pid)
// return true
//})
Conf.Close()
sql.CloseDatabase()
util.WebSocketServer.Close()
clearWorkspaceTemp()
util.LogInfof("exited kernel")
go func() {
time.Sleep(500 * time.Millisecond)
os.Exit(util.ExitCodeOk)
}()
return
}
var CustomEmojis = sync.Map{}
func NewLute() (ret *lute.Lute) {
ret = util.NewLute()
ret.SetCodeSyntaxHighlightLineNum(Conf.Editor.CodeSyntaxHighlightLineNum)
ret.SetChineseParagraphBeginningSpace(Conf.Export.ParagraphBeginningSpace)
ret.SetProtyleMarkNetImg(Conf.Editor.DisplayNetImgMark)
customEmojiMap := map[string]string{}
CustomEmojis.Range(func(key, value interface{}) bool {
customEmojiMap[key.(string)] = value.(string)
return true
})
ret.PutEmojis(customEmojiMap)
return
}
var confSaveLock = sync.Mutex{}
func (conf *AppConf) Save() {
confSaveLock.Lock()
confSaveLock.Unlock()
newData, _ := gulu.JSON.MarshalIndentJSON(Conf, "", " ")
confPath := filepath.Join(util.ConfDir, "conf.json")
oldData, err := filesys.NoLockFileRead(confPath)
if nil != err {
conf.save0(newData)
return
}
if bytes.Equal(newData, oldData) {
return
}
conf.save0(newData)
}
func (conf *AppConf) save0(data []byte) {
confPath := filepath.Join(util.ConfDir, "conf.json")
if err := filesys.LockFileWrite(confPath, data); nil != err {
util.LogFatalf("write conf [%s] failed: %s", confPath, err)
}
}
func (conf *AppConf) Close() {
conf.Save()
}
func (conf *AppConf) Box(boxID string) *Box {
for _, box := range conf.GetOpenedBoxes() {
if box.ID == boxID {
return box
}
}
return nil
}
func (conf *AppConf) GetBoxes() (ret []*Box) {
ret = []*Box{}
notebooks, err := ListNotebooks()
if nil != err {
return
}
for _, notebook := range notebooks {
id := notebook.ID
name := notebook.Name
closed := notebook.Closed
box := &Box{ID: id, Name: name, Closed: closed}
ret = append(ret, box)
}
return
}
func (conf *AppConf) GetOpenedBoxes() (ret []*Box) {
ret = []*Box{}
notebooks, err := ListNotebooks()
if nil != err {
return
}
for _, notebook := range notebooks {
if !notebook.Closed {
ret = append(ret, notebook)
}
}
return
}
func (conf *AppConf) GetClosedBoxes() (ret []*Box) {
ret = []*Box{}
notebooks, err := ListNotebooks()
if nil != err {
return
}
for _, notebook := range notebooks {
if notebook.Closed {
ret = append(ret, notebook)
}
}
return
}
func (conf *AppConf) Language(num int) string {
return langs[conf.Lang][num]
}
func InitBoxes() {
initialized := false
blockCount := 0
if 1 > len(treenode.GetBlockTrees()) {
if gulu.File.IsExist(util.BlockTreePath) {
util.IncBootProgress(30, "Reading block trees...")
go func() {
for i := 0; i < 40; i++ {
util.RandomSleep(100, 200)
util.IncBootProgress(1, "Reading block trees...")
}
}()
if err := treenode.ReadBlockTree(); nil == err {
initialized = true
} else {
if err = os.RemoveAll(util.BlockTreePath); nil != err {
util.LogErrorf("remove block tree [%s] failed: %s", util.BlockTreePath, err)
}
}
}
} else { // 大于 1 的话说明在同步阶段已经加载过了
initialized = true
}
for _, box := range Conf.GetOpenedBoxes() {
box.UpdateHistoryGenerated() // 初始化历史生成时间为当前时间
if !initialized {
box.BootIndex()
}
ListDocTree(box.ID, "/", Conf.FileTree.Sort) // 缓存根一级的文档树展开
}
if !initialized {
treenode.SaveBlockTree()
}
blocktrees := treenode.GetBlockTrees()
blockCount = len(blocktrees)
var dbSize string
if dbFile, err := os.Stat(util.DBPath); nil == err {
dbSize = humanize.Bytes(uint64(dbFile.Size()))
}
util.LogInfof("database size [%s], block count [%d]", dbSize, blockCount)
}
func IsSubscriber() bool {
return nil != Conf.User && (-1 == Conf.User.UserSiYuanProExpireTime || 0 < Conf.User.UserSiYuanProExpireTime) && 0 == Conf.User.UserSiYuanSubscriptionStatus
}
func isBuiltInE2EEPasswd() bool {
if nil == Conf || nil == Conf.User || "" == Conf.E2EEPasswd {
return true
}
pwd := GetBuiltInE2EEPasswd()
return Conf.E2EEPasswd == util.AESEncrypt(pwd)
}
func GetBuiltInE2EEPasswd() (ret string) {
part1 := Conf.User.UserId[:7]
part2 := Conf.User.UserId[7:]
ret = part2 + part1
ret = fmt.Sprintf("%x", sha256.Sum256([]byte(ret)))[:7]
return
}
func clearWorkspaceTemp() {
os.RemoveAll(filepath.Join(util.TempDir, "bazaar"))
os.RemoveAll(filepath.Join(util.TempDir, "export"))
os.RemoveAll(filepath.Join(util.TempDir, "import"))
tmps, err := filepath.Glob(filepath.Join(util.TempDir, "*.tmp"))
if nil != err {
util.LogErrorf("glob temp files failed: %s", err)
}
for _, tmp := range tmps {
if err = os.RemoveAll(tmp); nil != err {
util.LogErrorf("remove temp file [%s] failed: %s", tmp, err)
} else {
util.LogInfof("removed temp file [%s]", tmp)
}
}
tmps, err = filepath.Glob(filepath.Join(util.DataDir, ".siyuan", "*.tmp"))
if nil != err {
util.LogErrorf("glob temp files failed: %s", err)
}
for _, tmp := range tmps {
if err = os.RemoveAll(tmp); nil != err {
util.LogErrorf("remove temp file [%s] failed: %s", tmp, err)
} else {
util.LogInfof("removed temp file [%s]", tmp)
}
}
}

303
kernel/model/css.go Normal file
View file

@ -0,0 +1,303 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/88250/css"
"github.com/88250/gulu"
"github.com/siyuan-note/siyuan/kernel/util"
)
var colorKeys = map[string][]string{
"colorPrimary": colorPrimary,
"colorFont": colorFont,
"colorBorder": colorBorder,
"colorScroll": colorScroll,
"colorTab": colorTab,
"colorTip": colorTip,
"colorGraph": colorGraph,
"colorInline": colorInline,
}
var colorPrimary = []string{
"--b3-theme-primary",
"--b3-theme-primary-light",
"--b3-theme-primary-lighter",
"--b3-theme-primary-lightest",
"--b3-theme-secondary",
"--b3-theme-background",
"--b3-theme-surface",
"--b3-theme-error",
}
var colorFont = []string{
"--b3-theme-on-primary",
"--b3-theme-on-secondary",
"--b3-theme-on-background",
"--b3-theme-on-surface",
"--b3-theme-on-error",
}
var colorBorder = []string{
"--b3-border-color",
}
var colorScroll = []string{
"--b3-scroll-color",
}
var colorTab = []string{
"--b3-tab-background",
}
var colorTip = []string{
"--b3-tooltips-color",
}
var colorGraph = []string{
"--b3-graph-line",
"--b3-graph-hl-point",
"--b3-graph-hl-line",
"--b3-graph-p-point",
"--b3-graph-heading-point",
"--b3-graph-math-point",
"--b3-graph-code-point",
"--b3-graph-table-point",
"--b3-graph-list-point",
"--b3-graph-todo-point",
"--b3-graph-olist-point",
"--b3-graph-listitem-point",
"--b3-graph-bq-point",
"--b3-graph-super-point",
"--b3-graph-doc-point",
"--b3-graph-tag-point",
"--b3-graph-asset-point",
"--b3-graph-line",
"--b3-graph-tag-line",
"--b3-graph-ref-line",
"--b3-graph-tag-tag-line",
"--b3-graph-asset-line",
"--b3-graph-hl-point",
"--b3-graph-hl-line",
}
var colorInline = []string{
"--b3-protyle-inline-strong-color",
"--b3-protyle-inline-em-color",
"--b3-protyle-inline-s-color",
"--b3-protyle-inline-link-color",
"--b3-protyle-inline-tag-color",
"--b3-protyle-inline-blockref-color",
"--b3-protyle-inline-mark-background",
"--b3-protyle-inline-mark-color",
}
func currentCSSValue(key string) string {
var themeName string
if 0 == Conf.Appearance.Mode {
themeName = Conf.Appearance.ThemeLight
} else {
themeName = Conf.Appearance.ThemeDark
}
themePath := filepath.Join(util.ThemesPath, themeName)
theme := filepath.Join(themePath, "theme.css")
custom := filepath.Join(themePath, "custom.css")
var data []byte
var err error
if Conf.Appearance.CustomCSS {
data, _ = os.ReadFile(custom)
}
if 1 > len(data) {
data, err = os.ReadFile(theme)
if nil != err {
util.LogErrorf("read theme css [%s] failed: %s", theme, err)
return "#ffffff"
}
}
ss := css.Parse(string(data))
rules := ss.GetCSSRuleList()
for _, rule := range rules {
for _, style := range rule.Style.Styles {
fixStyle(style)
if key == style.Property {
return style.Value.Text()
}
}
}
return ""
}
func ReadCustomCSS(themeName string) (ret map[string]map[string]string, err error) {
ret = map[string]map[string]string{}
themePath := filepath.Join(util.ThemesPath, themeName)
theme := filepath.Join(themePath, "theme.css")
custom := filepath.Join(themePath, "custom.css")
if !gulu.File.IsExist(custom) {
if err = gulu.File.CopyFile(theme, custom); nil != err {
util.LogErrorf("copy theme [%s] to [%s] failed: %s", theme, custom, err)
return
}
}
data, err := os.ReadFile(custom)
if nil != err {
util.LogErrorf("read custom css [%s] failed: %s", custom, err)
return
}
fullColorMap := map[string]string{}
ss := css.Parse(string(data))
rules := ss.GetCSSRuleList()
for _, rule := range rules {
for _, style := range rule.Style.Styles {
fixStyle(style)
fullColorMap[style.Property] = style.Value.Text()
}
}
// 补充现有主题中的样式
data, err = os.ReadFile(theme)
if nil != err {
util.LogErrorf("read theme css [%s] failed: %s", theme, err)
return
}
ss = css.Parse(string(data))
rules = ss.GetCSSRuleList()
for _, rule := range rules {
for _, style := range rule.Style.Styles {
fixStyle(style)
if _, ok := fullColorMap[style.Property]; !ok {
fullColorMap[style.Property] = style.Value.ParsedText()
}
}
}
buildColor(&ret, fullColorMap, "colorPrimary")
buildColor(&ret, fullColorMap, "colorFont")
buildColor(&ret, fullColorMap, "colorBorder")
buildColor(&ret, fullColorMap, "colorScroll")
buildColor(&ret, fullColorMap, "colorTab")
buildColor(&ret, fullColorMap, "colorTip")
buildColor(&ret, fullColorMap, "colorGraph")
buildColor(&ret, fullColorMap, "colorInline")
return
}
func buildColor(ret *map[string]map[string]string, fullColorMap map[string]string, colorMapKey string) {
colorMap := map[string]string{}
for _, colorKey := range colorKeys[colorMapKey] {
colorMap[colorKey] = fullColorMap[colorKey]
}
(*ret)[colorMapKey] = colorMap
}
func WriteCustomCSS(themeName string, cssMap map[string]interface{}) (err error) {
customCSS := map[string]string{}
for _, vMap := range cssMap {
cssKV := vMap.(map[string]interface{})
for k, v := range cssKV {
customCSS[k] = v.(string)
}
}
themePath := filepath.Join(util.ThemesPath, themeName)
custom := filepath.Join(themePath, "custom.css")
data, err := os.ReadFile(custom)
if nil != err {
util.LogErrorf("read custom css [%s] failed: %s", custom, err)
return
}
cssData := util.RemoveInvisible(string(data))
customStyleSheet := css.Parse(cssData)
buf := &bytes.Buffer{}
customRules := customStyleSheet.CssRuleList
for _, customRule := range customRules {
if css.KEYFRAMES_RULE == customRule.Type {
keyframes(customRule, buf)
continue
} else if css.STYLE_RULE != customRule.Type {
buf.WriteString(customRule.Type.Text())
buf.WriteString(customRule.Style.Text())
buf.WriteString("\n\n")
continue
}
for _, style := range customRule.Style.Styles {
fixStyle(style)
if val, ok := customCSS[style.Property]; ok {
style.Value = css.NewCSSValue(val)
delete(customCSS, style.Property)
}
}
for k, v := range customCSS {
customRule.Style.Styles = append(customRule.Style.Styles, &css.CSSStyleDeclaration{Property: k, Value: css.NewCSSValue(v)})
}
buf.WriteString(customRule.Style.Text())
buf.WriteString("\n\n")
}
if err := gulu.File.WriteFileSafer(custom, buf.Bytes(), 0644); nil != err {
util.LogErrorf("write custom css [%s] failed: %s", custom, err)
}
util.BroadcastByType("main", "refreshtheme", 0, "", map[string]interface{}{
"theme": "/appearance/themes/" + themeName + "/custom.css?" + fmt.Sprintf("%d", time.Now().Unix()),
})
return
}
func keyframes(rule *css.CSSRule, buf *bytes.Buffer) {
buf.WriteString(rule.Type.Text())
buf.WriteString(" ")
buf.WriteString(rule.Style.Selector.Text())
buf.WriteString(" {\n")
for _, r := range rule.Rules {
buf.WriteString(r.Style.Text())
buf.WriteString("\n")
}
buf.WriteString("\n\n")
}
func fixStyle(style *css.CSSStyleDeclaration) {
// css 解析库似乎有 bug这里做修正
if strings.HasPrefix(style.Property, "-") && !strings.HasPrefix(style.Property, "--") {
style.Property = "-" + style.Property
}
if strings.HasPrefix(style.Value.Text(), "- ") {
value := style.Value.Text()[2:]
style.Value = css.NewCSSValue(value)
}
}

1387
kernel/model/export.go Normal file

File diff suppressed because it is too large Load diff

1626
kernel/model/file.go Normal file

File diff suppressed because it is too large Load diff

115
kernel/model/format.go Normal file
View file

@ -0,0 +1,115 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"os"
"path/filepath"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/88250/lute/render"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/util"
)
func AutoSpace(rootID string) (err error) {
tree, err := loadTreeByBlockID(rootID)
if nil != err {
return
}
util.PushEndlessProgress(Conf.Language(116))
defer util.ClearPushProgress(100)
generateFormatHistory(tree)
var blocks []*ast.Node
var rootIAL [][]string
// 添加 block ial后面格式化渲染需要
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || !n.IsBlock() {
return ast.WalkContinue
}
if ast.NodeDocument == n.Type {
rootIAL = n.KramdownIAL
return ast.WalkContinue
}
if ast.NodeBlockQueryEmbed == n.Type {
if script := n.ChildByType(ast.NodeBlockQueryEmbedScript); nil != script {
script.Tokens = bytes.ReplaceAll(script.Tokens, []byte("\n"), []byte(" "))
}
}
if 0 < len(n.KramdownIAL) {
blocks = append(blocks, n)
}
return ast.WalkContinue
})
for _, block := range blocks {
block.InsertAfter(&ast.Node{Type: ast.NodeKramdownBlockIAL, Tokens: parse.IAL2Tokens(block.KramdownIAL)})
}
luteEngine := NewLute()
luteEngine.SetAutoSpace(true)
formatRenderer := render.NewFormatRenderer(tree, luteEngine.RenderOptions)
md := formatRenderer.Render()
newTree := parseKTree(md)
newTree.Root.ID = tree.ID
newTree.Root.KramdownIAL = rootIAL
newTree.ID = tree.ID
newTree.Path = tree.Path
newTree.HPath = tree.HPath
newTree.Box = tree.Box
err = writeJSONQueue(newTree)
if nil != err {
return
}
sql.WaitForWritingDatabase()
return
}
func generateFormatHistory(tree *parse.Tree) {
historyDir, err := util.GetHistoryDir("format")
if nil != err {
util.LogErrorf("get history dir failed: %s", err)
return
}
historyPath := filepath.Join(historyDir, tree.Box, tree.Path)
if err = os.MkdirAll(filepath.Dir(historyPath), 0755); nil != err {
util.LogErrorf("generate history failed: %s", err)
return
}
var data []byte
if data, err = filesys.NoLockFileRead(filepath.Join(util.DataDir, tree.Box, tree.Path)); err != nil {
util.LogErrorf("generate history failed: %s", err)
return
}
if err = gulu.File.WriteFileSafer(historyPath, data, 0644); err != nil {
util.LogErrorf("generate history failed: %s", err)
return
}
}

677
kernel/model/graph.go Normal file
View file

@ -0,0 +1,677 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"math"
"strings"
"unicode/utf8"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/html"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
type GraphNode struct {
ID string `json:"id"`
Box string `json:"box"`
Path string `json:"path"`
Size float64 `json:"size"`
Title string `json:"title,omitempty"`
Label string `json:"label"`
Type string `json:"type"`
Refs int `json:"refs"`
Defs int `json:"defs"`
Color *GraphNodeColor `json:"color"`
}
type GraphNodeColor struct {
Background string `json:"background"`
}
type GraphLink struct {
From string `json:"from"`
To string `json:"to"`
Ref bool `json:"-"`
Color *GraphLinkColor `json:"color"`
Arrows *GraphArrows `json:"arrows"`
}
type GraphLinkColor struct {
Color string `json:"color"`
}
type GraphArrows struct {
To *GraphArrowsTo `json:"to"`
}
type GraphArrowsTo struct {
Enabled bool `json:"enabled"`
}
func BuildTreeGraph(id, query string) (boxID string, nodes []*GraphNode, links []*GraphLink) {
nodes = []*GraphNode{}
links = []*GraphLink{}
tree, err := loadTreeByBlockID(id)
if nil != err {
return
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
return
}
sqlBlock := sql.BuildBlockFromNode(node, tree)
boxID = sqlBlock.Box
block := fromSQLBlock(sqlBlock, "", 0)
stmt := query2Stmt(query)
stmt += graphTypeFilter(true)
stmt += graphDailyNoteFilter(true)
stmt = strings.ReplaceAll(stmt, "content", "ref.content")
forwardlinks, backlinks := buildFullLinks(stmt)
var sqlBlocks []*sql.Block
var rootID string
if "NodeDocument" == block.Type {
sqlBlocks = sql.GetAllChildBlocks(block.ID, stmt)
rootID = block.ID
} else {
sqlBlocks = sql.GetChildBlocks(block.ID, stmt)
}
blocks := fromSQLBlocks(&sqlBlocks, "", 0)
if "" != rootID {
// 局部关系图中添加文档链接关系 https://github.com/siyuan-note/siyuan/issues/4996
rootBlock := getBlockIn(blocks, rootID)
if nil != rootBlock {
// 按引用处理
sqlRootDefs := sql.QueryDefRootBlocksByRefRootID(rootID)
for _, sqlRootDef := range sqlRootDefs {
rootDef := fromSQLBlock(sqlRootDef, "", 0)
blocks = append(blocks, rootDef)
sqlRootRefs := sql.QueryRefRootBlocksByDefRootID(sqlRootDef.ID)
rootRefs := fromSQLBlocks(&sqlRootRefs, "", 0)
rootDef.Refs = append(rootDef.Refs, rootRefs...)
}
// 按定义处理
sqlRootRefs := sql.QueryRefRootBlocksByDefRootID(rootID)
for _, sqlRootRef := range sqlRootRefs {
rootRef := fromSQLBlock(sqlRootRef, "", 0)
blocks = append(blocks, rootRef)
rootBlock.Refs = append(rootBlock.Refs, rootRef)
}
}
}
style := graphStyle(true)
genTreeNodes(blocks, &nodes, &links, true, style)
growTreeGraph(&forwardlinks, &backlinks, &nodes)
blocks = append(blocks, forwardlinks...)
blocks = append(blocks, backlinks...)
buildLinks(&blocks, &links, style, true)
if Conf.Graph.Local.Tag {
p := sqlBlock.Path
linkTagBlocks(&blocks, &nodes, &links, p, style)
}
markLinkedNodes(&nodes, &links, true)
nodes = removeDuplicatedUnescape(nodes)
return
}
func BuildGraph(query string) (boxID string, nodes []*GraphNode, links []*GraphLink) {
nodes = []*GraphNode{}
links = []*GraphLink{}
stmt := query2Stmt(query)
stmt = strings.TrimPrefix(stmt, "select * from blocks where")
stmt += graphTypeFilter(false)
stmt += graphDailyNoteFilter(false)
stmt = strings.ReplaceAll(stmt, "content", "ref.content")
forwardlinks, backlinks := buildFullLinks(stmt)
var blocks []*Block
roots := sql.GetAllRootBlocks()
style := graphStyle(false)
if 0 < len(roots) {
boxID = roots[0].Box
}
for _, root := range roots {
sqlBlocks := sql.GetAllChildBlocks(root.ID, stmt)
treeBlocks := fromSQLBlocks(&sqlBlocks, "", 0)
genTreeNodes(treeBlocks, &nodes, &links, false, style)
blocks = append(blocks, treeBlocks...)
// 文档块关联
rootBlock := getBlockIn(treeBlocks, root.ID)
if nil == rootBlock {
//util.LogWarnf("root block is nil [rootID=%s], tree blocks [len=%d], just skip it", root.ID, len(treeBlocks))
continue
}
sqlRootRefs := sql.QueryRefRootBlocksByDefRootID(root.ID)
rootRefs := fromSQLBlocks(&sqlRootRefs, "", 0)
rootBlock.Refs = append(rootBlock.Refs, rootRefs...)
}
growTreeGraph(&forwardlinks, &backlinks, &nodes)
blocks = append(blocks, forwardlinks...)
blocks = append(blocks, backlinks...)
buildLinks(&blocks, &links, style, false)
if Conf.Graph.Global.Tag {
linkTagBlocks(&blocks, &nodes, &links, "", style)
}
markLinkedNodes(&nodes, &links, false)
pruneUnref(&nodes, &links)
nodes = removeDuplicatedUnescape(nodes)
return
}
func linkTagBlocks(blocks *[]*Block, nodes *[]*GraphNode, links *[]*GraphLink, p string, style map[string]string) {
tagSpans := sql.QueryTagSpans(p, 1024)
if 1 > len(tagSpans) {
return
}
nodeSize := Conf.Graph.Local.NodeSize
if "" != p {
nodeSize = Conf.Graph.Global.NodeSize
}
// 构造标签节点
var tagNodes []*GraphNode
for _, tagSpan := range tagSpans {
if nil == tagNodeIn(tagNodes, tagSpan.Content) {
node := &GraphNode{
ID: tagSpan.Content,
Label: tagSpan.Content,
Size: nodeSize,
Type: tagSpan.Type,
Color: &GraphNodeColor{Background: style["--b3-graph-tag-point"]},
}
*nodes = append(*nodes, node)
tagNodes = append(tagNodes, node)
}
}
// 连接标签和块
for _, block := range *blocks {
for _, tagSpan := range tagSpans {
if block.ID == tagSpan.BlockID {
*links = append(*links, &GraphLink{
From: tagSpan.Content,
To: block.ID,
Color: &GraphLinkColor{Color: style["--b3-graph-tag-line"]},
})
}
}
}
// 连接层级标签
for _, tagNode := range tagNodes {
ids := strings.Split(tagNode.ID, "/")
if 2 > len(ids) {
continue
}
for _, targetID := range ids[:len(ids)-1] {
if targetTag := tagNodeIn(tagNodes, targetID); nil != targetTag {
*links = append(*links, &GraphLink{
From: tagNode.ID,
To: targetID,
Color: &GraphLinkColor{Color: style["--b3-graph-tag-tag-line"]},
})
}
}
}
}
func tagNodeIn(tagNodes []*GraphNode, content string) *GraphNode {
for _, tagNode := range tagNodes {
if tagNode.Label == content {
return tagNode
}
}
return nil
}
func growTreeGraph(forwardlinks, backlinks *[]*Block, nodes *[]*GraphNode) {
forwardDepth, backDepth := 0, 0
growLinkedNodes(forwardlinks, backlinks, nodes, nodes, &forwardDepth, &backDepth)
}
func growLinkedNodes(forwardlinks, backlinks *[]*Block, nodes, all *[]*GraphNode, forwardDepth, backDepth *int) {
if 1 > len(*nodes) {
return
}
forwardGeneration := &[]*GraphNode{}
if 16 > *forwardDepth {
for _, ref := range *forwardlinks {
for _, node := range *nodes {
if node.ID == ref.ID {
var defs []*Block
for _, refDef := range ref.Defs {
if existNodes(all, refDef.ID) || existNodes(forwardGeneration, refDef.ID) || existNodes(nodes, refDef.ID) {
continue
}
defs = append(defs, refDef)
}
for _, refDef := range defs {
defNode := &GraphNode{
ID: refDef.ID,
Box: refDef.Box,
Path: refDef.Path,
Size: Conf.Graph.Local.NodeSize,
Type: refDef.Type,
}
nodeTitleLabel(defNode, nodeContentByBlock(refDef))
*forwardGeneration = append(*forwardGeneration, defNode)
}
}
}
}
}
backGeneration := &[]*GraphNode{}
if 16 > *backDepth {
for _, def := range *backlinks {
for _, node := range *nodes {
if node.ID == def.ID {
for _, ref := range def.Refs {
if existNodes(all, ref.ID) || existNodes(backGeneration, ref.ID) || existNodes(nodes, ref.ID) {
continue
}
refNode := &GraphNode{
ID: ref.ID,
Box: ref.Box,
Path: ref.Path,
Size: Conf.Graph.Local.NodeSize,
Type: ref.Type,
}
nodeTitleLabel(refNode, nodeContentByBlock(ref))
*backGeneration = append(*backGeneration, refNode)
}
}
}
}
}
generation := &[]*GraphNode{}
*generation = append(*generation, *forwardGeneration...)
*generation = append(*generation, *backGeneration...)
*forwardDepth++
*backDepth++
growLinkedNodes(forwardlinks, backlinks, generation, nodes, forwardDepth, backDepth)
*nodes = append(*nodes, *generation...)
}
func existNodes(nodes *[]*GraphNode, id string) bool {
for _, node := range *nodes {
if node.ID == id {
return true
}
}
return false
}
func buildLinks(defs *[]*Block, links *[]*GraphLink, style map[string]string, local bool) {
for _, def := range *defs {
for _, ref := range def.Refs {
link := &GraphLink{
From: ref.ID,
To: def.ID,
Ref: true,
Color: linkColor(true, style),
}
if local {
if Conf.Graph.Local.Arrow {
link.Arrows = &GraphArrows{To: &GraphArrowsTo{Enabled: true}}
}
} else {
if Conf.Graph.Global.Arrow {
link.Arrows = &GraphArrows{To: &GraphArrowsTo{Enabled: true}}
}
}
*links = append(*links, link)
}
}
}
func genTreeNodes(blocks []*Block, nodes *[]*GraphNode, links *[]*GraphLink, local bool, style map[string]string) {
nodeSize := Conf.Graph.Local.NodeSize
if !local {
nodeSize = Conf.Graph.Global.NodeSize
}
for _, block := range blocks {
node := &GraphNode{
ID: block.ID,
Box: block.Box,
Path: block.Path,
Type: block.Type,
Size: nodeSize,
Color: &GraphNodeColor{Background: nodeColor(block.Type, style)},
}
nodeTitleLabel(node, nodeContentByBlock(block))
*nodes = append(*nodes, node)
*links = append(*links, &GraphLink{
From: block.ParentID,
To: block.ID,
Ref: false,
Color: linkColor(false, style),
})
}
}
func markLinkedNodes(nodes *[]*GraphNode, links *[]*GraphLink, local bool) {
nodeSize := Conf.Graph.Local.NodeSize
if !local {
nodeSize = Conf.Graph.Global.NodeSize
}
tmpLinks := (*links)[:0]
for _, link := range *links {
var sourceFound, targetFound bool
for _, node := range *nodes {
if link.To == node.ID {
if link.Ref {
size := nodeSize
node.Defs++
size = math.Log2(float64(node.Defs))*nodeSize + nodeSize
node.Size = size
}
targetFound = true
} else if link.From == node.ID {
node.Refs++
sourceFound = true
}
if targetFound && sourceFound {
break
}
}
if sourceFound && targetFound {
tmpLinks = append(tmpLinks, link)
}
}
*links = tmpLinks
}
func removeDuplicatedUnescape(nodes []*GraphNode) (ret []*GraphNode) {
m := map[string]*GraphNode{}
for _, n := range nodes {
if nil == m[n.ID] {
n.Title = html.UnescapeString(n.Title)
n.Label = html.UnescapeString(n.Label)
ret = append(ret, n)
m[n.ID] = n
}
}
return ret
}
func pruneUnref(nodes *[]*GraphNode, links *[]*GraphLink) {
maxBlocks := Conf.Graph.MaxBlocks
tmpNodes := (*nodes)[:0]
for _, node := range *nodes {
if 0 == Conf.Graph.Global.MinRefs {
tmpNodes = append(tmpNodes, node)
} else {
if Conf.Graph.Global.MinRefs <= node.Refs {
tmpNodes = append(tmpNodes, node)
continue
}
if Conf.Graph.Global.MinRefs <= node.Defs {
tmpNodes = append(tmpNodes, node)
continue
}
}
if maxBlocks < len(tmpNodes) {
util.LogWarnf("exceeded the maximum number of render nodes [%d]", maxBlocks)
break
}
}
*nodes = tmpNodes
tmpLinks := (*links)[:0]
for _, link := range *links {
var sourceFound, targetFound bool
for _, node := range *nodes {
if link.To == node.ID {
targetFound = true
} else if link.From == node.ID {
sourceFound = true
}
}
if sourceFound && targetFound {
tmpLinks = append(tmpLinks, link)
}
}
*links = tmpLinks
}
func nodeContentByBlock(block *Block) (ret string) {
if ret = block.Name; "" != ret {
return
}
if ret = block.Memo; "" != ret {
return
}
ret = block.Content
if maxLen := 48; maxLen < utf8.RuneCountInString(ret) {
ret = gulu.Str.SubStr(ret, maxLen) + "..."
}
return
}
func nodeContentByNode(node *ast.Node, text string) (ret string) {
if ret = node.IALAttr("name"); "" != ret {
return
}
if ret = node.IALAttr("memo"); "" != ret {
return
}
if maxLen := 48; maxLen < utf8.RuneCountInString(text) {
text = gulu.Str.SubStr(text, maxLen) + "..."
}
ret = html.EscapeString(text)
return
}
func linkColor(ref bool, style map[string]string) (ret *GraphLinkColor) {
ret = &GraphLinkColor{}
if ref {
ret.Color = style["--b3-graph-ref-line"]
return
}
ret.Color = style["--b3-graph-line"]
return
}
func nodeColor(typ string, style map[string]string) string {
switch typ {
case "NodeDocument":
return style["--b3-graph-doc-point"]
case "NodeParagraph":
return style["--b3-graph-p-point"]
case "NodeHeading":
return style["--b3-graph-heading-point"]
case "NodeMathBlock":
return style["--b3-graph-math-point"]
case "NodeCodeBlock":
return style["--b3-graph-code-point"]
case "NodeTable":
return style["--b3-graph-table-point"]
case "NodeList":
return style["--b3-graph-list-point"]
case "NodeListItem":
return style["--b3-graph-listitem-point"]
case "NodeBlockquote":
return style["--b3-graph-bq-point"]
case "NodeSuperBlock":
return style["--b3-graph-super-point"]
}
return style["--b3-graph-p-point"]
}
func graphTypeFilter(local bool) string {
var inList []string
paragraph := Conf.Graph.Local.Paragraph
if !local {
paragraph = Conf.Graph.Global.Paragraph
}
if paragraph {
inList = append(inList, "'p'")
}
heading := Conf.Graph.Local.Heading
if !local {
heading = Conf.Graph.Global.Heading
}
if heading {
inList = append(inList, "'h'")
}
math := Conf.Graph.Local.Math
if !local {
math = Conf.Graph.Global.Math
}
if math {
inList = append(inList, "'m'")
}
code := Conf.Graph.Local.Code
if !local {
code = Conf.Graph.Global.Code
}
if code {
inList = append(inList, "'c'")
}
table := Conf.Graph.Local.Table
if !local {
table = Conf.Graph.Global.Table
}
if table {
inList = append(inList, "'t'")
}
list := Conf.Graph.Local.List
if !local {
list = Conf.Graph.Global.List
}
if list {
inList = append(inList, "'l'")
}
listItem := Conf.Graph.Local.ListItem
if !local {
listItem = Conf.Graph.Global.ListItem
}
if listItem {
inList = append(inList, "'i'")
}
blockquote := Conf.Graph.Local.Blockquote
if !local {
blockquote = Conf.Graph.Global.Blockquote
}
if blockquote {
inList = append(inList, "'b'")
}
super := Conf.Graph.Local.Super
if !local {
super = Conf.Graph.Global.Super
}
if super {
inList = append(inList, "'s'")
}
inList = append(inList, "'d'")
return " AND ref.type IN (" + strings.Join(inList, ",") + ")"
}
func graphDailyNoteFilter(local bool) string {
dailyNote := Conf.Graph.Local.DailyNote
if !local {
dailyNote = Conf.Graph.Global.DailyNote
}
if dailyNote {
return ""
}
var dailyNotesPaths []string
for _, box := range Conf.GetOpenedBoxes() {
boxConf := box.GetConf()
if 1 < strings.Count(boxConf.DailyNoteSavePath, "/") {
dailyNoteSaveDir := strings.Split(boxConf.DailyNoteSavePath, "/")[1]
dailyNotesPaths = append(dailyNotesPaths, "/"+dailyNoteSaveDir)
}
}
if 1 > len(dailyNotesPaths) {
return ""
}
buf := bytes.Buffer{}
for _, p := range dailyNotesPaths {
buf.WriteString(" AND ref.hpath NOT LIKE '" + p + "%'")
}
return buf.String()
}
func graphStyle(local bool) (ret map[string]string) {
ret = map[string]string{}
ret["--b3-graph-doc-point"] = currentCSSValue("--b3-graph-doc-point")
ret["--b3-graph-p-point"] = currentCSSValue("--b3-graph-p-point")
ret["--b3-graph-heading-point"] = currentCSSValue("--b3-graph-heading-point")
ret["--b3-graph-math-point"] = currentCSSValue("--b3-graph-math-point")
ret["--b3-graph-code-point"] = currentCSSValue("--b3-graph-code-point")
ret["--b3-graph-table-point"] = currentCSSValue("--b3-graph-table-point")
ret["--b3-graph-list-point"] = currentCSSValue("--b3-graph-list-point")
ret["--b3-graph-listitem-point"] = currentCSSValue("--b3-graph-listitem-point")
ret["--b3-graph-bq-point"] = currentCSSValue("--b3-graph-bq-point")
ret["--b3-graph-super-point"] = currentCSSValue("--b3-graph-super-point")
ret["--b3-graph-line"] = currentCSSValue("--b3-graph-line")
ret["--b3-graph-ref-line"] = currentCSSValue("--b3-graph-ref-line")
ret["--b3-graph-tag-line"] = currentCSSValue("--b3-graph-tag-line")
ret["--b3-graph-tag-tag-line"] = currentCSSValue("--b3-graph-tag-tag-line")
ret["--b3-graph-asset-line"] = currentCSSValue("--b3-graph-asset-line")
return
}
func nodeTitleLabel(node *GraphNode, blockContent string) {
if "NodeDocument" != node.Type && "NodeHeading" != node.Type {
node.Title = blockContent
} else {
node.Label = blockContent
}
}

312
kernel/model/heading.go Normal file
View file

@ -0,0 +1,312 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"errors"
"os"
"path"
"path/filepath"
"strings"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/siyuan-note/siyuan/kernel/cache"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func (tx *Transaction) doFoldHeading(operation *Operation) (ret *TxErr) {
headingID := operation.ID
tree, err := loadTreeByBlockID(headingID)
if nil != err {
return &TxErr{code: TxErrCodeBlockNotFound, id: headingID}
}
childrenIDs := []string{} // 这里不能用 nil否则折叠下方没内容的标题时会内核中断 https://github.com/siyuan-note/siyuan/issues/3643
heading := treenode.GetNodeInTree(tree, headingID)
if nil == heading {
return &TxErr{code: TxErrCodeBlockNotFound, id: headingID}
}
children := treenode.HeadingChildren(heading)
for _, child := range children {
childrenIDs = append(childrenIDs, child.ID)
child.RemoveIALAttr("fold")
child.SetIALAttr("heading-fold", "1")
}
heading.SetIALAttr("fold", "1")
if err = tx.writeTree(tree); nil != err {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: headingID}
}
IncWorkspaceDataVer()
cache.PutBlockIAL(headingID, parse.IAL2Map(heading.KramdownIAL))
for _, child := range children {
cache.PutBlockIAL(child.ID, parse.IAL2Map(child.KramdownIAL))
}
sql.UpsertTreeQueue(tree)
operation.RetData = childrenIDs
return
}
func (tx *Transaction) doUnfoldHeading(operation *Operation) (ret *TxErr) {
headingID := operation.ID
tree, err := loadTreeByBlockID(headingID)
if nil != err {
return &TxErr{code: TxErrCodeBlockNotFound, id: headingID}
}
heading := treenode.GetNodeInTree(tree, headingID)
if nil == heading {
return &TxErr{code: TxErrCodeBlockNotFound, id: headingID}
}
children := treenode.FoldedHeadingChildren(heading)
for _, child := range children {
child.RemoveIALAttr("heading-fold")
child.RemoveIALAttr("fold")
}
heading.RemoveIALAttr("fold")
if err = tx.writeTree(tree); nil != err {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: headingID}
}
IncWorkspaceDataVer()
cache.PutBlockIAL(headingID, parse.IAL2Map(heading.KramdownIAL))
for _, child := range children {
cache.PutBlockIAL(child.ID, parse.IAL2Map(child.KramdownIAL))
}
sql.UpsertTreeQueue(tree)
luteEngine := NewLute()
operation.RetData = renderBlockDOMByNodes(children, luteEngine)
return
}
func Doc2Heading(srcID, targetID string, after bool) (srcTreeBox, srcTreePath string, err error) {
WaitForWritingFiles()
srcTree, _ := loadTreeByBlockID(srcID)
if nil == srcTree {
err = ErrBlockNotFound
return
}
subDir := filepath.Join(util.DataDir, srcTree.Box, strings.TrimSuffix(srcTree.Path, ".sy"))
if gulu.File.IsDir(subDir) {
if !util.IsEmptyDir(subDir) {
err = errors.New(Conf.Language(20))
return
} else {
os.Remove(subDir) // 移除空文件夹不会有副作用
}
}
targetTree, _ := loadTreeByBlockID(targetID)
if nil == targetTree {
err = ErrBlockNotFound
return
}
pivot := treenode.GetNodeInTree(targetTree, targetID)
if nil == pivot {
err = ErrBlockNotFound
return
}
if ast.NodeListItem == pivot.Type {
pivot = pivot.LastChild
}
pivotLevel := treenode.HeadingLevel(pivot)
deltaLevel := pivotLevel - treenode.TopHeadingLevel(srcTree) + 1
headingLevel := pivotLevel
if ast.NodeHeading == pivot.Type { // 平级插入
children := treenode.HeadingChildren(pivot)
if after {
if length := len(children); 0 < length {
pivot = children[length-1]
}
}
} else { // 子节点插入
headingLevel++
deltaLevel++
}
if 6 < headingLevel {
headingLevel = 6
}
srcTree.Root.RemoveIALAttr("type")
heading := &ast.Node{ID: srcTree.Root.ID, Type: ast.NodeHeading, HeadingLevel: headingLevel, KramdownIAL: srcTree.Root.KramdownIAL}
heading.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(srcTree.Root.IALAttr("title"))})
heading.Box = targetTree.Box
heading.Path = targetTree.Path
var nodes []*ast.Node
if after {
for c := srcTree.Root.LastChild; nil != c; c = c.Previous {
nodes = append(nodes, c)
}
} else {
for c := srcTree.Root.FirstChild; nil != c; c = c.Next {
nodes = append(nodes, c)
}
}
if !after {
pivot.InsertBefore(heading)
}
for _, n := range nodes {
if ast.NodeHeading == n.Type {
n.HeadingLevel = n.HeadingLevel + deltaLevel
if 6 < n.HeadingLevel {
n.HeadingLevel = 6
}
}
n.Box = targetTree.Box
n.Path = targetTree.Path
if after {
pivot.InsertAfter(n)
} else {
pivot.InsertBefore(n)
}
}
if after {
pivot.InsertAfter(heading)
}
if contentPivot := treenode.GetNodeInTree(targetTree, targetID); nil != contentPivot && ast.NodeParagraph == contentPivot.Type && nil == contentPivot.FirstChild { // 插入到空的段落块下
contentPivot.Unlink()
}
srcTreeBox, srcTreePath = srcTree.Box, srcTree.Path
srcTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
if err = indexWriteJSONQueue(srcTree); nil != err {
return
}
targetTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
err = indexWriteJSONQueue(targetTree)
IncWorkspaceDataVer()
RefreshBacklink(srcTree.ID)
RefreshBacklink(targetTree.ID)
return
}
func Heading2Doc(srcHeadingID, targetBoxID, targetPath string) (srcRootBlockID, newTargetPath string, err error) {
WaitForWritingFiles()
srcTree, _ := loadTreeByBlockID(srcHeadingID)
if nil == srcTree {
err = ErrBlockNotFound
return
}
srcRootBlockID = srcTree.Root.ID
headingBlock, err := getBlock(srcHeadingID)
if nil != err {
return
}
if nil == headingBlock {
err = ErrBlockNotFound
return
}
headingNode := treenode.GetNodeInTree(srcTree, srcHeadingID)
if nil == headingNode {
err = ErrBlockNotFound
return
}
box := Conf.Box(targetBoxID)
headingText := sql.GetRefText(headingNode.ID)
headingText = util.FilterFileName(headingText)
moveToRoot := "/" == targetPath
toHP := path.Join("/", headingText)
toFolder := "/"
if !moveToRoot {
toBlock := treenode.GetBlockTreeRootByPath(targetBoxID, targetPath)
if nil == toBlock {
err = ErrBlockNotFound
return
}
toHP = path.Join(toBlock.HPath, headingText)
toFolder = path.Join(path.Dir(targetPath), toBlock.ID)
}
newTargetPath = path.Join(toFolder, srcHeadingID+".sy")
if !box.Exist(toFolder) {
if err = box.MkdirAll(toFolder); nil != err {
return
}
}
// 折叠标题转换为文档时需要自动展开下方块 https://github.com/siyuan-note/siyuan/issues/2947
children := treenode.FoldedHeadingChildren(headingNode)
for _, child := range children {
child.RemoveIALAttr("heading-fold")
child.RemoveIALAttr("fold")
}
headingNode.RemoveIALAttr("fold")
luteEngine := NewLute()
newTree := &parse.Tree{Root: &ast.Node{Type: ast.NodeDocument, ID: srcHeadingID}, Context: &parse.Context{ParseOption: luteEngine.ParseOptions}}
children = treenode.HeadingChildren(headingNode)
for _, c := range children {
newTree.Root.AppendChild(c)
}
newTree.ID = srcHeadingID
newTree.Path = newTargetPath
newTree.HPath = toHP
headingNode.SetIALAttr("type", "doc")
headingNode.SetIALAttr("id", srcHeadingID)
headingNode.SetIALAttr("title", headingText)
newTree.Root.KramdownIAL = headingNode.KramdownIAL
topLevel := treenode.TopHeadingLevel(newTree)
for c := newTree.Root.FirstChild; nil != c; c = c.Next {
if ast.NodeHeading == c.Type {
c.HeadingLevel = c.HeadingLevel - topLevel + 1
if 6 < c.HeadingLevel {
c.HeadingLevel = 6
}
}
}
headingNode.Unlink()
srcTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
if err = indexWriteJSONQueue(srcTree); nil != err {
return "", "", err
}
newTree.Box, newTree.Path = targetBoxID, newTargetPath
newTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
if err = indexWriteJSONQueue(newTree); nil != err {
return "", "", err
}
IncWorkspaceDataVer()
RefreshBacklink(srcTree.ID)
RefreshBacklink(newTree.ID)
return
}

566
kernel/model/history.go Normal file
View file

@ -0,0 +1,566 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"encoding/json"
"io"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/88250/gulu"
"github.com/88250/protyle"
"github.com/siyuan-note/siyuan/kernel/conf"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
var historyTicker = time.NewTicker(time.Minute * 10)
func AutoGenerateDocHistory() {
ChangeHistoryTick(Conf.Editor.GenerateHistoryInterval)
for {
<-historyTicker.C
generateDocHistory()
}
}
func generateDocHistory() {
if 1 > Conf.Editor.GenerateHistoryInterval {
return
}
WaitForWritingFiles()
syncLock.Lock()
defer syncLock.Unlock()
for _, box := range Conf.GetOpenedBoxes() {
box.generateDocHistory0()
}
historyDir := filepath.Join(util.WorkspaceDir, "history")
clearOutdatedHistoryDir(historyDir)
// 以下部分是老版本的清理逻辑,暂时保留
for _, box := range Conf.GetBoxes() {
historyDir = filepath.Join(util.DataDir, box.ID, ".siyuan", "history")
clearOutdatedHistoryDir(historyDir)
}
historyDir = filepath.Join(util.DataDir, "assets", ".siyuan", "history")
clearOutdatedHistoryDir(historyDir)
historyDir = filepath.Join(util.DataDir, ".siyuan", "history")
clearOutdatedHistoryDir(historyDir)
}
func ChangeHistoryTick(minutes int) {
if 0 >= minutes {
minutes = 3600
}
historyTicker.Reset(time.Minute * time.Duration(minutes))
}
func ClearWorkspaceHistory() (err error) {
historyDir := filepath.Join(util.WorkspaceDir, "history")
if gulu.File.IsDir(historyDir) {
if err = os.RemoveAll(historyDir); nil != err {
util.LogErrorf("remove workspace history dir [%s] failed: %s", historyDir, err)
return
}
util.LogInfof("removed workspace history dir [%s]", historyDir)
}
// 以下部分是老版本的清理逻辑,暂时保留
notebooks, err := ListNotebooks()
if nil != err {
return
}
for _, notebook := range notebooks {
boxID := notebook.ID
historyDir := filepath.Join(util.DataDir, boxID, ".siyuan", "history")
if !gulu.File.IsDir(historyDir) {
continue
}
if err = os.RemoveAll(historyDir); nil != err {
util.LogErrorf("remove notebook history dir [%s] failed: %s", historyDir, err)
return
}
util.LogInfof("removed notebook history dir [%s]", historyDir)
}
historyDir = filepath.Join(util.DataDir, ".siyuan", "history")
if gulu.File.IsDir(historyDir) {
if err = os.RemoveAll(historyDir); nil != err {
util.LogErrorf("remove data history dir [%s] failed: %s", historyDir, err)
return
}
util.LogInfof("removed data history dir [%s]", historyDir)
}
historyDir = filepath.Join(util.DataDir, "assets", ".siyuan", "history")
if gulu.File.IsDir(historyDir) {
if err = os.RemoveAll(historyDir); nil != err {
util.LogErrorf("remove assets history dir [%s] failed: %s", historyDir, err)
return
}
util.LogInfof("removed assets history dir [%s]", historyDir)
}
return
}
func GetDocHistoryContent(historyPath string) (content string, err error) {
if !gulu.File.IsExist(historyPath) {
return
}
data, err := filesys.NoLockFileRead(historyPath)
if nil != err {
util.LogErrorf("read file [%s] failed: %s", historyPath, err)
return
}
luteEngine := NewLute()
historyTree, err := protyle.ParseJSONWithoutFix(luteEngine, data)
if nil != err {
util.LogErrorf("parse tree from file [%s] failed, remove it", historyPath)
os.RemoveAll(historyPath)
return
}
content = renderBlockMarkdown(historyTree.Root)
return
}
func RollbackDocHistory(boxID, historyPath string) (err error) {
if !gulu.File.IsExist(historyPath) {
return
}
WaitForWritingFiles()
syncLock.Lock()
srcPath := historyPath
var destPath string
baseName := filepath.Base(historyPath)
id := strings.TrimSuffix(baseName, ".sy")
filesys.ReleaseFileLocks(filepath.Join(util.DataDir, boxID))
workingDoc := treenode.GetBlockTree(id)
if nil != workingDoc {
if err = os.RemoveAll(filepath.Join(util.DataDir, boxID, workingDoc.Path)); nil != err {
syncLock.Unlock()
return
}
}
destPath, err = getRollbackDockPath(boxID, historyPath)
if nil != err {
syncLock.Unlock()
return
}
if err = gulu.File.Copy(srcPath, destPath); nil != err {
syncLock.Unlock()
return
}
syncLock.Unlock()
RefreshFileTree()
IncWorkspaceDataVer()
return nil
}
func getRollbackDockPath(boxID, historyPath string) (destPath string, err error) {
baseName := filepath.Base(historyPath)
parentID := strings.TrimSuffix(filepath.Base(filepath.Dir(historyPath)), ".sy")
parentWorkingDoc := treenode.GetBlockTree(parentID)
if nil != parentWorkingDoc {
// 父路径如果是文档,则恢复到父路径下
parentDir := strings.TrimSuffix(parentWorkingDoc.Path, ".sy")
parentDir = filepath.Join(util.DataDir, boxID, parentDir)
if err = os.MkdirAll(parentDir, 0755); nil != err {
return
}
destPath = filepath.Join(parentDir, baseName)
} else {
// 父路径如果不是文档,则恢复到笔记本根路径下
destPath = filepath.Join(util.DataDir, boxID, baseName)
}
return
}
func RollbackAssetsHistory(historyPath string) (err error) {
historyPath = filepath.Join(util.WorkspaceDir, historyPath)
if !gulu.File.IsExist(historyPath) {
return
}
from := historyPath
to := filepath.Join(util.DataDir, "assets", filepath.Base(historyPath))
if err = gulu.File.Copy(from, to); nil != err {
util.LogErrorf("copy file [%s] to [%s] failed: %s", from, to, err)
return
}
IncWorkspaceDataVer()
return nil
}
func RollbackNotebookHistory(historyPath string) (err error) {
if !gulu.File.IsExist(historyPath) {
return
}
from := historyPath
to := filepath.Join(util.DataDir, filepath.Base(historyPath))
if err = gulu.File.Copy(from, to); nil != err {
util.LogErrorf("copy file [%s] to [%s] failed: %s", from, to, err)
return
}
RefreshFileTree()
IncWorkspaceDataVer()
return nil
}
type History struct {
Time string `json:"time"`
Items []*HistoryItem `json:"items"`
}
type HistoryItem struct {
Title string `json:"title"`
Path string `json:"path"`
}
const maxHistory = 32
func GetDocHistory(boxID string) (ret []*History, err error) {
ret = []*History{}
historyDir := filepath.Join(util.WorkspaceDir, "history")
if !gulu.File.IsDir(historyDir) {
return
}
historyBoxDirs, err := filepath.Glob(historyDir + "/*/" + boxID)
if nil != err {
util.LogErrorf("read dir [%s] failed: %s", historyDir, err)
return
}
sort.Slice(historyBoxDirs, func(i, j int) bool {
return historyBoxDirs[i] > historyBoxDirs[j]
})
luteEngine := NewLute()
count := 0
for _, historyBoxDir := range historyBoxDirs {
var docs []*HistoryItem
itemCount := 0
filepath.Walk(historyBoxDir, func(path string, info fs.FileInfo, err error) error {
if info.IsDir() {
return nil
}
if !strings.HasSuffix(info.Name(), ".sy") {
return nil
}
data, err := filesys.NoLockFileRead(path)
if nil != err {
util.LogErrorf("read file [%s] failed: %s", path, err)
return nil
}
historyTree, err := protyle.ParseJSONWithoutFix(luteEngine, data)
if nil != err {
util.LogErrorf("parse tree from file [%s] failed, remove it", path)
os.RemoveAll(path)
return nil
}
historyName := historyTree.Root.IALAttr("title")
if "" == historyName {
historyName = info.Name()
}
docs = append(docs, &HistoryItem{
Title: historyTree.Root.IALAttr("title"),
Path: path,
})
itemCount++
if maxHistory < itemCount {
return io.EOF
}
return nil
})
if 1 > len(docs) {
continue
}
timeDir := filepath.Base(filepath.Dir(historyBoxDir))
t := timeDir[:strings.LastIndex(timeDir, "-")]
if ti, parseErr := time.Parse("2006-01-02-150405", t); nil == parseErr {
t = ti.Format("2006-01-02 15:04:05")
}
ret = append(ret, &History{
Time: t,
Items: docs,
})
count++
if maxHistory <= count {
break
}
}
sort.Slice(ret, func(i, j int) bool {
return ret[i].Time > ret[j].Time
})
return
}
func GetNotebookHistory() (ret []*History, err error) {
ret = []*History{}
historyDir := filepath.Join(util.WorkspaceDir, "history")
if !gulu.File.IsDir(historyDir) {
return
}
historyNotebookConfs, err := filepath.Glob(historyDir + "/*-delete/*/.siyuan/conf.json")
if nil != err {
util.LogErrorf("read dir [%s] failed: %s", historyDir, err)
return
}
sort.Slice(historyNotebookConfs, func(i, j int) bool {
iTimeDir := filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(historyNotebookConfs[i]))))
jTimeDir := filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(historyNotebookConfs[j]))))
return iTimeDir > jTimeDir
})
historyCount := 0
for _, historyNotebookConf := range historyNotebookConfs {
timeDir := filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(historyNotebookConf))))
t := timeDir[:strings.LastIndex(timeDir, "-")]
if ti, parseErr := time.Parse("2006-01-02-150405", t); nil == parseErr {
t = ti.Format("2006-01-02 15:04:05")
}
var c conf.BoxConf
data, readErr := os.ReadFile(historyNotebookConf)
if nil != readErr {
util.LogErrorf("read notebook conf [%s] failed: %s", historyNotebookConf, readErr)
continue
}
if err = json.Unmarshal(data, &c); nil != err {
util.LogErrorf("parse notebook conf [%s] failed: %s", historyNotebookConf, err)
continue
}
ret = append(ret, &History{
Time: t,
Items: []*HistoryItem{
{
Title: c.Name,
Path: filepath.Dir(filepath.Dir(historyNotebookConf)),
},
},
})
historyCount++
if maxHistory <= historyCount {
break
}
}
sort.Slice(ret, func(i, j int) bool {
return ret[i].Time > ret[j].Time
})
return
}
func GetAssetsHistory() (ret []*History, err error) {
ret = []*History{}
historyDir := filepath.Join(util.WorkspaceDir, "history")
if !gulu.File.IsDir(historyDir) {
return
}
historyAssetsDirs, err := filepath.Glob(historyDir + "/*/assets")
if nil != err {
util.LogErrorf("read dir [%s] failed: %s", historyDir, err)
return
}
sort.Slice(historyAssetsDirs, func(i, j int) bool {
return historyAssetsDirs[i] > historyAssetsDirs[j]
})
historyCount := 0
for _, historyAssetsDir := range historyAssetsDirs {
var assets []*HistoryItem
itemCount := 0
filepath.Walk(historyAssetsDir, func(path string, info fs.FileInfo, err error) error {
if isSkipFile(info.Name()) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
if info.IsDir() {
return nil
}
assets = append(assets, &HistoryItem{
Title: info.Name(),
Path: filepath.ToSlash(strings.TrimPrefix(path, util.WorkspaceDir)),
})
itemCount++
if maxHistory < itemCount {
return io.EOF
}
return nil
})
if 1 > len(assets) {
continue
}
timeDir := filepath.Base(filepath.Dir(historyAssetsDir))
t := timeDir[:strings.LastIndex(timeDir, "-")]
if ti, parseErr := time.Parse("2006-01-02-150405", t); nil == parseErr {
t = ti.Format("2006-01-02 15:04:05")
}
ret = append(ret, &History{
Time: t,
Items: assets,
})
historyCount++
if maxHistory <= historyCount {
break
}
}
sort.Slice(ret, func(i, j int) bool {
return ret[i].Time > ret[j].Time
})
return
}
func (box *Box) generateDocHistory0() {
files := box.recentModifiedDocs()
if 1 > len(files) {
return
}
historyDir, err := util.GetHistoryDir("update")
if nil != err {
util.LogErrorf("get history dir failed: %s", err)
return
}
for _, file := range files {
historyPath := filepath.Join(historyDir, box.ID, strings.TrimPrefix(file, filepath.Join(util.DataDir, box.ID)))
if err = os.MkdirAll(filepath.Dir(historyPath), 0755); nil != err {
util.LogErrorf("generate history failed: %s", err)
return
}
var data []byte
if data, err = filesys.NoLockFileRead(file); err != nil {
util.LogErrorf("generate history failed: %s", err)
return
}
if err = gulu.File.WriteFileSafer(historyPath, data, 0644); err != nil {
util.LogErrorf("generate history failed: %s", err)
return
}
}
return
}
func clearOutdatedHistoryDir(historyDir string) {
if !gulu.File.IsExist(historyDir) {
return
}
dirs, err := os.ReadDir(historyDir)
if nil != err {
util.LogErrorf("clear history [%s] failed: %s", historyDir, err)
return
}
now := time.Now()
var removes []string
for _, dir := range dirs {
dirInfo, err := dir.Info()
if nil != err {
util.LogErrorf("read history dir [%s] failed: %s", dir.Name(), err)
continue
}
if Conf.Editor.HistoryRetentionDays < int(now.Sub(dirInfo.ModTime()).Hours()/24) {
removes = append(removes, filepath.Join(historyDir, dir.Name()))
}
}
for _, dir := range removes {
if err = os.RemoveAll(dir); nil != err {
util.LogErrorf("remove history dir [%s] failed: %s", err)
continue
}
//util.LogInfof("auto removed history dir [%s]", dir)
}
}
var boxLatestHistoryTime = map[string]time.Time{}
func (box *Box) recentModifiedDocs() (ret []string) {
latestHistoryTime := boxLatestHistoryTime[box.ID]
filepath.Walk(filepath.Join(util.DataDir, box.ID), func(path string, info fs.FileInfo, err error) error {
if nil == info {
return nil
}
if isSkipFile(info.Name()) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
if info.IsDir() {
return nil
}
if info.ModTime().After(latestHistoryTime) {
ret = append(ret, filepath.Join(path))
}
return nil
})
box.UpdateHistoryGenerated()
return
}

646
kernel/model/import.go Normal file
View file

@ -0,0 +1,646 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"math/rand"
"os"
"path"
"path/filepath"
"runtime/debug"
"sort"
"strconv"
"strings"
"time"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/html"
"github.com/88250/lute/parse"
"github.com/88250/protyle"
"github.com/mattn/go-zglob"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func ImportSY(zipPath, boxID, toPath string) (err error) {
util.PushEndlessProgress(Conf.Language(73))
defer util.ClearPushProgress(100)
baseName := filepath.Base(zipPath)
ext := filepath.Ext(baseName)
baseName = strings.TrimSuffix(baseName, ext)
unzipPath := filepath.Join(filepath.Dir(zipPath), baseName+"-"+gulu.Rand.String(7))
err = gulu.Zip.Unzip(zipPath, unzipPath)
if nil != err {
return
}
defer os.RemoveAll(unzipPath)
var syPaths []string
filepath.Walk(unzipPath, func(path string, info fs.FileInfo, err error) error {
if nil != err {
return err
}
if !info.IsDir() && strings.HasSuffix(info.Name(), ".sy") {
syPaths = append(syPaths, path)
}
return nil
})
unzipRootPaths, err := filepath.Glob(unzipPath + "/*")
if nil != err {
return
}
if 1 != len(unzipRootPaths) {
util.LogErrorf("invalid .sy.zip")
return errors.New("invalid .sy.zip")
}
unzipRootPath := unzipRootPaths[0]
luteEngine := util.NewLute()
blockIDs := map[string]string{}
trees := map[string]*parse.Tree{}
// 重新生成块 ID
for _, syPath := range syPaths {
data, readErr := os.ReadFile(syPath)
if nil != readErr {
util.LogErrorf("read .sy [%s] failed: %s", syPath, readErr)
err = readErr
return
}
tree, _, parseErr := protyle.ParseJSON(luteEngine, data)
if nil != parseErr {
util.LogErrorf("parse .sy [%s] failed: %s", syPath, parseErr)
err = parseErr
return
}
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if "" != n.ID {
newNodeID := ast.NewNodeID()
blockIDs[n.ID] = newNodeID
n.ID = newNodeID
n.SetIALAttr("id", newNodeID)
}
return ast.WalkContinue
})
tree.ID = tree.Root.ID
tree.Path = filepath.ToSlash(strings.TrimPrefix(syPath, unzipRootPath))
trees[tree.ID] = tree
}
// 引用指向重新生成的块 ID
for _, tree := range trees {
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeBlockRefID == n.Type {
newDefID := blockIDs[n.TokensStr()]
if "" != newDefID {
n.Tokens = gulu.Str.ToBytes(newDefID)
} else {
util.LogWarnf("not found def [" + n.TokensStr() + "]")
}
}
return ast.WalkContinue
})
}
// 写回 .sy
for _, tree := range trees {
syPath := filepath.Join(unzipRootPath, tree.Path)
renderer := protyle.NewJSONRenderer(tree, luteEngine.RenderOptions)
data := renderer.Render()
buf := bytes.Buffer{}
buf.Grow(4096)
if err = json.Indent(&buf, data, "", "\t"); nil != err {
return
}
data = buf.Bytes()
if err = os.WriteFile(syPath, data, 0644); nil != err {
util.LogErrorf("write .sy [%s] failed: %s", syPath, err)
return
}
newSyPath := filepath.Join(filepath.Dir(syPath), tree.ID+".sy")
if err = os.Rename(syPath, newSyPath); nil != err {
util.LogErrorf("rename .sy from [%s] to [%s] failed: %s", syPath, newSyPath, err)
return
}
}
// 重命名文件路径
renamePaths := map[string]string{}
filepath.Walk(unzipRootPath, func(path string, info fs.FileInfo, err error) error {
if nil != err {
return err
}
if info.IsDir() && util.IsIDPattern(info.Name()) {
renamePaths[path] = path
}
return nil
})
for p, _ := range renamePaths {
originalPath := p
p = strings.TrimPrefix(p, unzipRootPath)
p = filepath.ToSlash(p)
parts := strings.Split(p, "/")
buf := bytes.Buffer{}
buf.WriteString("/")
for i, part := range parts {
if "" == part {
continue
}
newNodeID := blockIDs[part]
if "" != newNodeID {
buf.WriteString(newNodeID)
} else {
buf.WriteString(part)
}
if i < len(parts)-1 {
buf.WriteString("/")
}
}
newPath := buf.String()
renamePaths[originalPath] = filepath.Join(unzipRootPath, newPath)
}
var oldPaths []string
for oldPath, _ := range renamePaths {
oldPaths = append(oldPaths, oldPath)
}
sort.Slice(oldPaths, func(i, j int) bool {
return strings.Count(oldPaths[i], string(os.PathSeparator)) < strings.Count(oldPaths[j], string(os.PathSeparator))
})
for i, oldPath := range oldPaths {
newPath := renamePaths[oldPath]
if err = os.Rename(oldPath, newPath); nil != err {
util.LogErrorf("rename path from [%s] to [%s] failed: %s", oldPath, renamePaths[oldPath], err)
return errors.New("rename path failed")
}
delete(renamePaths, oldPath)
var toRemoves []string
newRenamedPaths := map[string]string{}
for oldP, newP := range renamePaths {
if strings.HasPrefix(oldP, oldPath) {
renamedOldP := strings.Replace(oldP, oldPath, newPath, 1)
newRenamedPaths[renamedOldP] = newP
toRemoves = append(toRemoves, oldPath)
}
}
for _, toRemove := range toRemoves {
delete(renamePaths, toRemove)
}
for oldP, newP := range newRenamedPaths {
renamePaths[oldP] = newP
}
for j := i + 1; j < len(oldPaths); j++ {
if strings.HasPrefix(oldPaths[j], oldPath) {
renamedOldP := strings.Replace(oldPaths[j], oldPath, newPath, 1)
oldPaths[j] = renamedOldP
}
}
}
assetsDirs, err := zglob.Glob(unzipRootPath + "/**/assets")
if nil != err {
return
}
if 0 < len(assetsDirs) {
for _, assets := range assetsDirs {
if gulu.File.IsDir(assets) {
dataAssets := filepath.Join(util.DataDir, "assets")
if err = gulu.File.Copy(assets, dataAssets); nil != err {
util.LogErrorf("copy assets from [%s] to [%s] failed: %s", assets, dataAssets, err)
return
}
}
os.RemoveAll(assets)
}
}
syncLock.Lock()
defer syncLock.Unlock()
filesys.ReleaseAllFileLocks()
var baseTargetPath string
if "/" == toPath {
baseTargetPath = "/"
} else {
block := treenode.GetBlockTreeRootByPath(boxID, toPath)
if nil == block {
util.LogErrorf("not found block by path [%s]", toPath)
return nil
}
baseTargetPath = strings.TrimSuffix(block.Path, ".sy")
}
targetDir := filepath.Join(util.DataDir, boxID, baseTargetPath)
if err = os.MkdirAll(targetDir, 0755); nil != err {
return
}
if err = stableCopy(unzipRootPath, targetDir); nil != err {
util.LogErrorf("copy data dir from [%s] to [%s] failed: %s", unzipRootPath, util.DataDir, err)
err = errors.New("copy data failed")
return
}
IncWorkspaceDataVer()
refreshFileTree()
return
}
func ImportData(zipPath string) (err error) {
util.PushEndlessProgress(Conf.Language(73))
defer util.ClearPushProgress(100)
baseName := filepath.Base(zipPath)
ext := filepath.Ext(baseName)
baseName = strings.TrimSuffix(baseName, ext)
unzipPath := filepath.Join(filepath.Dir(zipPath), baseName)
err = gulu.Zip.Unzip(zipPath, unzipPath)
if nil != err {
return
}
defer os.RemoveAll(unzipPath)
files, err := filepath.Glob(filepath.Join(unzipPath, "*/.siyuan/conf.json"))
if nil != err {
util.LogErrorf("glob conf.json failed: %s", err)
return errors.New("not found conf.json")
}
if 1 > len(files) {
return errors.New("not found conf.json")
}
confPath := files[0]
confData, err := os.ReadFile(confPath)
if nil != err {
return errors.New("read conf.json failed")
}
dataConf := &filesys.DataConf{}
if err = gulu.JSON.UnmarshalJSON(confData, dataConf); nil != err {
util.LogErrorf("unmarshal conf.json failed: %s", err)
return errors.New("unmarshal conf.json failed")
}
dataConf.Device = util.GetDeviceID()
confData, err = gulu.JSON.MarshalJSON(dataConf)
if nil != err {
util.LogErrorf("marshal conf.json failed: %s", err)
return errors.New("marshal conf.json failed")
}
if err = os.WriteFile(confPath, confData, 0644); nil != err {
util.LogErrorf("write conf.json failed: %s", err)
return errors.New("write conf.json failed")
}
syncLock.Lock()
defer syncLock.Unlock()
filesys.ReleaseAllFileLocks()
tmpDataPath := filepath.Dir(filepath.Dir(confPath))
if err = stableCopy(tmpDataPath, util.DataDir); nil != err {
util.LogErrorf("copy data dir from [%s] to [%s] failed: %s", tmpDataPath, util.DataDir, err)
err = errors.New("copy data failed")
return
}
IncWorkspaceDataVer()
refreshFileTree()
return
}
func ImportFromLocalPath(boxID, localPath string, toPath string) (err error) {
util.PushEndlessProgress(Conf.Language(73))
WaitForWritingFiles()
syncLock.Lock()
defer syncLock.Unlock()
box := Conf.Box(boxID)
var baseHPath, baseTargetPath, boxLocalPath string
if "/" == toPath {
baseHPath = "/"
baseTargetPath = "/"
} else {
block := treenode.GetBlockTreeRootByPath(boxID, toPath)
if nil == block {
util.LogErrorf("not found block by path [%s]", toPath)
return nil
}
baseHPath = block.HPath
baseTargetPath = strings.TrimSuffix(block.Path, ".sy")
}
boxLocalPath = filepath.Join(util.DataDir, boxID)
if gulu.File.IsDir(localPath) {
folderName := filepath.Base(localPath)
p := path.Join(toPath, folderName)
if box.Exist(p) {
return errors.New(Conf.Language(1))
}
// 收集所有资源文件
assets := map[string]string{}
filepath.Walk(localPath, func(currentPath string, info os.FileInfo, walkErr error) error {
if localPath == currentPath {
return nil
}
if strings.HasPrefix(info.Name(), ".") {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(info.Name(), ".md") && !strings.HasSuffix(info.Name(), ".markdown") {
dest := currentPath
assets[dest] = currentPath
return nil
}
return nil
})
targetPaths := map[string]string{}
// md 转换 sy
i := 0
filepath.Walk(localPath, func(currentPath string, info os.FileInfo, walkErr error) error {
if strings.HasPrefix(info.Name(), ".") {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
var tree *parse.Tree
ext := path.Ext(info.Name())
title := strings.TrimSuffix(info.Name(), ext)
id := ast.NewNodeID()
curRelPath := filepath.ToSlash(strings.TrimPrefix(currentPath, localPath))
targetPath := path.Join(baseTargetPath, id)
if "" == curRelPath {
curRelPath = "/"
} else {
dirPath := targetPaths[path.Dir(curRelPath)]
targetPath = path.Join(dirPath, id)
}
targetPath = strings.ReplaceAll(targetPath, ".sy/", "/")
targetPath += ".sy"
targetPaths[curRelPath] = targetPath
hPath := path.Join(baseHPath, filepath.ToSlash(strings.TrimPrefix(currentPath, localPath)))
hPath = strings.TrimSuffix(hPath, ext)
if info.IsDir() {
tree = treenode.NewTree(boxID, targetPath, hPath, title)
if err = filesys.WriteTree(tree); nil != err {
return io.EOF
}
return nil
}
if !strings.HasSuffix(info.Name(), ".md") && !strings.HasSuffix(info.Name(), ".markdown") {
return nil
}
data, readErr := os.ReadFile(currentPath)
if nil != readErr {
err = readErr
return io.EOF
}
tree = parseKTree(data)
if nil == tree {
util.LogErrorf("parse tree [%s] failed", currentPath)
return nil
}
tree.ID = id
tree.Root.ID = id
tree.Root.SetIALAttr("id", tree.Root.ID)
tree.Root.SetIALAttr("title", title)
tree.Box = boxID
targetPath = path.Join(path.Dir(targetPath), tree.Root.ID+".sy")
tree.Path = targetPath
targetPaths[curRelPath] = targetPath
tree.HPath = hPath
docDirLocalPath := filepath.Dir(filepath.Join(boxLocalPath, targetPath))
assetDirPath := getAssetsDir(boxLocalPath, docDirLocalPath)
currentDir := filepath.Dir(currentPath)
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || ast.NodeLinkDest != n.Type {
return ast.WalkContinue
}
dest := n.TokensStr()
if !util.IsRelativePath(dest) || "" == dest {
return ast.WalkContinue
}
absDest := filepath.Join(currentDir, dest)
fullPath, exist := assets[absDest]
if !exist {
absDest = filepath.Join(currentDir, string(html.DecodeDestination([]byte(dest))))
}
fullPath, exist = assets[absDest]
if exist {
name := filepath.Base(fullPath)
ext := filepath.Ext(name)
name = strings.TrimSuffix(name, ext)
name += "-" + ast.NewNodeID() + ext
assetTargetPath := filepath.Join(assetDirPath, name)
delete(assets, absDest)
if err = gulu.File.Copy(fullPath, assetTargetPath); nil != err {
util.LogErrorf("copy asset from [%s] to [%s] failed: %s", fullPath, assetTargetPath, err)
return ast.WalkContinue
}
n.Tokens = gulu.Str.ToBytes("assets/" + name)
}
return ast.WalkContinue
})
reassignIDUpdated(tree)
if err = filesys.WriteTree(tree); nil != err {
return io.EOF
}
i++
if 0 == i%4 {
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(66), util.ShortPathForBootingDisplay(tree.Path)))
}
return nil
})
if nil != err {
return err
}
IncWorkspaceDataVer()
refreshFileTree()
} else { // 导入单个文件
fileName := filepath.Base(localPath)
if !strings.HasSuffix(fileName, ".md") && !strings.HasSuffix(fileName, ".markdown") {
return errors.New(Conf.Language(79))
}
title := strings.TrimSuffix(fileName, ".markdown")
title = strings.TrimSuffix(title, ".md")
targetPath := strings.TrimSuffix(toPath, ".sy")
id := ast.NewNodeID()
targetPath = path.Join(targetPath, id+".sy")
var data []byte
data, err = os.ReadFile(localPath)
if nil != err {
return err
}
tree := parseKTree(data)
if nil == tree {
msg := fmt.Sprintf("parse tree [%s] failed", localPath)
util.LogErrorf(msg)
return errors.New(msg)
}
tree.ID = id
tree.Root.ID = id
tree.Root.SetIALAttr("id", tree.Root.ID)
tree.Root.SetIALAttr("title", title)
tree.Box = boxID
tree.Path = targetPath
tree.HPath = path.Join(baseHPath, title)
docDirLocalPath := filepath.Dir(filepath.Join(boxLocalPath, targetPath))
assetDirPath := getAssetsDir(boxLocalPath, docDirLocalPath)
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || ast.NodeLinkDest != n.Type {
return ast.WalkContinue
}
dest := n.TokensStr()
if !util.IsRelativePath(dest) {
return ast.WalkContinue
}
dest = filepath.ToSlash(dest)
if "" == dest {
return ast.WalkContinue
}
absolutePath := filepath.Join(filepath.Dir(localPath), dest)
exist := gulu.File.IsExist(absolutePath)
if !exist {
absolutePath = filepath.Join(filepath.Dir(localPath), string(html.DecodeDestination([]byte(dest))))
exist = gulu.File.IsExist(absolutePath)
}
if exist {
name := filepath.Base(absolutePath)
ext := filepath.Ext(name)
name = strings.TrimSuffix(name, ext)
name += "-" + ast.NewNodeID() + ext
assetTargetPath := filepath.Join(assetDirPath, name)
if err = gulu.File.CopyFile(absolutePath, assetTargetPath); nil != err {
util.LogErrorf("copy asset from [%s] to [%s] failed: %s", absolutePath, assetTargetPath, err)
return ast.WalkContinue
}
n.Tokens = gulu.Str.ToBytes("assets/" + name)
}
return ast.WalkContinue
})
reassignIDUpdated(tree)
if err = indexWriteJSONQueue(tree); nil != err {
return
}
IncWorkspaceDataVer()
sql.WaitForWritingDatabase()
util.PushEndlessProgress(Conf.Language(58))
go func() {
time.Sleep(2 * time.Second)
util.ReloadUI()
}()
}
debug.FreeOSMemory()
IncWorkspaceDataVer()
return
}
func reassignIDUpdated(tree *parse.Tree) {
var blockCount int
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || "" == n.ID {
return ast.WalkContinue
}
blockCount++
return ast.WalkContinue
})
ids := make([]string, blockCount)
min, _ := strconv.ParseInt(time.Now().Add(-1*time.Duration(blockCount)*time.Second).Format("20060102150405"), 10, 64)
for i := 0; i < blockCount; i++ {
ids[i] = newID(fmt.Sprintf("%d", min))
min++
}
var i int
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || "" == n.ID {
return ast.WalkContinue
}
n.ID = ids[i]
n.SetIALAttr("id", n.ID)
n.SetIALAttr("updated", util.TimeFromID(n.ID))
i++
return ast.WalkContinue
})
tree.ID = tree.Root.ID
tree.Path = path.Join(path.Dir(tree.Path), tree.ID+".sy")
tree.Root.SetIALAttr("id", tree.Root.ID)
}
func newID(t string) string {
return t + "-" + randStr(7)
}
func randStr(length int) string {
letter := []rune("abcdefghijklmnopqrstuvwxyz0123456789")
b := make([]rune, length)
for i := range b {
b[i] = letter[rand.Intn(len(letter))]
}
return string(b)
}

391
kernel/model/index.go Normal file
View file

@ -0,0 +1,391 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"crypto/sha256"
"fmt"
"runtime/debug"
"sort"
"strings"
"time"
"unicode/utf8"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/dustin/go-humanize"
"github.com/emirpasic/gods/sets/hashset"
"github.com/siyuan-note/siyuan/kernel/cache"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func (box *Box) BootIndex() {
util.SetBootDetails("Listing files...")
files := box.ListFiles("/")
boxLen := len(Conf.GetOpenedBoxes())
if 1 > boxLen {
boxLen = 1
}
bootProgressPart := 10.0 / float64(boxLen) / float64(len(files))
luteEngine := NewLute()
i := 0
// 读取并缓存路径映射
for _, file := range files {
if file.isdir || !strings.HasSuffix(file.name, ".sy") {
continue
}
p := file.path
tree, err := filesys.LoadTree(box.ID, p, luteEngine)
if nil != err {
util.LogErrorf("read box [%s] tree [%s] failed: %s", box.ID, p, err)
continue
}
docIAL := parse.IAL2MapUnEsc(tree.Root.KramdownIAL)
cache.PutDocIAL(p, docIAL)
util.IncBootProgress(bootProgressPart, "Parsing tree "+util.ShortPathForBootingDisplay(tree.Path))
// 缓存块树
treenode.IndexBlockTree(tree)
if 1 < i && 0 == i%64 {
filesys.ReleaseAllFileLocks()
}
i++
}
return
}
func (box *Box) Index(fullRebuildIndex bool) (treeCount int, treeSize int64) {
defer debug.FreeOSMemory()
sql.IndexMode()
defer sql.NormalMode()
//os.MkdirAll("pprof", 0755)
//cpuProfile, _ := os.Create("pprof/cpu_profile_index")
//pprof.StartCPUProfile(cpuProfile)
//defer pprof.StopCPUProfile()
util.SetBootDetails("Listing files...")
files := box.ListFiles("/")
boxLen := len(Conf.GetOpenedBoxes())
if 1 > boxLen {
boxLen = 1
}
bootProgressPart := 10.0 / float64(boxLen) / float64(len(files))
luteEngine := NewLute()
idTitleMap := map[string]string{}
idHashMap := map[string]string{}
util.PushEndlessProgress(fmt.Sprintf("["+box.Name+"] "+Conf.Language(64), len(files)))
i := 0
// 读取并缓存路径映射
for _, file := range files {
if file.isdir || !strings.HasSuffix(file.name, ".sy") {
continue
}
p := file.path
tree, err := filesys.LoadTree(box.ID, p, luteEngine)
if nil != err {
util.LogErrorf("read box [%s] tree [%s] failed: %s", box.ID, p, err)
continue
}
docIAL := parse.IAL2MapUnEsc(tree.Root.KramdownIAL)
cache.PutDocIAL(p, docIAL)
util.IncBootProgress(bootProgressPart, "Parsing tree "+util.ShortPathForBootingDisplay(tree.Path))
treeSize += file.size
treeCount++
// 缓存文档标题,后面做 Path -> HPath 路径映射时需要
idTitleMap[tree.ID] = tree.Root.IALAttr("title")
// 缓存块树
treenode.IndexBlockTree(tree)
// 缓存 ID-Hash后面需要用于判断是否要重建库
idHashMap[tree.ID] = tree.Hash
if 1 < i && 0 == i%64 {
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(88), i, len(files)-i))
filesys.ReleaseAllFileLocks()
}
i++
}
box.UpdateHistoryGenerated() // 初始化历史生成时间为当前时间
// 检查是否需要重新建库
util.SetBootDetails("Checking data hashes...")
var ids []string
for id := range idTitleMap {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] >= ids[j] })
buf := bytes.Buffer{}
for _, id := range ids {
hash, _ := idHashMap[id]
buf.WriteString(hash)
util.SetBootDetails("Checking hash " + hash)
}
boxHash := fmt.Sprintf("%x", sha256.Sum256(buf.Bytes()))
dbBoxHash := sql.GetBoxHash(box.ID)
if boxHash == dbBoxHash {
//util.LogInfof("use existing database for box [%s]", box.ID)
util.SetBootDetails("Use existing database for notebook " + box.ID)
return
}
// 开始重建库
sql.DisableCache()
defer sql.EnableCache()
start := time.Now()
if !fullRebuildIndex {
tx, err := sql.BeginTx()
if nil != err {
return
}
sql.PutBoxHash(tx, box.ID, boxHash)
util.SetBootDetails("Cleaning obsolete indexes...")
util.PushEndlessProgress(Conf.Language(108))
if err = sql.DeleteByBoxTx(tx, box.ID); nil != err {
return
}
if err = sql.CommitTx(tx); nil != err {
return
}
}
bootProgressPart = 40.0 / float64(boxLen) / float64(treeCount)
i = 0
// 块级行级入库,缓存块
// 这里不能并行插入,因为 SQLite 不支持
for _, file := range files {
if file.isdir || !strings.HasSuffix(file.name, ".sy") {
continue
}
tree, err := filesys.LoadTree(box.ID, file.path, luteEngine)
if nil != err {
util.LogErrorf("read box [%s] tree [%s] failed: %s", box.ID, file.path, err)
continue
}
util.IncBootProgress(bootProgressPart, "Indexing tree "+util.ShortPathForBootingDisplay(tree.Path))
tx, err := sql.BeginTx()
if nil != err {
continue
}
if err = sql.InsertBlocksSpans(tx, tree); nil != err {
continue
}
if err = sql.CommitTx(tx); nil != err {
continue
}
if 1 < i && 0 == i%64 {
util.PushEndlessProgress(fmt.Sprintf("["+box.Name+"] "+Conf.Language(53), i, treeCount-i))
filesys.ReleaseAllFileLocks()
}
i++
}
end := time.Now()
elapsed := end.Sub(start).Seconds()
util.LogInfof("rebuilt database for notebook [%s] in [%.2fs], tree [count=%d, size=%s]", box.ID, elapsed, treeCount, humanize.Bytes(uint64(treeSize)))
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(56), treeCount))
return
}
func IndexRefs() {
sql.EnableCache()
defer sql.ClearBlockCache()
start := time.Now()
util.SetBootDetails("Resolving refs...")
util.PushEndlessProgress(Conf.Language(54))
// 解析并更新引用块
util.SetBootDetails("Resolving ref block content...")
refUnresolvedBlocks := sql.GetRefUnresolvedBlocks() // TODO: v2.2.0 以后移除
if 0 < len(refUnresolvedBlocks) {
dynamicRefTreeIDs := hashset.New()
bootProgressPart := 10.0 / float64(len(refUnresolvedBlocks))
anchors := map[string]string{}
var refBlockIDs []string
for i, refBlock := range refUnresolvedBlocks {
util.IncBootProgress(bootProgressPart, "Resolving ref block content "+util.ShortPathForBootingDisplay(refBlock.ID))
tx, err := sql.BeginTx()
if nil != err {
return
}
blockContent := sql.ResolveRefContent(refBlock, &anchors)
refBlock.Content = blockContent
refBlockIDs = append(refBlockIDs, refBlock.ID)
dynamicRefTreeIDs.Add(refBlock.RootID)
sql.CommitTx(tx)
if 1 < i && 0 == i%64 {
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(53), i, len(refUnresolvedBlocks)-i))
}
}
// 将需要更新动态引用文本内容的块先删除,后面会重新插入,这样比直接 update 快很多
util.SetBootDetails("Deleting unresolved block content...")
tx, err := sql.BeginTx()
if nil != err {
return
}
sql.DeleteBlockByIDs(tx, refBlockIDs)
sql.CommitTx(tx)
bootProgressPart = 10.0 / float64(len(refUnresolvedBlocks))
for i, refBlock := range refUnresolvedBlocks {
util.IncBootProgress(bootProgressPart, "Updating block content "+util.ShortPathForBootingDisplay(refBlock.ID))
tx, err = sql.BeginTx()
if nil != err {
return
}
sql.InsertBlock(tx, refBlock)
sql.CommitTx(tx)
if 1 < i && 0 == i%64 {
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(53), i, len(refUnresolvedBlocks)-i))
}
}
if 0 < dynamicRefTreeIDs.Size() {
// 块引锚文本静态化
for _, dynamicRefTreeIDVal := range dynamicRefTreeIDs.Values() {
dynamicRefTreeID := dynamicRefTreeIDVal.(string)
util.IncBootProgress(bootProgressPart, "Persisting block ref text "+util.ShortPathForBootingDisplay(dynamicRefTreeID))
tree, err := loadTreeByBlockID(dynamicRefTreeID)
if nil != err {
util.LogErrorf("tree [%s] dynamic ref text to static failed: %s", dynamicRefTreeID, err)
continue
}
legacyDynamicRefTreeToStatic(tree)
if err := filesys.WriteTree(tree); nil == err {
//util.LogInfof("persisted tree [%s] dynamic ref text", tree.Box+tree.Path)
}
}
}
}
// 引用入库
util.SetBootDetails("Indexing refs...")
refBlocks := sql.GetRefExistedBlocks()
refTreeIDs := hashset.New()
for _, refBlock := range refBlocks {
refTreeIDs.Add(refBlock.RootID)
}
if 0 < refTreeIDs.Size() {
luteEngine := NewLute()
bootProgressPart := 10.0 / float64(refTreeIDs.Size())
for _, box := range Conf.GetOpenedBoxes() {
tx, err := sql.BeginTx()
if nil != err {
return
}
sql.DeleteRefsByBoxTx(tx, box.ID)
sql.CommitTx(tx)
files := box.ListFiles("/")
i := 0
for _, file := range files {
if file.isdir || !strings.HasSuffix(file.name, ".sy") {
continue
}
if file.isdir || !strings.HasSuffix(file.name, ".sy") {
continue
}
id := strings.TrimSuffix(file.name, ".sy")
if !refTreeIDs.Contains(id) {
continue
}
util.IncBootProgress(bootProgressPart, "Indexing ref "+util.ShortPathForBootingDisplay(file.path))
tree, err := filesys.LoadTree(box.ID, file.path, luteEngine)
if nil != err {
util.LogErrorf("parse box [%s] tree [%s] failed", box.ID, file.path)
continue
}
tx, err = sql.BeginTx()
if nil != err {
continue
}
sql.InsertRefs(tx, tree)
if err = sql.CommitTx(tx); nil != err {
continue
}
if 1 < i && 0 == i%64 {
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(55), i))
filesys.ReleaseAllFileLocks()
}
i++
}
}
}
util.LogInfof("resolved refs [%d] in [%dms]", len(refBlocks), time.Now().Sub(start).Milliseconds())
}
func legacyDynamicRefTreeToStatic(tree *parse.Tree) {
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || ast.NodeBlockRef != n.Type {
return ast.WalkContinue
}
if isLegacyDynamicBlockRef(n) {
idNode := n.ChildByType(ast.NodeBlockRefID)
defID := idNode.TokensStr()
def := sql.GetBlock(defID)
var text string
if nil == def {
if "zh_CN" == Conf.Lang {
text = "解析引用锚文本失败,请尝试更新该引用指向的定义块后再重新打开该文档"
} else {
text = "Failed to parse the ref anchor text, please try to update the def block pointed to by the ref and then reopen this document"
}
} else {
text = sql.GetRefText(defID)
}
if Conf.Editor.BlockRefDynamicAnchorTextMaxLen < utf8.RuneCountInString(text) {
text = gulu.Str.SubStr(text, Conf.Editor.BlockRefDynamicAnchorTextMaxLen) + "..."
}
treenode.SetDynamicBlockRefText(n, text)
return ast.WalkSkipChildren
}
return ast.WalkContinue
})
}
func isLegacyDynamicBlockRef(blockRef *ast.Node) bool {
return nil == blockRef.ChildByType(ast.NodeBlockRefText) && nil == blockRef.ChildByType(ast.NodeBlockRefDynamicText)
}

412
kernel/model/liandi.go Normal file
View file

@ -0,0 +1,412 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"encoding/hex"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"github.com/88250/gulu"
"github.com/siyuan-note/siyuan/kernel/conf"
"github.com/siyuan-note/siyuan/kernel/util"
)
var ErrFailedToConnectCloudServer = errors.New("failed to connect cloud server")
func DeactivateUser() (err error) {
requestResult := gulu.Ret.NewResult()
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
resp, err := request.
SetResult(requestResult).
SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
Post(util.AliyunServer + "/apis/siyuan/user/deactivate")
if nil != err {
util.LogErrorf("deactivate user failed: %s", err)
return ErrFailedToConnectCloudServer
}
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
if 0 != requestResult.Code {
util.LogErrorf("deactivate user failed: %s", requestResult.Msg)
return errors.New(requestResult.Msg)
}
return
}
func SetCloudBlockReminder(id, data string, timed int64) (err error) {
requestResult := gulu.Ret.NewResult()
payload := map[string]interface{}{"dataId": id, "data": data, "timed": timed}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
resp, err := request.
SetResult(requestResult).
SetBody(payload).
SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
Post(util.AliyunServer + "/apis/siyuan/calendar/setBlockReminder")
if nil != err {
util.LogErrorf("set block reminder failed: %s", err)
return ErrFailedToConnectCloudServer
}
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
if 0 != requestResult.Code {
util.LogErrorf("set block reminder failed: %s", requestResult.Msg)
return errors.New(requestResult.Msg)
}
return
}
var uploadToken = ""
var uploadTokenTime int64
func LoadUploadToken() (err error) {
now := time.Now().Unix()
if 3600 >= now-uploadTokenTime {
return
}
requestResult := gulu.Ret.NewResult()
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
resp, err := request.
SetResult(requestResult).
SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
Post(util.AliyunServer + "/apis/siyuan/upload/token")
if nil != err {
util.LogErrorf("get upload token failed: %s", err)
return ErrFailedToConnectCloudServer
}
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
if 0 != requestResult.Code {
util.LogErrorf("get upload token failed: %s", requestResult.Msg)
return
}
resultData := requestResult.Data.(map[string]interface{})
uploadToken = resultData["uploadToken"].(string)
uploadTokenTime = now
return
}
var (
refreshUserTicker = time.NewTicker(30 * time.Minute)
subscriptionExpirationReminded bool
)
func AutoRefreshUser() {
for {
if !subscriptionExpirationReminded {
subscriptionExpirationReminded = true
go func() {
if "ios" == util.Container {
return
}
if IsSubscriber() && -1 != Conf.User.UserSiYuanProExpireTime {
expired := int64(Conf.User.UserSiYuanProExpireTime)
if time.Now().UnixMilli() >= expired { // 已经过期
time.Sleep(time.Second * 30)
util.PushErrMsg(Conf.Language(128), 0)
return
}
remains := (expired - time.Now().Add(24*time.Hour*15).UnixMilli()) / 1000 / 60 / 60 / 24
if 0 <= remains && 15 > remains { // 15 后过期
time.Sleep(3 * time.Minute)
util.PushErrMsg(fmt.Sprintf(Conf.Language(127), remains), 0)
return
}
}
}()
}
if nil != Conf.User {
time.Sleep(3 * time.Minute)
RefreshUser(Conf.User.UserToken)
subscriptionExpirationReminded = false
}
<-refreshUserTicker.C
}
}
func RefreshUser(token string) error {
threeDaysAfter := util.CurrentTimeMillis() + 1000*60*60*24*3
if "" == token {
if "" != Conf.UserData {
Conf.User = loadUserFromConf()
}
if nil == Conf.User {
return errors.New(Conf.Language(19))
}
var tokenExpireTime int64
tokenExpireTime, err := strconv.ParseInt(Conf.User.UserTokenExpireTime+"000", 10, 64)
if nil != err {
util.LogErrorf("convert token expire time [%s] failed: %s", Conf.User.UserTokenExpireTime, err)
return errors.New(Conf.Language(19))
}
if threeDaysAfter > tokenExpireTime {
token = Conf.User.UserToken
goto Net
}
return nil
}
Net:
start := time.Now()
user, err := getUser(token)
if err != nil {
if nil == Conf.User || errInvalidUser == err {
return errors.New(Conf.Language(19))
}
var tokenExpireTime int64
tokenExpireTime, err = strconv.ParseInt(Conf.User.UserTokenExpireTime+"000", 10, 64)
if nil != err {
util.LogErrorf("convert token expire time [%s] failed: %s", Conf.User.UserTokenExpireTime, err)
return errors.New(Conf.Language(19))
}
if threeDaysAfter > tokenExpireTime {
return errors.New(Conf.Language(19))
}
return nil
}
Conf.User = user
data, _ := gulu.JSON.MarshalJSON(user)
Conf.UserData = util.AESEncrypt(string(data))
Conf.Save()
if elapsed := time.Now().Sub(start).Milliseconds(); 3000 < elapsed {
util.LogInfof("get cloud user elapsed [%dms]", elapsed)
}
return nil
}
func loadUserFromConf() *conf.User {
if "" == Conf.UserData {
return nil
}
data := util.AESDecrypt(Conf.UserData)
data, _ = hex.DecodeString(string(data))
user := &conf.User{}
if err := gulu.JSON.UnmarshalJSON(data, &user); nil == err {
return user
}
return nil
}
func RemoveCloudShorthands(ids []string) (err error) {
result := map[string]interface{}{}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
body := map[string]interface{}{
"ids": ids,
}
resp, err := request.
SetResult(&result).
SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
SetBody(body).
Post(util.AliyunServer + "/apis/siyuan/inbox/removeCloudShorthands")
if nil != err {
util.LogErrorf("remove cloud shorthands failed: %s", err)
err = ErrFailedToConnectCloudServer
return
}
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
code := result["code"].(float64)
if 0 != code {
util.LogErrorf("remove cloud shorthands failed: %s", result["msg"])
err = errors.New(result["msg"].(string))
return
}
return
}
func GetCloudShorthands(page int) (result map[string]interface{}, err error) {
result = map[string]interface{}{}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
resp, err := request.
SetResult(&result).
SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
Post(util.AliyunServer + "/apis/siyuan/inbox/getCloudShorthands?p=" + strconv.Itoa(page))
if nil != err {
util.LogErrorf("get cloud shorthands failed: %s", err)
err = ErrFailedToConnectCloudServer
return
}
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
code := result["code"].(float64)
if 0 != code {
util.LogErrorf("get cloud shorthands failed: %s", result["msg"])
err = errors.New(result["msg"].(string))
return
}
shorthands := result["data"].(map[string]interface{})["shorthands"].([]interface{})
for _, item := range shorthands {
shorthand := item.(map[string]interface{})
id := shorthand["oId"].(string)
t, _ := strconv.ParseInt(id, 10, 64)
hCreated := util.Millisecond2Time(t)
shorthand["hCreated"] = hCreated.Format("2006-01-02 15:04")
}
return
}
var errInvalidUser = errors.New("invalid user")
func getUser(token string) (*conf.User, error) {
result := map[string]interface{}{}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
_, err := request.
SetResult(&result).
SetBody(map[string]string{"token": token}).
Post(util.AliyunServer + "/apis/siyuan/user")
if nil != err {
util.LogErrorf("get community user failed: %s", err)
return nil, errors.New(Conf.Language(18))
}
code := result["code"].(float64)
if 0 != code {
if 255 == code {
return nil, errInvalidUser
}
util.LogErrorf("get community user failed: %s", result["msg"])
return nil, errors.New(Conf.Language(18))
}
dataStr := result["data"].(string)
data := util.AESDecrypt(dataStr)
user := &conf.User{}
if err = gulu.JSON.UnmarshalJSON(data, &user); nil != err {
util.LogErrorf("get community user failed: %s", err)
return nil, errors.New(Conf.Language(18))
}
return user, nil
}
func UseActivationcode(code string) (err error) {
requestResult := gulu.Ret.NewResult()
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
_, err = request.
SetResult(requestResult).
SetBody(map[string]string{"data": code}).
SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
Post(util.AliyunServer + "/apis/siyuan/useActivationcode")
if nil != err {
util.LogErrorf("check activation code failed: %s", err)
return ErrFailedToConnectCloudServer
}
if 0 != requestResult.Code {
return errors.New(requestResult.Msg)
}
return
}
func CheckActivationcode(code string) (retCode int, msg string) {
retCode = 1
requestResult := gulu.Ret.NewResult()
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
_, err := request.
SetResult(requestResult).
SetBody(map[string]string{"data": code}).
SetCookies(&http.Cookie{Name: "symphony", Value: Conf.User.UserToken}).
Post(util.AliyunServer + "/apis/siyuan/checkActivationcode")
if nil != err {
util.LogErrorf("check activation code failed: %s", err)
msg = ErrFailedToConnectCloudServer.Error()
return
}
if 0 == requestResult.Code {
retCode = 0
}
msg = requestResult.Msg
return
}
func Login(userName, password, captcha string) (ret *gulu.Result, err error) {
result := map[string]interface{}{}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
_, err = request.
SetResult(&result).
SetBody(map[string]string{"userName": userName, "userPassword": password, "captcha": captcha}).
Post(util.AliyunServer + "/apis/siyuan/login")
if nil != err {
util.LogErrorf("login failed: %s", err)
return nil, errors.New(Conf.Language(18))
}
ret = &gulu.Result{
Code: int(result["code"].(float64)),
Msg: result["msg"].(string),
Data: map[string]interface{}{
"userName": result["userName"],
"token": result["token"],
"needCaptcha": result["needCaptcha"],
},
}
if -1 == ret.Code {
ret.Code = 1
}
return
}
func Login2fa(token, code string) (map[string]interface{}, error) {
result := map[string]interface{}{}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
_, err := request.
SetResult(&result).
SetBody(map[string]string{"twofactorAuthCode": code}).
SetHeader("token", token).
Post(util.AliyunServer + "/apis/siyuan/login/2fa")
if nil != err {
util.LogErrorf("login 2fa failed: %s", err)
return nil, errors.New(Conf.Language(18))
}
return result, nil
}
func LogoutUser() {
Conf.UserData = ""
Conf.User = nil
Conf.Save()
}

112
kernel/model/listitem.go Normal file
View file

@ -0,0 +1,112 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"path"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/88250/protyle"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func ListItem2Doc(srcListItemID, targetBoxID, targetPath string) (srcRootBlockID, newTargetPath string, err error) {
WaitForWritingFiles()
srcTree, _ := loadTreeByBlockID(srcListItemID)
if nil == srcTree {
err = ErrBlockNotFound
return
}
srcRootBlockID = srcTree.Root.ID
listItemNode := treenode.GetNodeInTree(srcTree, srcListItemID)
if nil == listItemNode {
err = ErrBlockNotFound
return
}
box := Conf.Box(targetBoxID)
listItemText := sql.GetContainerText(listItemNode)
listItemText = util.FilterFileName(listItemText)
moveToRoot := "/" == targetPath
toHP := path.Join("/", listItemText)
toFolder := "/"
if !moveToRoot {
toBlock := treenode.GetBlockTreeRootByPath(targetBoxID, targetPath)
if nil == toBlock {
err = ErrBlockNotFound
return
}
toHP = path.Join(toBlock.HPath, listItemText)
toFolder = path.Join(path.Dir(targetPath), toBlock.ID)
}
newTargetPath = path.Join(toFolder, srcListItemID+".sy")
if !box.Exist(toFolder) {
if err = box.MkdirAll(toFolder); nil != err {
return
}
}
var children []*ast.Node
for c := listItemNode.FirstChild.Next; nil != c; c = c.Next {
children = append(children, c)
}
if 1 > len(children) {
newNode := protyle.NewParagraph()
children = append(children, newNode)
}
luteEngine := NewLute()
newTree := &parse.Tree{Root: &ast.Node{Type: ast.NodeDocument, ID: srcListItemID}, Context: &parse.Context{ParseOption: luteEngine.ParseOptions}}
for _, c := range children {
newTree.Root.AppendChild(c)
}
newTree.ID = srcListItemID
newTree.Path = newTargetPath
newTree.HPath = toHP
listItemNode.SetIALAttr("type", "doc")
listItemNode.SetIALAttr("id", srcListItemID)
listItemNode.SetIALAttr("title", listItemText)
newTree.Root.KramdownIAL = listItemNode.KramdownIAL
srcLiParent := listItemNode.Parent
listItemNode.Unlink()
if nil != srcLiParent && nil == srcLiParent.FirstChild {
srcLiParent.Unlink()
}
srcTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
if err = indexWriteJSONQueue(srcTree); nil != err {
return "", "", err
}
newTree.Box, newTree.Path = targetBoxID, newTargetPath
newTree.Root.SetIALAttr("updated", util.CurrentTimeSecondsStr())
if err = indexWriteJSONQueue(newTree); nil != err {
return "", "", err
}
IncWorkspaceDataVer()
RefreshBacklink(srcTree.ID)
RefreshBacklink(newTree.ID)
return
}

214
kernel/model/mount.go Normal file
View file

@ -0,0 +1,214 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"errors"
"fmt"
"os"
"path/filepath"
"runtime/debug"
"strings"
"time"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func CreateBox(name string) (id string, err error) {
WaitForWritingFiles()
syncLock.Lock()
defer syncLock.Unlock()
id = ast.NewNodeID()
boxLocalPath := filepath.Join(util.DataDir, id)
err = os.MkdirAll(boxLocalPath, 0755)
if nil != err {
return
}
box := &Box{ID: id, Name: name}
boxConf := box.GetConf()
boxConf.Name = name
box.SaveConf(boxConf)
IncWorkspaceDataVer()
return
}
func RenameBox(boxID, name string) (err error) {
WaitForWritingFiles()
syncLock.Lock()
defer syncLock.Unlock()
box := Conf.Box(boxID)
if nil == box {
return errors.New(Conf.Language(0))
}
boxConf := box.GetConf()
boxConf.Name = name
box.Name = name
box.SaveConf(boxConf)
IncWorkspaceDataVer()
return
}
func RemoveBox(boxID string) (err error) {
WaitForWritingFiles()
syncLock.Lock()
defer syncLock.Unlock()
if util.IsReservedFilename(boxID) {
return errors.New(fmt.Sprintf("can not remove [%s] caused by it is a reserved file", boxID))
}
localPath := filepath.Join(util.DataDir, boxID)
if !gulu.File.IsExist(localPath) {
return
}
if !gulu.File.IsDir(localPath) {
return errors.New(fmt.Sprintf("can not remove [%s] caused by it is not a dir", boxID))
}
filesys.ReleaseFileLocks(localPath)
if !isUserGuide(boxID) {
var historyDir string
historyDir, err = util.GetHistoryDir("delete")
if nil != err {
util.LogErrorf("get history dir failed: %s", err)
return
}
p := strings.TrimPrefix(localPath, util.DataDir)
historyPath := filepath.Join(historyDir, p)
if err = gulu.File.Copy(localPath, historyPath); nil != err {
util.LogErrorf("gen sync history failed: %s", err)
return
}
copyBoxAssetsToDataAssets(boxID)
}
unmount0(boxID)
if err = os.RemoveAll(localPath); nil != err {
return
}
IncWorkspaceDataVer()
return
}
func Unmount(boxID string) {
WaitForWritingFiles()
syncLock.Lock()
defer syncLock.Unlock()
unmount0(boxID)
evt := util.NewCmdResult("unmount", 0, util.PushModeBroadcast, 0)
evt.Data = map[string]interface{}{
"box": boxID,
}
util.PushEvent(evt)
}
func unmount0(boxID string) {
for _, box := range Conf.GetOpenedBoxes() {
if box.ID == boxID {
boxConf := box.GetConf()
boxConf.Closed = true
box.SaveConf(boxConf)
box.Unindex()
debug.FreeOSMemory()
return
}
}
}
func Mount(boxID string) (alreadyMount bool, err error) {
WaitForWritingFiles()
syncLock.Lock()
defer syncLock.Unlock()
localPath := filepath.Join(util.DataDir, boxID)
var reMountGuide bool
if isUserGuide(boxID) {
// 重新挂载帮助文档
guideBox := Conf.Box(boxID)
if nil != guideBox {
unmount0(guideBox.ID)
reMountGuide = true
}
if err = os.RemoveAll(localPath); nil != err {
return
}
p := filepath.Join(util.WorkingDir, "guide", boxID)
if err = gulu.File.Copy(p, localPath); nil != err {
return
}
if box := Conf.Box(boxID); nil != box {
boxConf := box.GetConf()
boxConf.Closed = true
box.SaveConf(boxConf)
}
if Conf.Newbie {
Conf.Newbie = false
Conf.Save()
}
go func() {
time.Sleep(time.Second * 5)
util.PushErrMsg(Conf.Language(52), 9000)
}()
}
if !gulu.File.IsDir(localPath) {
return false, errors.New("can not open file, just support open folder only")
}
for _, box := range Conf.GetOpenedBoxes() {
if box.ID == boxID {
return true, nil
}
}
box := &Box{ID: boxID}
boxConf := box.GetConf()
boxConf.Closed = false
box.SaveConf(boxConf)
box.Index(false)
IndexRefs()
// 缓存根一级的文档树展开
ListDocTree(box.ID, "/", Conf.FileTree.Sort)
treenode.SaveBlockTree()
util.ClearPushProgress(100)
if reMountGuide {
return true, nil
}
return false, nil
}
func isUserGuide(boxID string) bool {
return "20210808180117-czj9bvb" == boxID || "20210808180117-6v0mkxr" == boxID || "20211226090932-5lcq56f" == boxID
}

821
kernel/model/osssync.go Normal file
View file

@ -0,0 +1,821 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"context"
"crypto/sha1"
"encoding/base64"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/88250/gulu"
"github.com/panjf2000/ants/v2"
"github.com/qiniu/go-sdk/v7/storage"
"github.com/siyuan-note/siyuan/kernel/util"
)
func getCloudSpaceOSS() (sync, backup map[string]interface{}, assetSize int64, err error) {
result := map[string]interface{}{}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
resp, err := request.
SetResult(&result).
SetBody(map[string]string{"token": Conf.User.UserToken}).
Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanWorkspace")
if nil != err {
util.LogErrorf("get cloud space failed: %s", err)
return nil, nil, 0, ErrFailedToConnectCloudServer
}
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
code := result["code"].(float64)
if 0 != code {
util.LogErrorf("get cloud space failed: %s", result["msg"])
return nil, nil, 0, errors.New(result["msg"].(string))
}
data := result["data"].(map[string]interface{})
sync = data["sync"].(map[string]interface{})
backup = data["backup"].(map[string]interface{})
assetSize = int64(data["assetSize"].(float64))
return
}
func removeCloudDirPath(dirPath string) (err error) {
result := map[string]interface{}{}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
resp, err := request.
SetResult(&result).
SetBody(map[string]string{"dirPath": dirPath, "token": Conf.User.UserToken}).
Post(util.AliyunServer + "/apis/siyuan/data/removeSiYuanDirPath")
if nil != err {
util.LogErrorf("create cloud sync dir failed: %s", err)
return ErrFailedToConnectCloudServer
}
if 200 != resp.StatusCode {
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
msg := fmt.Sprintf("remove cloud dir failed: %d", resp.StatusCode)
util.LogErrorf(msg)
err = errors.New(msg)
return
}
return
}
func createCloudSyncDirOSS(name string) (err error) {
result := map[string]interface{}{}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
resp, err := request.
SetResult(&result).
SetBody(map[string]string{"name": name, "token": Conf.User.UserToken}).
Post(util.AliyunServer + "/apis/siyuan/data/createSiYuanSyncDir")
if nil != err {
util.LogErrorf("create cloud sync dir failed: %s", err)
return ErrFailedToConnectCloudServer
}
if 200 != resp.StatusCode {
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
msg := fmt.Sprintf("create cloud sync dir failed: %d", resp.StatusCode)
util.LogErrorf(msg)
err = errors.New(msg)
return
}
code := result["code"].(float64)
if 0 != code {
util.LogErrorf("create cloud sync dir failed: %s", result["msg"])
return errors.New(result["msg"].(string))
}
return
}
func listCloudSyncDirOSS() (dirs []map[string]interface{}, size int64, err error) {
result := map[string]interface{}{}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
resp, err := request.
SetBody(map[string]interface{}{"token": Conf.User.UserToken}).
SetResult(&result).
Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanSyncDirList?uid=" + Conf.User.UserId)
if nil != err {
util.LogErrorf("get cloud sync dirs failed: %s", err)
return nil, 0, ErrFailedToConnectCloudServer
}
if 200 != resp.StatusCode {
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
msg := fmt.Sprintf("get cloud sync dirs failed: %d", resp.StatusCode)
util.LogErrorf(msg)
err = errors.New(msg)
return
}
code := result["code"].(float64)
if 0 != code {
util.LogErrorf("get cloud sync dirs failed: %s", result["msg"])
return nil, 0, ErrFailedToConnectCloudServer
}
data := result["data"].(map[string]interface{})
dataDirs := data["dirs"].([]interface{})
for _, d := range dataDirs {
dirs = append(dirs, d.(map[string]interface{}))
}
sort.Slice(dirs, func(i, j int) bool { return dirs[i]["name"].(string) < dirs[j]["name"].(string) })
size = int64(data["size"].(float64))
return
}
func ossDownload(localDirPath, cloudDirPath string, bootOrExit bool) (fetchedFiles int, transferSize uint64, err error) {
if !gulu.File.IsExist(localDirPath) {
return
}
cloudFileList, err := getCloudFileListOSS(cloudDirPath)
if nil != err {
return
}
localRemoves, cloudFetches, err := localUpsertRemoveListOSS(localDirPath, cloudFileList)
if nil != err {
return
}
for _, localRemove := range localRemoves {
if err = os.RemoveAll(localRemove); nil != err {
util.LogErrorf("local remove [%s] failed: %s", localRemove, err)
return
}
}
needPushProgress := 32 < len(cloudFetches)
waitGroup := &sync.WaitGroup{}
var downloadErr error
poolSize := 4
if poolSize > len(cloudFetches) {
poolSize = len(cloudFetches)
}
p, _ := ants.NewPoolWithFunc(poolSize, func(arg interface{}) {
defer waitGroup.Done()
if nil != downloadErr {
return // 快速失败
}
fetch := arg.(string)
err = ossDownload0(localDirPath, cloudDirPath, fetch, &fetchedFiles, &transferSize, bootOrExit)
if nil != err {
downloadErr = err
return
}
if needPushProgress {
msg := fmt.Sprintf(Conf.Language(103), fetchedFiles, len(cloudFetches)-fetchedFiles)
util.PushProgress(util.PushProgressCodeProgressed, fetchedFiles, len(cloudFetches), msg)
}
if bootOrExit {
msg := fmt.Sprintf("Downloading data from the cloud %d/%d", fetchedFiles, len(cloudFetches))
util.IncBootProgress(0, msg)
}
})
for _, fetch := range cloudFetches {
waitGroup.Add(1)
p.Invoke(fetch)
}
waitGroup.Wait()
p.Release()
if nil != downloadErr {
err = downloadErr
return
}
if needPushProgress {
util.ClearPushProgress(len(cloudFetches))
util.PushMsg(Conf.Language(106), 1000*60*10)
}
if bootOrExit {
util.IncBootProgress(0, "Decrypting from sync to data...")
}
return
}
func ossDownload0(localDirPath, cloudDirPath, fetch string, fetchedFiles *int, transferSize *uint64, bootORExit bool) (err error) {
localFilePath := filepath.Join(localDirPath, fetch)
remoteFileURL := path.Join(cloudDirPath, fetch)
var result map[string]interface{}
resp, err := util.NewCloudRequest(Conf.System.NetworkProxy.String()).
SetResult(&result).
SetBody(map[string]interface{}{"token": Conf.User.UserToken, "path": remoteFileURL}).
Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanFile?uid=" + Conf.User.UserId)
if nil != err {
util.LogErrorf("download request [%s] failed: %s", remoteFileURL, err)
return errors.New(fmt.Sprintf(Conf.Language(93), err))
}
if 200 != resp.StatusCode {
if 401 == resp.StatusCode {
err = errors.New("account authentication failed, please login again")
return errors.New(fmt.Sprintf(Conf.Language(93), err))
}
util.LogErrorf("download request status code [%d]", resp.StatusCode)
err = errors.New("download file URL failed")
return errors.New(fmt.Sprintf(Conf.Language(93), err))
}
code := result["code"].(float64)
if 0 != code {
msg := result["msg"].(string)
util.LogErrorf("download cloud file failed: %s", msg)
return errors.New(fmt.Sprintf(Conf.Language(93), msg))
}
resultData := result["data"].(map[string]interface{})
downloadURL := resultData["url"].(string)
if err = os.MkdirAll(filepath.Dir(localFilePath), 0755); nil != err {
return
}
os.Remove(localFilePath)
if bootORExit {
resp, err = util.NewCloudFileRequest15s(Conf.System.NetworkProxy.String()).Get(downloadURL)
} else {
resp, err = util.NewCloudFileRequest2m(Conf.System.NetworkProxy.String()).Get(downloadURL)
}
if nil != err {
util.LogErrorf("download request [%s] failed: %s", downloadURL, err)
return errors.New(fmt.Sprintf(Conf.Language(93), err))
}
if 200 != resp.StatusCode {
util.LogErrorf("download request [%s] status code [%d]", downloadURL, resp.StatusCode)
err = errors.New(fmt.Sprintf("download file failed [%d]", resp.StatusCode))
if 404 == resp.StatusCode {
err = errors.New(Conf.Language(135))
}
return errors.New(fmt.Sprintf(Conf.Language(93), err))
}
data, err := resp.ToBytes()
if nil != err {
util.LogErrorf("download read response body data failed: %s, %s", err, string(data))
err = errors.New("download read data failed")
return errors.New(fmt.Sprintf(Conf.Language(93), err))
}
size := int64(len(data))
if err = gulu.File.WriteFileSafer(localFilePath, data, 0644); nil != err {
util.LogErrorf("write file [%s] failed: %s", localFilePath, err)
return errors.New(fmt.Sprintf(Conf.Language(93), err))
}
*fetchedFiles++
*transferSize += uint64(size)
return
}
func ossUpload(localDirPath, cloudDirPath, cloudDevice string, boot bool) (wroteFiles int, transferSize uint64, err error) {
if !gulu.File.IsExist(localDirPath) {
return
}
var cloudFileList map[string]*CloudIndex
localDevice := Conf.System.ID
if "" != localDevice && localDevice == cloudDevice {
//util.LogInfof("cloud device is the same as local device, get index from local")
cloudFileList, err = getLocalFileListOSS(cloudDirPath)
if nil != err {
util.LogInfof("get local index failed [%s], get index from cloud", err)
cloudFileList, err = getCloudFileListOSS(cloudDirPath)
}
} else {
cloudFileList, err = getCloudFileListOSS(cloudDirPath)
}
if nil != err {
return
}
localUpserts, cloudRemoves, err := cloudUpsertRemoveListOSS(localDirPath, cloudFileList)
if nil != err {
return
}
needPushProgress := 32 < len(localUpserts)
waitGroup := &sync.WaitGroup{}
var uploadErr error
poolSize := 4
if poolSize > len(localUpserts) {
poolSize = len(localUpserts)
}
p, _ := ants.NewPoolWithFunc(poolSize, func(arg interface{}) {
defer waitGroup.Done()
if nil != uploadErr {
return // 快速失败
}
localUpsert := arg.(string)
err = ossUpload0(localDirPath, cloudDirPath, localUpsert, &wroteFiles, &transferSize)
if nil != err {
uploadErr = err
return
}
if needPushProgress {
util.PushMsg(fmt.Sprintf(Conf.Language(104), wroteFiles, len(localUpserts)-wroteFiles), 1000*60*10)
}
if boot {
msg := fmt.Sprintf("Uploading data to the cloud %d/%d", wroteFiles, len(localUpserts))
util.IncBootProgress(0, msg)
}
})
var index string
localIndex := filepath.Join(localDirPath, "index.json")
for _, localUpsert := range localUpserts {
if localIndex == localUpsert {
// 同步过程中断导致的一致性问题 https://github.com/siyuan-note/siyuan/issues/4912
// index 最后单独上传
index = localUpsert
continue
}
waitGroup.Add(1)
p.Invoke(localUpsert)
}
waitGroup.Wait()
p.Release()
if nil != uploadErr {
err = uploadErr
return
}
// 单独上传 index
if uploadErr = ossUpload0(localDirPath, cloudDirPath, index, &wroteFiles, &transferSize); nil != uploadErr {
err = uploadErr
return
}
if needPushProgress {
util.PushMsg(Conf.Language(105), 3000)
}
err = ossRemove0(cloudDirPath, cloudRemoves)
return
}
func ossRemove0(cloudDirPath string, removes []string) (err error) {
if 1 > len(removes) {
return
}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
resp, err := request.
SetBody(map[string]interface{}{"token": Conf.User.UserToken, "dirPath": cloudDirPath, "paths": removes}).
Post(util.AliyunServer + "/apis/siyuan/data/removeSiYuanFile?uid=" + Conf.User.UserId)
if nil != err {
util.LogErrorf("remove cloud file failed: %s", err)
err = ErrFailedToConnectCloudServer
return
}
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
if 200 != resp.StatusCode {
msg := fmt.Sprintf("remove cloud file failed [sc=%d]", resp.StatusCode)
util.LogErrorf(msg)
err = errors.New(msg)
return
}
return
}
func ossUpload0(localDirPath, cloudDirPath, localUpsert string, wroteFiles *int, transferSize *uint64) (err error) {
info, statErr := os.Stat(localUpsert)
if nil != statErr {
err = statErr
return
}
filename := filepath.ToSlash(strings.TrimPrefix(localUpsert, localDirPath))
upToken, err := getOssUploadToken(filename, cloudDirPath, info.Size())
if nil != err {
return
}
key := path.Join("siyuan", Conf.User.UserId, cloudDirPath, filename)
if err = putFileToCloud(localUpsert, key, upToken); nil != err {
util.LogErrorf("put file [%s] to cloud failed: %s", localUpsert, err)
return errors.New(fmt.Sprintf(Conf.Language(94), err))
}
//util.LogInfof("cloud wrote [%s], size [%d]", filename, info.Size())
*wroteFiles++
*transferSize += uint64(info.Size())
return
}
func getOssUploadToken(filename, cloudDirPath string, length int64) (ret string, err error) {
// 因为需要指定 key所以每次上传文件都必须在云端生成 Token否则有安全隐患
var result map[string]interface{}
req := util.NewCloudRequest(Conf.System.NetworkProxy.String()).
SetResult(&result)
req.SetBody(map[string]interface{}{
"token": Conf.User.UserToken,
"dirPath": cloudDirPath,
"name": filename,
"length": length})
resp, err := req.Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanFileUploadToken?uid=" + Conf.User.UserId)
if nil != err {
util.LogErrorf("get file [%s] upload token failed: %+v", filename, err)
err = errors.New(fmt.Sprintf(Conf.Language(94), err))
return
}
if 200 != resp.StatusCode {
if 401 == resp.StatusCode {
err = errors.New(fmt.Sprintf(Conf.Language(94), Conf.Language(31)))
return
}
util.LogErrorf("get file [%s] upload token failed [sc=%d]", filename, resp.StatusCode)
err = errors.New(fmt.Sprintf(Conf.Language(94), strconv.Itoa(resp.StatusCode)))
return
}
code := result["code"].(float64)
if 0 != code {
msg := result["msg"].(string)
util.LogErrorf("download cloud file failed: %s", msg)
err = errors.New(fmt.Sprintf(Conf.Language(93), msg))
return
}
resultData := result["data"].(map[string]interface{})
ret = resultData["token"].(string)
return
}
func getCloudSyncVer(cloudDir string) (cloudSyncVer int64, err error) {
start := time.Now()
result := map[string]interface{}{}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
resp, err := request.
SetResult(&result).
SetBody(map[string]string{"syncDir": cloudDir, "token": Conf.User.UserToken}).
Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanWorkspaceSyncVer?uid=" + Conf.User.UserId)
if nil != err {
util.LogErrorf("get cloud sync ver failed: %s", err)
err = ErrFailedToConnectCloudServer
return
}
if 200 != resp.StatusCode {
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
util.LogErrorf("get cloud sync ver failed: %d", resp.StatusCode)
err = ErrFailedToConnectCloudServer
return
}
code := result["code"].(float64)
if 0 != code {
msg := result["msg"].(string)
util.LogErrorf("get cloud sync ver failed: %s", msg)
err = errors.New(msg)
return
}
data := result["data"].(map[string]interface{})
cloudSyncVer = int64(data["v"].(float64))
if elapsed := time.Now().Sub(start).Milliseconds(); 2000 < elapsed {
util.LogInfof("get cloud sync ver elapsed [%dms]", elapsed)
}
return
}
func getCloudSync(cloudDir string) (assetSize, backupSize int64, device string, err error) {
start := time.Now()
result := map[string]interface{}{}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
resp, err := request.
SetResult(&result).
SetBody(map[string]string{"syncDir": cloudDir, "token": Conf.User.UserToken}).
Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanWorkspaceSync?uid=" + Conf.User.UserId)
if nil != err {
util.LogErrorf("get cloud sync info failed: %s", err)
err = ErrFailedToConnectCloudServer
return
}
if 200 != resp.StatusCode {
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
util.LogErrorf("get cloud sync info failed: %d", resp.StatusCode)
err = ErrFailedToConnectCloudServer
return
}
code := result["code"].(float64)
if 0 != code {
msg := result["msg"].(string)
util.LogErrorf("get cloud sync info failed: %s", msg)
err = errors.New(msg)
return
}
data := result["data"].(map[string]interface{})
assetSize = int64(data["assetSize"].(float64))
backupSize = int64(data["backupSize"].(float64))
if nil != data["d"] {
device = data["d"].(string)
}
if elapsed := time.Now().Sub(start).Milliseconds(); 5000 < elapsed {
util.LogInfof("get cloud sync [%s] elapsed [%dms]", elapsed)
}
return
}
func getLocalFileListOSS(dirPath string) (ret map[string]*CloudIndex, err error) {
dir := "sync"
if !strings.HasPrefix(dirPath, "sync") {
dir = "backup"
}
data, err := os.ReadFile(filepath.Join(util.WorkspaceDir, dir, "index.json"))
if nil != err {
return
}
if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err {
return
}
return
}
func getCloudFileListOSS(cloudDirPath string) (ret map[string]*CloudIndex, err error) {
result := map[string]interface{}{}
request := util.NewCloudRequest(Conf.System.NetworkProxy.String())
resp, err := request.
SetResult(&result).
SetBody(map[string]string{"dirPath": cloudDirPath, "token": Conf.User.UserToken}).
Post(util.AliyunServer + "/apis/siyuan/data/getSiYuanFileListURL?uid=" + Conf.User.UserId)
if nil != err {
util.LogErrorf("get cloud file list failed: %s", err)
err = ErrFailedToConnectCloudServer
return
}
if 401 == resp.StatusCode {
err = errors.New(Conf.Language(31))
return
}
code := result["code"].(float64)
if 0 != code {
util.LogErrorf("get cloud file list failed: %s", result["msg"])
err = ErrFailedToConnectCloudServer
return
}
retData := result["data"].(map[string]interface{})
downloadURL := retData["url"].(string)
resp, err = util.NewCloudFileRequest15s(Conf.System.NetworkProxy.String()).Get(downloadURL)
if nil != err {
util.LogErrorf("download request [%s] failed: %s", downloadURL, err)
return
}
if 200 != resp.StatusCode {
util.LogErrorf("download request [%s] status code [%d]", downloadURL, resp.StatusCode)
err = errors.New(fmt.Sprintf("download file list failed [%d]", resp.StatusCode))
return
}
data, err := resp.ToBytes()
if err = gulu.JSON.UnmarshalJSON(data, &ret); nil != err {
util.LogErrorf("unmarshal index failed: %s", err)
err = errors.New(fmt.Sprintf("unmarshal index failed"))
return
}
return
}
func localUpsertRemoveListOSS(localDirPath string, cloudFileList map[string]*CloudIndex) (localRemoves, cloudFetches []string, err error) {
unchanged := map[string]bool{}
filepath.Walk(localDirPath, func(path string, info fs.FileInfo, err error) error {
if localDirPath == path {
return nil
}
if info.IsDir() {
return nil
}
relPath := filepath.ToSlash(strings.TrimPrefix(path, localDirPath))
cloudIdx, ok := cloudFileList[relPath]
if !ok {
if util.CloudSingleFileMaxSizeLimit < info.Size() {
util.LogWarnf("file [%s] larger than 100MB, ignore removing it", path)
return nil
}
localRemoves = append(localRemoves, path)
return nil
}
localHash, hashErr := GetEtag(path)
if nil != hashErr {
util.LogErrorf("get local file [%s] etag failed: %s", path, hashErr)
return nil
}
if cloudIdx.Hash == localHash {
unchanged[relPath] = true
}
return nil
})
for cloudPath, cloudIndex := range cloudFileList {
if _, ok := unchanged[cloudPath]; ok {
continue
}
if util.CloudSingleFileMaxSizeLimit < cloudIndex.Size {
util.LogWarnf("cloud file [%s] larger than 100MB, ignore fetching it", cloudPath)
continue
}
cloudFetches = append(cloudFetches, cloudPath)
}
return
}
func cloudUpsertRemoveListOSS(localDirPath string, cloudFileList map[string]*CloudIndex) (localUpserts, cloudRemoves []string, err error) {
localUpserts, cloudRemoves = []string{}, []string{}
unchanged := map[string]bool{}
for cloudFile, cloudIdx := range cloudFileList {
localCheckPath := filepath.Join(localDirPath, cloudFile)
if !gulu.File.IsExist(localCheckPath) {
cloudRemoves = append(cloudRemoves, cloudFile)
continue
}
localHash, hashErr := GetEtag(localCheckPath)
if nil != hashErr {
util.LogErrorf("get local file [%s] hash failed: %s", localCheckPath, hashErr)
err = hashErr
return
}
if localHash == cloudIdx.Hash {
unchanged[localCheckPath] = true
}
}
syncIgnoreList := getSyncIgnoreList()
excludes := map[string]bool{}
ignores := syncIgnoreList.Values()
for _, p := range ignores {
relPath := p.(string)
relPath = pathSha246(relPath, "/")
relPath = filepath.Join(localDirPath, relPath)
excludes[relPath] = true
}
delete(unchanged, filepath.Join(localDirPath, "index.json")) // 同步偶尔报错 `The system cannot find the path specified.` https://github.com/siyuan-note/siyuan/issues/4942
err = genCloudIndex(localDirPath, excludes)
if nil != err {
return
}
filepath.Walk(localDirPath, func(path string, info fs.FileInfo, err error) error {
if localDirPath == path || info.IsDir() {
return nil
}
if !unchanged[path] {
if excludes[path] {
return nil
}
if util.CloudSingleFileMaxSizeLimit < info.Size() {
util.LogWarnf("file [%s] larger than 100MB, ignore uploading it", path)
return nil
}
localUpserts = append(localUpserts, path)
return nil
}
return nil
})
return
}
func putFileToCloud(filePath, key, upToken string) (err error) {
formUploader := storage.NewFormUploader(&storage.Config{UseHTTPS: true})
ret := storage.PutRet{}
err = formUploader.PutFile(context.Background(), &ret, upToken, key, filePath, nil)
if nil != err {
util.LogWarnf("put file [%s] to cloud failed [%s], retry it after 3s", filePath, err)
time.Sleep(3 * time.Second)
err = formUploader.PutFile(context.Background(), &ret, upToken, key, filePath, nil)
if nil != err {
return
}
util.LogInfof("put file [%s] to cloud retry success", filePath)
}
return
}
// 以下是七牛云 Hash 算法实现 https://github.com/qiniu/qetag/blob/master/qetag.go
func GetEtag(filename string) (etag string, err error) {
f, err := os.Open(filename)
if err != nil {
return
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return
}
fsize := fi.Size()
blockCnt := BlockCount(fsize)
sha1Buf := make([]byte, 0, 21)
if blockCnt <= 1 { // file size <= 4M
sha1Buf = append(sha1Buf, 0x16)
sha1Buf, err = CalSha1(sha1Buf, f)
if err != nil {
return
}
} else { // file size > 4M
sha1Buf = append(sha1Buf, 0x96)
sha1BlockBuf := make([]byte, 0, blockCnt*20)
for i := 0; i < blockCnt; i++ {
body := io.LimitReader(f, BLOCK_SIZE)
sha1BlockBuf, err = CalSha1(sha1BlockBuf, body)
if err != nil {
return
}
}
sha1Buf, _ = CalSha1(sha1Buf, bytes.NewReader(sha1BlockBuf))
}
etag = base64.URLEncoding.EncodeToString(sha1Buf)
return
}
const (
BLOCK_BITS = 22 // Indicate that the blocksize is 4M
BLOCK_SIZE = 1 << BLOCK_BITS
)
func BlockCount(fsize int64) int {
return int((fsize + (BLOCK_SIZE - 1)) >> BLOCK_BITS)
}
func CalSha1(b []byte, r io.Reader) ([]byte, error) {
h := sha1.New()
_, err := io.Copy(h, r)
if err != nil {
return nil, err
}
return h.Sum(b), nil
}

111
kernel/model/outline.go Normal file
View file

@ -0,0 +1,111 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"time"
"github.com/88250/lute/ast"
"github.com/emirpasic/gods/stacks/linkedliststack"
"github.com/siyuan-note/siyuan/kernel/treenode"
)
func Outline(rootID string) (ret []*Path, err error) {
time.Sleep(512 * time.Millisecond /* 前端队列轮询间隔 */)
WaitForWritingFiles()
ret = []*Path{}
tree, _ := loadTreeByBlockID(rootID)
if nil == tree {
return
}
luteEngine := NewLute()
var headings []*Block
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if entering && ast.NodeHeading == n.Type && !n.ParentIs(ast.NodeBlockquote) {
n.Box, n.Path = tree.Box, tree.Path
block := &Block{
RootID: rootID,
Depth: n.HeadingLevel,
Box: n.Box,
Path: n.Path,
ID: n.ID,
Content: renderOutline(n, luteEngine),
Type: n.Type.String(),
SubType: treenode.SubTypeAbbr(n),
}
headings = append(headings, block)
return ast.WalkSkipChildren
}
return ast.WalkContinue
})
if 1 > len(headings) {
return
}
var blocks []*Block
stack := linkedliststack.New()
for _, h := range headings {
L:
for ; ; stack.Pop() {
cur, ok := stack.Peek()
if !ok {
blocks = append(blocks, h)
stack.Push(h)
break L
}
tip := cur.(*Block)
if tip.Depth < h.Depth {
tip.Children = append(tip.Children, h)
stack.Push(h)
break L
}
tip.Count = len(tip.Children)
}
}
ret = toFlatTree(blocks, 0, "outline")
if 0 < len(ret) {
children := ret[0].Blocks
ret = nil
for _, b := range children {
resetDepth(b, 0)
ret = append(ret, &Path{
ID: b.ID,
Box: b.Box,
Name: b.Content,
Type: b.Type,
SubType: b.SubType,
Blocks: b.Children,
Depth: 0,
Count: b.Count,
})
}
}
return
}
func resetDepth(b *Block, depth int) {
b.Depth = depth
b.Count = len(b.Children)
for _, c := range b.Children {
resetDepth(c, depth+1)
}
}

354
kernel/model/path.go Normal file
View file

@ -0,0 +1,354 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"os"
"path"
"path/filepath"
"sort"
"strings"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/siyuan-note/siyuan/kernel/search"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func createDocsByHPath(boxID, hPath, content string) (id string, err error) {
hPath = strings.TrimSuffix(hPath, ".sy")
if docExist := nil != treenode.GetBlockTreeRootByHPath(boxID, hPath); docExist {
hPath += "-" + gulu.Rand.String(7)
}
pathBuilder := bytes.Buffer{}
pathBuilder.WriteString("/")
hPathBuilder := bytes.Buffer{}
hPathBuilder.WriteString("/")
parts := strings.Split(hPath, "/")[1:]
for i, part := range parts {
hPathBuilder.WriteString(part)
hp := hPathBuilder.String()
root := treenode.GetBlockTreeRootByHPath(boxID, hp)
isNotLast := i < len(parts)-1
if nil == root {
id = ast.NewNodeID()
pathBuilder.WriteString(id)
docP := pathBuilder.String() + ".sy"
if isNotLast {
if err = createDoc(boxID, docP, part, ""); nil != err {
return
}
} else {
if err = createDoc(boxID, docP, part, content); nil != err {
return
}
}
if isNotLast {
dirPath := filepath.Join(util.DataDir, boxID, pathBuilder.String())
if err = os.MkdirAll(dirPath, 0755); nil != err {
util.LogErrorf("mkdir [%s] failed: %s", dirPath, err)
return
}
}
} else {
id = root.ID
pathBuilder.WriteString(root.ID)
if !isNotLast {
pathBuilder.WriteString(".sy")
}
}
if isNotLast {
pathBuilder.WriteString("/")
hPathBuilder.WriteString("/")
}
}
return
}
func toFlatTree(blocks []*Block, baseDepth int, typ string) (ret []*Path) {
var blockRoots []*Block
for _, block := range blocks {
root := getBlockIn(blockRoots, block.RootID)
if nil == root {
root, _ = getBlock(block.RootID)
blockRoots = append(blockRoots, root)
}
if nil == root {
return
}
block.Depth = baseDepth + 1
block.Count = len(block.Children)
root.Children = append(root.Children, block)
}
for _, root := range blockRoots {
treeNode := &Path{
ID: root.ID,
Box: root.Box,
Name: path.Base(root.HPath),
NodeType: root.Type,
Type: typ,
SubType: root.SubType,
Depth: baseDepth,
Count: len(root.Children),
}
for _, c := range root.Children {
treeNode.Blocks = append(treeNode.Blocks, c)
}
ret = append(ret, treeNode)
}
sort.Slice(ret, func(i, j int) bool {
return ret[i].ID > ret[j].ID
})
return
}
func toSubTree(blocks []*Block, keyword string) (ret []*Path) {
keyword = strings.TrimSpace(keyword)
var blockRoots []*Block
for _, block := range blocks {
root := getBlockIn(blockRoots, block.RootID)
if nil == root {
root, _ = getBlock(block.RootID)
blockRoots = append(blockRoots, root)
}
block.Depth = 1
block.Count = len(block.Children)
root.Children = append(root.Children, block)
}
for _, root := range blockRoots {
treeNode := &Path{
ID: root.ID,
Box: root.Box,
Name: path.Base(root.HPath),
Type: "backlink",
NodeType: "NodeDocument",
SubType: root.SubType,
Depth: 0,
Count: len(root.Children),
}
for _, c := range root.Children {
if "NodeListItem" == c.Type {
tree, _ := loadTreeByBlockID(c.RootID)
li := treenode.GetNodeInTree(tree, c.ID)
var first *sql.Block
if 3 != li.ListData.Typ {
first = sql.GetBlock(li.FirstChild.ID)
} else {
first = sql.GetBlock(li.FirstChild.Next.ID)
}
name := first.Content
parentPos := 0
if "" != keyword {
parentPos, name = search.MarkText(name, keyword, 12, Conf.Search.CaseSensitive)
}
subRoot := &Path{
ID: li.ID,
Box: li.Box,
Name: name,
Type: "backlink",
NodeType: li.Type.String(),
SubType: c.SubType,
Depth: 1,
Count: 1,
}
unfold := true
for liFirstBlockSpan := li.FirstChild.FirstChild; nil != liFirstBlockSpan; liFirstBlockSpan = liFirstBlockSpan.Next {
if ast.NodeBlockRef == liFirstBlockSpan.Type {
continue
}
if "" != strings.TrimSpace(liFirstBlockSpan.Text()) {
unfold = false
break
}
}
for next := li.FirstChild.Next; nil != next; next = next.Next {
subBlock, _ := getBlock(next.ID)
if unfold {
if ast.NodeList == next.Type {
for subLi := next.FirstChild; nil != subLi; subLi = subLi.Next {
subLiBlock, _ := getBlock(subLi.ID)
var subFirst *sql.Block
if 3 != subLi.ListData.Typ {
subFirst = sql.GetBlock(subLi.FirstChild.ID)
} else {
subFirst = sql.GetBlock(subLi.FirstChild.Next.ID)
}
subPos := 0
content := subFirst.Content
if "" != keyword {
subPos, content = search.MarkText(subFirst.Content, keyword, 12, Conf.Search.CaseSensitive)
}
if -1 < subPos {
parentPos = 0 // 需要显示父级
}
subLiBlock.Content = content
subLiBlock.Depth = 2
subRoot.Blocks = append(subRoot.Blocks, subLiBlock)
}
} else if ast.NodeHeading == next.Type {
subBlock.Depth = 2
subRoot.Blocks = append(subRoot.Blocks, subBlock)
headingChildren := treenode.HeadingChildren(next)
var breakSub bool
for _, n := range headingChildren {
block, _ := getBlock(n.ID)
subPos := 0
content := block.Content
if "" != keyword {
subPos, content = search.MarkText(block.Content, keyword, 12, Conf.Search.CaseSensitive)
}
if -1 < subPos {
parentPos = 0
}
block.Content = content
block.Depth = 3
subRoot.Blocks = append(subRoot.Blocks, block)
if ast.NodeHeading == n.Type {
// 跳过子标题下面的块
breakSub = true
break
}
}
if breakSub {
break
}
} else {
if nil == treenode.HeadingParent(next) {
subBlock.Depth = 2
subRoot.Blocks = append(subRoot.Blocks, subBlock)
}
}
}
}
if -1 < parentPos {
treeNode.Children = append(treeNode.Children, subRoot)
}
} else if "NodeHeading" == c.Type {
tree, _ := loadTreeByBlockID(c.RootID)
h := treenode.GetNodeInTree(tree, c.ID)
name := sql.GetBlock(h.ID).Content
parentPos := 0
if "" != keyword {
parentPos, name = search.MarkText(name, keyword, 12, Conf.Search.CaseSensitive)
}
subRoot := &Path{
ID: h.ID,
Box: h.Box,
Name: name,
Type: "backlink",
NodeType: h.Type.String(),
SubType: c.SubType,
Depth: 1,
Count: 1,
}
unfold := true
for headingFirstSpan := h.FirstChild; nil != headingFirstSpan; headingFirstSpan = headingFirstSpan.Next {
if ast.NodeBlockRef == headingFirstSpan.Type {
continue
}
if "" != strings.TrimSpace(headingFirstSpan.Text()) {
unfold = false
break
}
}
if unfold {
headingChildren := treenode.HeadingChildren(h)
for _, headingChild := range headingChildren {
if ast.NodeList == headingChild.Type {
for subLi := headingChild.FirstChild; nil != subLi; subLi = subLi.Next {
subLiBlock, _ := getBlock(subLi.ID)
var subFirst *sql.Block
if 3 != subLi.ListData.Typ {
subFirst = sql.GetBlock(subLi.FirstChild.ID)
} else {
subFirst = sql.GetBlock(subLi.FirstChild.Next.ID)
}
subPos := 0
content := subFirst.Content
if "" != keyword {
subPos, content = search.MarkText(content, keyword, 12, Conf.Search.CaseSensitive)
}
if -1 < subPos {
parentPos = 0
}
subLiBlock.Content = subFirst.Content
subLiBlock.Depth = 2
subRoot.Blocks = append(subRoot.Blocks, subLiBlock)
}
} else {
subBlock, _ := getBlock(headingChild.ID)
subBlock.Depth = 2
subRoot.Blocks = append(subRoot.Blocks, subBlock)
}
}
}
if -1 < parentPos {
treeNode.Children = append(treeNode.Children, subRoot)
}
} else {
pos := 0
content := c.Content
if "" != keyword {
pos, content = search.MarkText(content, keyword, 12, Conf.Search.CaseSensitive)
}
if -1 < pos {
treeNode.Blocks = append(treeNode.Blocks, c)
}
}
}
rootPos := -1
var rootContent string
if "" != keyword {
rootPos, rootContent = search.MarkText(treeNode.Name, keyword, 12, Conf.Search.CaseSensitive)
treeNode.Name = rootContent
}
if 0 < len(treeNode.Children) || 0 < len(treeNode.Blocks) || (-1 < rootPos && "" != keyword) {
ret = append(ret, treeNode)
}
}
sort.Slice(ret, func(i, j int) bool {
return ret[i].ID > ret[j].ID
})
return
}
func getBlockIn(blocks []*Block, id string) *Block {
if "" == id {
return nil
}
for _, block := range blocks {
if block.ID == id {
return block
}
}
return nil
}

47
kernel/model/process.go Normal file
View file

@ -0,0 +1,47 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"os"
"os/signal"
"syscall"
"time"
"github.com/siyuan-note/siyuan/kernel/util"
)
func HookResident() {
if util.Resident {
return
}
for range time.Tick(time.Second * 30) {
if 0 == util.CountSessions() {
util.LogInfof("no active session, exit kernel process now")
Close(false)
}
}
}
func HandleSignal() {
c := make(chan os.Signal)
signal.Notify(c, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
s := <-c
util.LogInfof("received os signal [%s], exit kernel process now", s)
Close(false)
}

234
kernel/model/render.go Normal file
View file

@ -0,0 +1,234 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"strings"
"github.com/88250/lute"
"github.com/88250/lute/ast"
"github.com/88250/lute/html"
"github.com/88250/lute/parse"
"github.com/88250/lute/render"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
)
func renderOutline(node *ast.Node, luteEngine *lute.Lute) (ret string) {
if nil == node {
return ""
}
if ast.NodeDocument == node.Type {
return node.IALAttr("title")
}
buf := bytes.Buffer{}
buf.Grow(4096)
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
switch n.Type {
case ast.NodeTagOpenMarker, ast.NodeTagCloseMarker:
buf.WriteByte('#')
case ast.NodeBlockRef:
buf.WriteString(html.EscapeString(treenode.GetDynamicBlockRefText(n)))
return ast.WalkSkipChildren
case ast.NodeText, ast.NodeLinkText, ast.NodeFileAnnotationRefText, ast.NodeFootnotesRef, ast.NodeCodeBlockCode, ast.NodeMathBlockContent:
tokens := html.EscapeHTML(n.Tokens)
tokens = bytes.ReplaceAll(tokens, []byte(" "), []byte("&nbsp;")) // 大纲面板条目中无法显示多个空格 https://github.com/siyuan-note/siyuan/issues/4370
buf.Write(tokens)
case ast.NodeInlineMath, ast.NodeStrong, ast.NodeEmphasis, ast.NodeCodeSpan:
dom := lute.RenderNodeBlockDOM(n, luteEngine.ParseOptions, luteEngine.RenderOptions)
buf.WriteString(dom)
return ast.WalkSkipChildren
}
return ast.WalkContinue
})
ret = strings.TrimSpace(buf.String())
ret = strings.ReplaceAll(ret, "\n", "")
return
}
func renderBlockText(node *ast.Node) (ret string) {
ret = treenode.NodeStaticContent(node)
ret = strings.TrimSpace(ret)
ret = strings.ReplaceAll(ret, "\n", "")
ret = html.EscapeString(ret)
ret = strings.TrimSpace(ret)
if "" == ret {
// 复制内容为空的块作为块引用时粘贴无效 https://github.com/siyuan-note/siyuan/issues/4962
buf := bytes.Buffer{}
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeImage == n.Type {
title := n.ChildByType(ast.NodeLinkTitle)
if nil == title {
alt := n.ChildByType(ast.NodeLinkText)
if nil != alt && 0 < len(alt.Tokens) {
buf.Write(alt.Tokens)
} else {
buf.WriteString("image")
}
} else {
buf.Write(title.Tokens)
}
}
return ast.WalkContinue
})
ret = buf.String()
}
return
}
func renderBlockDOMByNodes(nodes []*ast.Node, luteEngine *lute.Lute) string {
tree := &parse.Tree{Root: &ast.Node{Type: ast.NodeDocument}, Context: &parse.Context{ParseOption: luteEngine.ParseOptions}}
blockRenderer := render.NewBlockRenderer(tree, luteEngine.RenderOptions)
for _, n := range nodes {
ast.Walk(n, func(node *ast.Node, entering bool) ast.WalkStatus {
rendererFunc := blockRenderer.RendererFuncs[node.Type]
return rendererFunc(node, entering)
})
}
h := strings.TrimSpace(blockRenderer.Writer.String())
if strings.HasPrefix(h, "<li") {
h = "<ul>" + h + "</ul>"
}
return h
}
func renderBlockMarkdownR(id string) string {
depth := 0
nodes := renderBlockMarkdownR0(id, &depth)
buf := bytes.Buffer{}
buf.Grow(4096)
luteEngine := NewLute()
for _, n := range nodes {
md := treenode.FormatNode(n, luteEngine)
buf.WriteString(md)
buf.WriteString("\n\n")
}
return buf.String()
}
func renderBlockMarkdownR0(id string, depth *int) (ret []*ast.Node) {
*depth++
if 7 < *depth {
return
}
b := treenode.GetBlockTree(id)
if nil == b {
return
}
var err error
var t *parse.Tree
if t, err = loadTreeByBlockID(b.ID); nil != err {
return
}
node := treenode.GetNodeInTree(t, b.ID)
if nil == node {
return
}
var children []*ast.Node
if ast.NodeHeading == node.Type {
children = append(children, node)
children = append(children, treenode.HeadingChildren(node)...)
} else if ast.NodeDocument == node.Type {
for c := node.FirstChild; nil != c; c = c.Next {
children = append(children, c)
}
} else {
children = append(children, node)
}
for _, child := range children {
var unlinks, inserts []*ast.Node
ast.Walk(child, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || !n.IsBlock() {
return ast.WalkContinue
}
if ast.NodeBlockQueryEmbed == n.Type {
stmt := n.ChildByType(ast.NodeBlockQueryEmbedScript).TokensStr()
stmt = html.UnescapeString(stmt)
sqlBlocks := sql.SelectBlocksRawStmt(stmt, Conf.Search.Limit)
for _, sqlBlock := range sqlBlocks {
subNodes := renderBlockMarkdownR0(sqlBlock.ID, depth)
for _, subNode := range subNodes {
inserts = append(inserts, subNode)
}
}
unlinks = append(unlinks, n)
return ast.WalkSkipChildren
}
return ast.WalkContinue
})
for _, n := range unlinks {
n.Unlink()
}
if ast.NodeBlockQueryEmbed != child.Type {
ret = append(ret, child)
} else {
for _, n := range inserts {
ret = append(ret, n)
}
}
}
return
}
func renderBlockMarkdown(node *ast.Node) string {
var nodes []*ast.Node
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if entering {
nodes = append(nodes, n)
if ast.NodeHeading == node.Type {
// 支持“标题块”引用
children := treenode.HeadingChildren(n)
nodes = append(nodes, children...)
}
}
return ast.WalkSkipChildren
})
root := &ast.Node{Type: ast.NodeDocument}
luteEngine := NewLute()
luteEngine.SetKramdownIAL(false)
luteEngine.SetSuperBlock(false)
tree := &parse.Tree{Root: root, Context: &parse.Context{ParseOption: luteEngine.ParseOptions}}
renderer := render.NewFormatRenderer(tree, luteEngine.RenderOptions)
renderer.Writer = &bytes.Buffer{}
renderer.Writer.Grow(4096)
renderer.NodeWriterStack = append(renderer.NodeWriterStack, renderer.Writer) // 因为有可能不是从 root 开始渲染,所以需要初始化
for _, node := range nodes {
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
rendererFunc := renderer.RendererFuncs[n.Type]
return rendererFunc(n, entering)
})
}
return strings.TrimSpace(renderer.Writer.String())
}

513
kernel/model/search.go Normal file
View file

@ -0,0 +1,513 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"path"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/html"
"github.com/88250/lute/parse"
"github.com/jinzhu/copier"
"github.com/siyuan-note/siyuan/kernel/conf"
"github.com/siyuan-note/siyuan/kernel/search"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
"github.com/xrash/smetrics"
)
func SearchEmbedBlock(stmt string, excludeIDs []string, headingMode int) (ret []*Block) {
WaitForWritingFiles()
return searchEmbedBlock(stmt, excludeIDs, headingMode)
}
func searchEmbedBlock(stmt string, excludeIDs []string, headingMode int) (ret []*Block) {
sqlBlocks := sql.SelectBlocksRawStmtNoParse(stmt, Conf.Search.Limit)
var tmp []*sql.Block
for _, b := range sqlBlocks {
if !gulu.Str.Contains(b.ID, excludeIDs) {
tmp = append(tmp, b)
}
}
sqlBlocks = tmp
for _, sb := range sqlBlocks {
block := getBlockRendered(sb.ID, headingMode)
if nil == block {
continue
}
ret = append(ret, block)
}
if 1 > len(ret) {
ret = []*Block{}
}
return
}
func SearchRefBlock(id, rootID, keyword string, beforeLen int) (ret []*Block, newDoc bool) {
if "" == keyword {
// 查询为空时默认的块引排序规则按最近使用优先 https://github.com/siyuan-note/siyuan/issues/3218
refs := sql.QueryRefsRecent()
for _, ref := range refs {
sqlBlock := sql.GetBlock(ref.DefBlockID)
block := fromSQLBlock(sqlBlock, "", beforeLen)
if nil == block {
continue
}
block.Content = maxContent(block.Content, Conf.Editor.BlockRefDynamicAnchorTextMaxLen)
block.RefText = block.Content
if block.IsContainerBlock() {
block.RefText = block.FContent // `((` 引用列表项时使用第一个子块作为动态锚文本 https://github.com/siyuan-note/siyuan/issues/4536
}
ret = append(ret, block)
}
if 1 > len(ret) {
ret = []*Block{}
}
return
}
ret = fullTextSearchRefBlock(keyword, beforeLen)
tmp := ret[:0]
trees := map[string]*parse.Tree{}
for _, b := range ret {
hitFirstChildID := false
b.RefText = b.Content
if b.IsContainerBlock() {
b.RefText = b.FContent // `((` 引用列表项时使用第一个子块作为动态锚文本 https://github.com/siyuan-note/siyuan/issues/4536
// `((` 引用候选中排除当前块的父块 https://github.com/siyuan-note/siyuan/issues/4538
tree := trees[b.RootID]
if nil == tree {
tree, _ = loadTreeByBlockID(b.RootID)
trees[b.RootID] = tree
}
if nil != tree {
bNode := treenode.GetNodeInTree(tree, b.ID)
if fc := treenode.FirstLeafBlock(bNode); nil != fc && fc.ID == id {
hitFirstChildID = true
}
}
}
if b.ID != id && !hitFirstChildID && b.ID != rootID {
b.Content = maxContent(b.Content, Conf.Editor.BlockRefDynamicAnchorTextMaxLen)
tmp = append(tmp, b)
}
}
ret = tmp
if "" != keyword {
if block := treenode.GetBlockTree(id); nil != block {
p := path.Join(block.HPath, keyword)
newDoc = nil == treenode.GetBlockTreeRootByHPath(block.BoxID, p)
}
}
return
}
func FindReplace(keyword, replacement string, ids []string) (err error) {
keyword = strings.Trim(keyword, "\"") // FTS 字符串需要去除双引号
if keyword == replacement {
return
}
ids = util.RemoveDuplicatedElem(ids)
for _, id := range ids {
var tree *parse.Tree
tree, err = loadTreeByBlockID(id)
if nil != err {
return
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
return
}
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
switch n.Type {
case ast.NodeDocument:
title := n.IALAttr("title")
if strings.Contains(title, keyword) {
n.SetIALAttr("title", strings.ReplaceAll(title, keyword, replacement))
}
case ast.NodeText, ast.NodeLinkText, ast.NodeLinkTitle, ast.NodeCodeSpanContent, ast.NodeCodeBlockCode, ast.NodeInlineMathContent, ast.NodeMathBlockContent:
if bytes.Contains(n.Tokens, []byte(keyword)) {
n.Tokens = bytes.ReplaceAll(n.Tokens, []byte(keyword), []byte(replacement))
}
}
return ast.WalkContinue
})
if err = writeJSONQueue(tree); nil != err {
return
}
}
WaitForWritingFiles()
if 1 < len(ids) {
go func() {
time.Sleep(time.Second)
util.ReloadUI()
}()
}
return
}
func FullTextSearchBlock(query, box, path string, types map[string]bool, querySyntax bool) (ret []*Block) {
query = strings.TrimSpace(query)
if queryStrLower := strings.ToLower(query); strings.Contains(queryStrLower, "select ") && strings.Contains(queryStrLower, " * ") && strings.Contains(queryStrLower, " from ") {
ret = searchBySQL(query, 12)
} else {
filter := searchFilter(types)
ret = fullTextSearch(query, box, path, filter, 12, querySyntax)
}
return
}
func searchFilter(types map[string]bool) string {
s := conf.NewSearch()
if err := copier.Copy(s, Conf.Search); nil != err {
util.LogErrorf("copy search conf failed: %s", err)
}
if nil != types {
s.Document = types["document"]
s.Heading = types["heading"]
s.List = types["list"]
s.ListItem = types["listItem"]
s.CodeBlock = types["codeBlock"]
s.MathBlock = types["mathBlock"]
s.Table = types["table"]
s.Blockquote = types["blockquote"]
s.SuperBlock = types["superBlock"]
s.Paragraph = types["paragraph"]
s.HTMLBlock = types["htmlBlock"]
} else {
s.Document = Conf.Search.Document
s.Heading = Conf.Search.Heading
s.List = Conf.Search.List
s.ListItem = Conf.Search.ListItem
s.CodeBlock = Conf.Search.CodeBlock
s.MathBlock = Conf.Search.MathBlock
s.Table = Conf.Search.Table
s.Blockquote = Conf.Search.Blockquote
s.SuperBlock = Conf.Search.SuperBlock
s.Paragraph = Conf.Search.Paragraph
s.HTMLBlock = Conf.Search.HTMLBlock
}
return s.TypeFilter()
}
func searchBySQL(stmt string, beforeLen int) (ret []*Block) {
stmt = util.RemoveInvisible(stmt)
blocks := sql.SelectBlocksRawStmt(stmt, Conf.Search.Limit)
ret = fromSQLBlocks(&blocks, "", beforeLen)
if 1 > len(ret) {
ret = []*Block{}
}
return
}
func fullTextSearchRefBlock(keyword string, beforeLen int) (ret []*Block) {
keyword = util.RemoveInvisible(keyword)
if util.IsIDPattern(keyword) {
ret = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+keyword+"'", 12)
return
}
quotedKeyword := stringQuery(keyword)
table := "blocks_fts" // 大小写敏感
if !Conf.Search.CaseSensitive {
table = "blocks_fts_case_insensitive"
}
projections := "id, parent_id, root_id, hash, box, path, " +
"highlight(" + table + ", 6, '__@mark__', '__mark@__') AS hpath, " +
"highlight(" + table + ", 7, '__@mark__', '__mark@__') AS name, " +
"highlight(" + table + ", 8, '__@mark__', '__mark@__') AS alias, " +
"highlight(" + table + ", 9, '__@mark__', '__mark@__') AS memo, " +
"tag, " +
"highlight(" + table + ", 11, '__@mark__', '__mark@__') AS content, " +
"fcontent, markdown, length, type, subtype, ial, sort, created, updated"
stmt := "SELECT " + projections + " FROM " + table + " WHERE " + table + " MATCH '" + columnFilter() + ":(" + quotedKeyword + ")' AND type IN " + Conf.Search.TypeFilter()
orderBy := ` order by case
when name = '${keyword}' then 10
when alias = '${keyword}' then 20
when memo = '${keyword}' then 30
when content = '${keyword}' and type = 'd' then 40
when content LIKE '%${keyword}%' and type = 'd' then 41
when name LIKE '%${keyword}%' then 50
when alias LIKE '%${keyword}%' then 60
when content = '${keyword}' and type = 'h' then 70
when content LIKE '%${keyword}%' and type = 'h' then 71
when fcontent = '${keyword}' and type = 'i' then 80
when fcontent LIKE '%${keyword}%' and type = 'i' then 81
when memo LIKE '%${keyword}%' then 90
when content LIKE '%${keyword}%' and type != 'i' and type != 'l' then 100
else 65535 end ASC, sort ASC, length ASC`
orderBy = strings.ReplaceAll(orderBy, "${keyword}", keyword)
stmt += orderBy + " LIMIT " + strconv.Itoa(Conf.Search.Limit)
blocks := sql.SelectBlocksRawStmt(stmt, Conf.Search.Limit)
ret = fromSQLBlocks(&blocks, "", beforeLen)
if 1 > len(ret) {
ret = []*Block{}
}
return
}
func fullTextSearch(query, box, path, filter string, beforeLen int, querySyntax bool) (ret []*Block) {
query = util.RemoveInvisible(query)
if util.IsIDPattern(query) {
ret = searchBySQL("SELECT * FROM `blocks` WHERE `id` = '"+query+"'", beforeLen)
return
}
if !querySyntax {
query = stringQuery(query)
}
table := "blocks_fts" // 大小写敏感
if !Conf.Search.CaseSensitive {
table = "blocks_fts_case_insensitive"
}
projections := "id, parent_id, root_id, hash, box, path, " +
"highlight(" + table + ", 6, '__@mark__', '__mark@__') AS hpath, " +
"highlight(" + table + ", 7, '__@mark__', '__mark@__') AS name, " +
"highlight(" + table + ", 8, '__@mark__', '__mark@__') AS alias, " +
"highlight(" + table + ", 9, '__@mark__', '__mark@__') AS memo, " +
"tag, " +
"highlight(" + table + ", 11, '__@mark__', '__mark@__') AS content, " +
"fcontent, markdown, length, type, subtype, ial, sort, created, updated"
stmt := "SELECT " + projections + " FROM " + table + " WHERE " + table + " MATCH '" + columnFilter() + ":(" + query + ")' AND type IN " + filter
if "" != box {
stmt += " AND box = '" + box + "'"
}
if "" != path {
stmt += " AND path LIKE '" + path + "%'"
}
stmt += " ORDER BY sort ASC, rank ASC LIMIT " + strconv.Itoa(Conf.Search.Limit)
blocks := sql.SelectBlocksRawStmt(stmt, Conf.Search.Limit)
ret = fromSQLBlocks(&blocks, "", beforeLen)
if 1 > len(ret) {
ret = []*Block{}
}
return
}
func query2Stmt(queryStr string) (ret string) {
buf := bytes.Buffer{}
if util.IsIDPattern(queryStr) {
buf.WriteString("id = '" + queryStr + "'")
} else {
var tags []string
luteEngine := NewLute()
t := parse.Inline("", []byte(queryStr), luteEngine.ParseOptions)
ast.Walk(t.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeTag == n.Type {
tags = append(tags, n.Text())
}
return ast.WalkContinue
})
for _, tag := range tags {
queryStr = strings.ReplaceAll(queryStr, "#"+tag+"#", "")
}
parts := strings.Split(queryStr, " ")
for i, part := range parts {
if "" == part {
continue
}
part = strings.ReplaceAll(part, "'", "''")
buf.WriteString("(content LIKE '%" + part + "%'")
buf.WriteString(Conf.Search.NAMFilter(part))
buf.WriteString(")")
if i < len(parts)-1 {
buf.WriteString(" AND ")
}
}
if 0 < len(tags) {
if 0 < buf.Len() {
buf.WriteString(" OR ")
}
for i, tag := range tags {
buf.WriteString("(content LIKE '%#" + tag + "#%')")
if i < len(tags)-1 {
buf.WriteString(" AND ")
}
}
buf.WriteString(" OR ")
for i, tag := range tags {
buf.WriteString("ial LIKE '%tags=\"%" + tag + "%\"%'")
if i < len(tags)-1 {
buf.WriteString(" AND ")
}
}
}
}
if 1 > buf.Len() {
buf.WriteString("1=1")
}
ret = buf.String()
return
}
func markSearch(text string, keyword string, beforeLen int) (pos int, marked string, score float64) {
if 0 == len(keyword) {
marked = text
if maxLen := 5120; maxLen < utf8.RuneCountInString(marked) {
marked = gulu.Str.SubStr(marked, maxLen) + "..."
}
marked = html.EscapeString(marked)
marked = strings.ReplaceAll(marked, "__@mark__", "<mark>")
marked = strings.ReplaceAll(marked, "__mark@__", "</mark>")
return
}
pos, marked = search.MarkText(text, keyword, beforeLen, Conf.Search.CaseSensitive)
if -1 < pos {
if 0 == pos {
score = 1
}
score += float64(strings.Count(marked, "<mark>"))
winkler := smetrics.JaroWinkler(text, keyword, 0.7, 4)
score += winkler
}
score = -score // 分越小排序越靠前
return
}
func fromSQLBlocks(sqlBlocks *[]*sql.Block, terms string, beforeLen int) (ret []*Block) {
for _, sqlBlock := range *sqlBlocks {
ret = append(ret, fromSQLBlock(sqlBlock, terms, beforeLen))
}
return
}
func fromSQLBlock(sqlBlock *sql.Block, terms string, beforeLen int) (block *Block) {
if nil == sqlBlock {
return
}
id := sqlBlock.ID
content := sqlBlock.Content
p := sqlBlock.Path
_, content, _ = markSearch(content, terms, beforeLen)
markdown := maxContent(sqlBlock.Markdown, 5120)
content = maxContent(content, 5120)
block = &Block{
Box: sqlBlock.Box,
Path: p,
ID: id,
RootID: sqlBlock.RootID,
ParentID: sqlBlock.ParentID,
Alias: sqlBlock.Alias,
Name: sqlBlock.Name,
Memo: sqlBlock.Memo,
Tag: sqlBlock.Tag,
Content: content,
FContent: sqlBlock.FContent,
Markdown: markdown,
Type: treenode.FromAbbrType(sqlBlock.Type),
SubType: sqlBlock.SubType,
}
if "" != sqlBlock.IAL {
block.IAL = map[string]string{}
ialStr := strings.TrimPrefix(sqlBlock.IAL, "{:")
ialStr = strings.TrimSuffix(ialStr, "}")
ial := parse.Tokens2IAL([]byte(ialStr))
for _, kv := range ial {
block.IAL[kv[0]] = kv[1]
}
}
_, hPath, _ := markSearch(sqlBlock.HPath, terms, 18)
if !strings.HasPrefix(hPath, "/") {
hPath = "/" + hPath
}
block.HPath = hPath
if "" != block.Name {
_, block.Name, _ = markSearch(block.Name, terms, 256)
}
if "" != block.Alias {
_, block.Alias, _ = markSearch(block.Alias, terms, 256)
}
if "" != block.Memo {
_, block.Memo, _ = markSearch(block.Memo, terms, 256)
}
return
}
func maxContent(content string, maxLen int) string {
if maxLen < utf8.RuneCountInString(content) {
return gulu.Str.SubStr(content, maxLen) + "..."
}
return content
}
func columnFilter() string {
buf := bytes.Buffer{}
buf.WriteString("{content")
if Conf.Search.Name {
buf.WriteString(" name")
}
if Conf.Search.Alias {
buf.WriteString(" alias")
}
if Conf.Search.Memo {
buf.WriteString(" memo")
}
if Conf.Search.Custom {
buf.WriteString(" ial")
}
buf.WriteString(" tag}")
return buf.String()
}
func stringQuery(query string) string {
query = strings.ReplaceAll(query, "\"", "\"\"")
buf := bytes.Buffer{}
parts := strings.Split(query, " ")
for _, part := range parts {
part = strings.TrimSpace(part)
part = "\"" + part + "\""
buf.WriteString(part)
buf.WriteString(" ")
}
return strings.TrimSpace(buf.String())
}

149
kernel/model/session.go Normal file
View file

@ -0,0 +1,149 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"net/http"
"strings"
"github.com/88250/gulu"
ginSessions "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/siyuan-note/siyuan/kernel/util"
)
func LogoutAuth(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
if "" == Conf.AccessAuthCode {
ret.Code = -1
ret.Msg = Conf.Language(86)
ret.Data = map[string]interface{}{"closeTimeout": 5000}
return
}
session := ginSessions.Default(c)
session.Options(ginSessions.Options{
Path: "/",
MaxAge: -1,
})
session.Clear()
if err := session.Save(); nil != err {
util.LogErrorf("saves session failed: " + err.Error())
ret.Code = -1
ret.Msg = "save session failed"
}
}
func LoginAuth(c *gin.Context) {
ret := gulu.Ret.NewResult()
defer c.JSON(http.StatusOK, ret)
arg, ok := util.JsonArg(c, ret)
if !ok {
return
}
authCode := arg["authCode"].(string)
if Conf.AccessAuthCode != authCode {
ret.Code = -1
ret.Msg = Conf.Language(83)
return
}
session := &util.SessionData{ID: gulu.Rand.Int(0, 1024), AccessAuthCode: authCode}
if err := session.Save(c); nil != err {
util.LogErrorf("saves session failed: " + err.Error())
ret.Code = -1
ret.Msg = "save session failed"
return
}
}
func CheckReadonly(c *gin.Context) {
if util.ReadOnly {
result := util.NewResult()
result.Code = -1
result.Msg = Conf.Language(34)
result.Data = map[string]interface{}{"closeTimeout": 5000}
c.JSON(200, result)
c.Abort()
return
}
}
func CheckAuth(c *gin.Context) {
//util.LogInfof("check auth for [%s]", c.Request.RequestURI)
// 放过 /appearance/
if strings.HasPrefix(c.Request.RequestURI, "/appearance/") ||
strings.HasPrefix(c.Request.RequestURI, "/stage/build/export/") ||
strings.HasPrefix(c.Request.RequestURI, "/stage/build/fonts/") ||
strings.HasPrefix(c.Request.RequestURI, "/stage/protyle/") {
c.Next()
return
}
// 放过来自本机的资源文件请求
if strings.HasPrefix(c.Request.RemoteAddr, "127.0.0.1") &&
(strings.HasPrefix(c.Request.RequestURI, "/assets/") || strings.HasPrefix(c.Request.RequestURI, "/history/assets/")) {
c.Next()
return
}
// 通过 Cookie
session := util.GetSession(c)
if session.AccessAuthCode == Conf.AccessAuthCode {
c.Next()
return
}
// 通过 API token
if authHeader := c.GetHeader("Authorization"); "" != authHeader {
if strings.HasPrefix(authHeader, "Token ") {
token := strings.TrimPrefix(authHeader, "Token ")
if Conf.Api.Token == token {
c.Next()
return
}
c.JSON(401, map[string]interface{}{"code": -1, "msg": "Auth failed"})
c.Abort()
return
}
}
if strings.HasSuffix(c.Request.RequestURI, "/check-auth") {
c.Next()
return
}
if session.AccessAuthCode != Conf.AccessAuthCode {
userAgentHeader := c.GetHeader("User-Agent")
if strings.HasPrefix(userAgentHeader, "SiYuan/") || strings.HasPrefix(userAgentHeader, "Mozilla/") {
c.Redirect(302, "/check-auth")
c.Abort()
return
}
c.JSON(401, map[string]interface{}{"code": -1, "msg": "Auth failed"})
c.Abort()
return
}
c.Next()
}

1308
kernel/model/sync.go Normal file

File diff suppressed because it is too large Load diff

368
kernel/model/tag.go Normal file
View file

@ -0,0 +1,368 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"errors"
"fmt"
"sort"
"strings"
"github.com/88250/gulu"
"github.com/88250/lute/ast"
"github.com/88250/lute/html"
"github.com/emirpasic/gods/sets/hashset"
"github.com/facette/natsort"
"github.com/siyuan-note/siyuan/kernel/search"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func RemoveTag(label string) (err error) {
if "" == label {
return
}
util.PushEndlessProgress(Conf.Language(116))
util.RandomSleep(1000, 2000)
tags := sql.QueryTagSpansByKeyword(label, 102400)
treeBlocks := map[string][]string{}
for _, tag := range tags {
if blocks, ok := treeBlocks[tag.RootID]; !ok {
treeBlocks[tag.RootID] = []string{tag.BlockID}
} else {
treeBlocks[tag.RootID] = append(blocks, tag.BlockID)
}
}
for treeID, blocks := range treeBlocks {
util.PushEndlessProgress("[" + treeID + "]")
tree, e := loadTreeByBlockID(treeID)
if nil != e {
util.ClearPushProgress(100)
return e
}
var unlinks []*ast.Node
for _, blockID := range blocks {
node := treenode.GetNodeInTree(tree, blockID)
if nil == node {
continue
}
if ast.NodeDocument == node.Type {
if docTagsVal := node.IALAttr("tags"); strings.Contains(docTagsVal, label) {
docTags := strings.Split(docTagsVal, ",")
var tmp []string
for _, docTag := range docTags {
if docTag != label {
tmp = append(tmp, docTag)
continue
}
}
node.SetIALAttr("tags", strings.Join(tmp, ","))
}
continue
}
nodeTags := node.ChildrenByType(ast.NodeTag)
for _, nodeTag := range nodeTags {
nodeLabels := nodeTag.ChildrenByType(ast.NodeText)
for _, nodeLabel := range nodeLabels {
if bytes.Equal(nodeLabel.Tokens, []byte(label)) {
unlinks = append(unlinks, nodeTag)
}
}
}
}
for _, n := range unlinks {
n.Unlink()
}
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(111), tree.Root.IALAttr("title")))
if err = writeJSONQueue(tree); nil != err {
util.ClearPushProgress(100)
return
}
util.RandomSleep(50, 150)
}
util.PushEndlessProgress(Conf.Language(113))
sql.WaitForWritingDatabase()
util.ReloadUI()
return
}
func RenameTag(oldLabel, newLabel string) (err error) {
if treenode.ContainsMarker(newLabel) {
return errors.New(Conf.Language(112))
}
newLabel = strings.TrimSpace(newLabel)
newLabel = strings.TrimPrefix(newLabel, "/")
newLabel = strings.TrimSuffix(newLabel, "/")
newLabel = strings.TrimSpace(newLabel)
if "" == newLabel {
return errors.New(Conf.Language(114))
}
if oldLabel == newLabel {
return
}
util.PushEndlessProgress(Conf.Language(110))
util.RandomSleep(1000, 2000)
tags := sql.QueryTagSpansByKeyword(oldLabel, 102400)
treeBlocks := map[string][]string{}
for _, tag := range tags {
if blocks, ok := treeBlocks[tag.RootID]; !ok {
treeBlocks[tag.RootID] = []string{tag.BlockID}
} else {
treeBlocks[tag.RootID] = append(blocks, tag.BlockID)
}
}
for treeID, blocks := range treeBlocks {
util.PushEndlessProgress("[" + treeID + "]")
tree, e := loadTreeByBlockID(treeID)
if nil != e {
util.ClearPushProgress(100)
return e
}
for _, blockID := range blocks {
node := treenode.GetNodeInTree(tree, blockID)
if nil == node {
continue
}
if ast.NodeDocument == node.Type {
if docTagsVal := node.IALAttr("tags"); strings.Contains(docTagsVal, oldLabel) {
docTags := strings.Split(docTagsVal, ",")
if gulu.Str.Contains(newLabel, docTags) {
continue
}
var tmp []string
for i, docTag := range docTags {
if !strings.Contains(docTag, oldLabel) {
tmp = append(tmp, docTag)
continue
}
if newTag := strings.ReplaceAll(docTags[i], oldLabel, newLabel); !gulu.Str.Contains(newTag, tmp) {
tmp = append(tmp, newTag)
}
}
node.SetIALAttr("tags", strings.Join(tmp, ","))
}
continue
}
nodeTags := node.ChildrenByType(ast.NodeTag)
for _, nodeTag := range nodeTags {
nodeLabels := nodeTag.ChildrenByType(ast.NodeText)
for _, nodeLabel := range nodeLabels {
if bytes.Contains(nodeLabel.Tokens, []byte(oldLabel)) {
nodeLabel.Tokens = bytes.ReplaceAll(nodeLabel.Tokens, []byte(oldLabel), []byte(newLabel))
}
}
}
}
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(111), tree.Root.IALAttr("title")))
if err = writeJSONQueue(tree); nil != err {
util.ClearPushProgress(100)
return
}
util.RandomSleep(50, 150)
}
util.PushEndlessProgress(Conf.Language(113))
sql.WaitForWritingDatabase()
util.ReloadUI()
return
}
type TagBlocks []*Block
func (s TagBlocks) Len() int { return len(s) }
func (s TagBlocks) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s TagBlocks) Less(i, j int) bool { return s[i].ID < s[j].ID }
type Tag struct {
Name string `json:"name"`
Label string `json:"label"`
Children Tags `json:"children"`
Type string `json:"type"` // "tag"
Depth int `json:"depth"`
Count int `json:"count"`
tags Tags
}
type Tags []*Tag
func BuildTags() (ret *Tags) {
WaitForWritingFiles()
sql.WaitForWritingDatabase()
ret = &Tags{}
labels := labelTags()
tags := Tags{}
for label, _ := range labels {
tags = buildTags(tags, strings.Split(label, "/"), 0)
}
appendTagChildren(&tags, labels)
sortTags(tags)
ret = &tags
return
}
func sortTags(tags Tags) {
switch Conf.Tag.Sort {
case util.SortModeNameASC:
sort.Slice(tags, func(i, j int) bool {
return util.PinYinCompare(util.RemoveEmoji(tags[i].Name), util.RemoveEmoji(tags[j].Name))
})
case util.SortModeNameDESC:
sort.Slice(tags, func(j, i int) bool {
return util.PinYinCompare(util.RemoveEmoji(tags[i].Name), util.RemoveEmoji(tags[j].Name))
})
case util.SortModeAlphanumASC:
sort.Slice(tags, func(i, j int) bool {
return natsort.Compare(util.RemoveEmoji((tags)[i].Name), util.RemoveEmoji((tags)[j].Name))
})
case util.SortModeAlphanumDESC:
sort.Slice(tags, func(i, j int) bool {
return natsort.Compare(util.RemoveEmoji((tags)[j].Name), util.RemoveEmoji((tags)[i].Name))
})
case util.SortModeRefCountASC:
sort.Slice(tags, func(i, j int) bool { return (tags)[i].Count < (tags)[j].Count })
case util.SortModeRefCountDESC:
sort.Slice(tags, func(i, j int) bool { return (tags)[i].Count > (tags)[j].Count })
default:
sort.Slice(tags, func(i, j int) bool {
return natsort.Compare(util.RemoveEmoji((tags)[i].Name), util.RemoveEmoji((tags)[j].Name))
})
}
}
func SearchTags(keyword string) (ret []string) {
ret = []string{}
labels := labelBlocksByKeyword(keyword)
for label, _ := range labels {
_, t := search.MarkText(label, keyword, 1024, Conf.Search.CaseSensitive)
ret = append(ret, t)
}
sort.Strings(ret)
return
}
func labelBlocksByKeyword(keyword string) (ret map[string]TagBlocks) {
ret = map[string]TagBlocks{}
tags := sql.QueryTagSpansByKeyword(keyword, Conf.Search.Limit)
set := hashset.New()
for _, tag := range tags {
set.Add(tag.BlockID)
}
var blockIDs []string
for _, v := range set.Values() {
blockIDs = append(blockIDs, v.(string))
}
sort.SliceStable(blockIDs, func(i, j int) bool {
return blockIDs[i] > blockIDs[j]
})
sqlBlocks := sql.GetBlocks(blockIDs)
blockMap := map[string]*sql.Block{}
for _, block := range sqlBlocks {
blockMap[block.ID] = block
}
for _, tag := range tags {
label := tag.Content
parentSQLBlock := blockMap[tag.BlockID]
block := fromSQLBlock(parentSQLBlock, "", 0)
if blocks, ok := ret[label]; ok {
blocks = append(blocks, block)
ret[label] = blocks
} else {
ret[label] = []*Block{block}
}
}
return
}
func labelTags() (ret map[string]Tags) {
ret = map[string]Tags{}
tagSpans := sql.QueryTagSpans("", 10240)
for _, tagSpan := range tagSpans {
label := tagSpan.Content
if _, ok := ret[label]; ok {
ret[label] = append(ret[label], &Tag{})
} else {
ret[label] = Tags{}
}
}
return
}
func appendTagChildren(tags *Tags, labels map[string]Tags) {
for _, tag := range *tags {
tag.Label = tag.Name
tag.Count = len(labels[tag.Label]) + 1
appendChildren0(tag, labels)
sortTags(tag.Children)
}
}
func appendChildren0(tag *Tag, labels map[string]Tags) {
sortTags(tag.tags)
for _, t := range tag.tags {
t.Label = tag.Label + "/" + t.Name
t.Count = len(labels[t.Label]) + 1
tag.Children = append(tag.Children, t)
}
for _, child := range tag.tags {
appendChildren0(child, labels)
}
}
func buildTags(root Tags, labels []string, depth int) Tags {
if 1 > len(labels) {
return root
}
i := 0
for ; i < len(root); i++ {
if (root)[i].Name == labels[0] {
break
}
}
if i == len(root) {
root = append(root, &Tag{Name: html.EscapeHTMLStr(labels[0]), Type: "tag", Depth: depth})
}
depth++
root[i].tags = buildTags(root[i].tags, labels[1:], depth)
return root
}

289
kernel/model/template.go Normal file
View file

@ -0,0 +1,289 @@
// SiYuan - Build Your Eternal Digital Garden
// 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 model
import (
"bytes"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"text/template"
"time"
"unicode/utf8"
"github.com/88250/lute/ast"
"github.com/88250/lute/parse"
"github.com/88250/lute/render"
"github.com/88250/protyle"
"github.com/araddon/dateparse"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
"github.com/88250/gulu"
sprig "github.com/Masterminds/sprig/v3"
"github.com/siyuan-note/siyuan/kernel/search"
"github.com/siyuan-note/siyuan/kernel/sql"
)
func RenderCreateDocNameTemplate(nameTemplate string) (ret string, err error) {
tpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate)
if nil != err {
return "", errors.New(fmt.Sprintf(Conf.Language(44), err.Error()))
}
buf := &bytes.Buffer{}
buf.Grow(4096)
err = tpl.Execute(buf, nil)
if nil != err {
return "", errors.New(fmt.Sprintf(Conf.Language(44), err.Error()))
}
ret = buf.String()
return
}
func SearchTemplate(keyword string) (ret []*Block) {
templates := filepath.Join(util.DataDir, "templates")
k := strings.ToLower(keyword)
filepath.Walk(templates, func(path string, info fs.FileInfo, err error) error {
name := strings.ToLower(info.Name())
if !strings.HasSuffix(name, ".md") {
return nil
}
if strings.HasPrefix(name, ".") || "readme.md" == name {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
if strings.Contains(name, k) {
content := strings.TrimPrefix(path, templates)
content = strings.ReplaceAll(content, "templates"+string(os.PathSeparator), "")
content = strings.TrimSuffix(content, ".md")
content = filepath.ToSlash(content)
content = content[1:]
_, content = search.MarkText(content, keyword, 32, Conf.Search.CaseSensitive)
b := &Block{Path: path, Content: content}
ret = append(ret, b)
}
return nil
})
return
}
func DocSaveAsTemplate(id string, overwrite bool) (code int, err error) {
tree, err := loadTreeByBlockID(id)
if nil != err {
return
}
var blocks []*ast.Node
// 添加 block ial后面格式化渲染需要
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || !n.IsBlock() {
return ast.WalkContinue
}
if ast.NodeBlockQueryEmbed == n.Type {
if script := n.ChildByType(ast.NodeBlockQueryEmbedScript); nil != script {
script.Tokens = bytes.ReplaceAll(script.Tokens, []byte("\n"), []byte(" "))
}
} else if ast.NodeHTMLBlock == n.Type {
n.Tokens = bytes.TrimSpace(n.Tokens)
// 使用 <div> 包裹,否则后续解析模板时会识别为行级 HTML https://github.com/siyuan-note/siyuan/issues/4244
if !bytes.HasPrefix(n.Tokens, []byte("<div>")) {
n.Tokens = append([]byte("<div>\n"), n.Tokens...)
}
if !bytes.HasSuffix(n.Tokens, []byte("</div>")) {
n.Tokens = append(n.Tokens, []byte("\n</div>")...)
}
}
n.RemoveIALAttr("updated")
if 0 < len(n.KramdownIAL) {
blocks = append(blocks, n)
}
return ast.WalkContinue
})
for _, block := range blocks {
block.InsertAfter(&ast.Node{Type: ast.NodeKramdownBlockIAL, Tokens: parse.IAL2Tokens(block.KramdownIAL)})
}
luteEngine := NewLute()
formatRenderer := render.NewFormatRenderer(tree, luteEngine.RenderOptions)
md := formatRenderer.Render()
title := tree.Root.IALAttr("title")
title = util.FilterFileName(title)
title += ".md"
savePath := filepath.Join(util.DataDir, "templates", title)
if gulu.File.IsExist(savePath) {
if !overwrite {
code = 1
return
}
}
err = os.WriteFile(savePath, md, 0644)
return
}
func RenderTemplate(p, id string) (string, error) {
return renderTemplate(p, id)
}
func renderTemplate(p, id string) (string, error) {
tree, err := loadTreeByBlockID(id)
if nil != err {
return "", err
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
return "", ErrBlockNotFound
}
block := sql.BuildBlockFromNode(node, tree)
md, err := os.ReadFile(p)
if nil != err {
return "", err
}
dataModel := map[string]string{}
var titleVar string
if nil != block {
titleVar = block.Name
if "d" == block.Type {
titleVar = block.Content
}
dataModel["title"] = titleVar
dataModel["id"] = block.ID
dataModel["name"] = block.Name
dataModel["alias"] = block.Alias
}
funcMap := sprig.TxtFuncMap()
funcMap["queryBlocks"] = func(stmt string, args ...string) (ret []*sql.Block) {
for _, arg := range args {
stmt = strings.Replace(stmt, "?", arg, 1)
}
ret = sql.SelectBlocksRawStmt(stmt, Conf.Search.Limit)
return
}
funcMap["querySpans"] = func(stmt string, args ...string) (ret []*sql.Span) {
for _, arg := range args {
stmt = strings.Replace(stmt, "?", arg, 1)
}
ret = sql.SelectSpansRawStmt(stmt, Conf.Search.Limit)
return
}
funcMap["parseTime"] = func(dateStr string) time.Time {
now := time.Now()
ret, err := dateparse.ParseIn(dateStr, now.Location())
if nil != err {
util.LogWarnf("parse date [%s] failed [%s], return current time instead", dateStr, err)
return now
}
return ret
}
goTpl := template.New("").Delims(".action{", "}")
tpl, err := goTpl.Funcs(funcMap).Parse(gulu.Str.FromBytes(md))
if nil != err {
return "", errors.New(fmt.Sprintf(Conf.Language(44), err.Error()))
}
buf := &bytes.Buffer{}
buf.Grow(4096)
if err = tpl.Execute(buf, dataModel); nil != err {
return "", errors.New(fmt.Sprintf(Conf.Language(44), err.Error()))
}
md = buf.Bytes()
tree = parseKTree(md)
if nil == tree {
msg := fmt.Sprintf("parse tree [%s] failed", p)
util.LogErrorf(msg)
return "", errors.New(msg)
}
var nodesNeedAppendChild []*ast.Node
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if "" != n.ID {
// 重新生成 ID
n.ID = ast.NewNodeID()
n.SetIALAttr("id", n.ID)
}
if (ast.NodeListItem == n.Type && (nil == n.FirstChild ||
(3 == n.ListData.Typ && (nil == n.FirstChild.Next || ast.NodeKramdownBlockIAL == n.FirstChild.Next.Type)))) ||
(ast.NodeBlockquote == n.Type && nil != n.FirstChild && nil != n.FirstChild.Next && ast.NodeKramdownBlockIAL == n.FirstChild.Next.Type) {
nodesNeedAppendChild = append(nodesNeedAppendChild, n)
}
appendRefTextRenderResultForBlockRef(n)
return ast.WalkContinue
})
for _, n := range nodesNeedAppendChild {
n.AppendChild(protyle.NewParagraph())
}
// 折叠标题导出为模板后使用会出现内容重复 https://github.com/siyuan-note/siyuan/issues/4488
ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if "1" == n.IALAttr("heading-fold") { // 为标题折叠下方块添加属性,前端渲染以后会统一做移除处理
n.SetIALAttr("status", "temp")
}
return ast.WalkContinue
})
luteEngine := NewLute()
dom := luteEngine.Tree2BlockDOM(tree, luteEngine.RenderOptions)
return dom, nil
}
func appendRefTextRenderResultForBlockRef(blockRef *ast.Node) {
if ast.NodeBlockRef != blockRef.Type {
return
}
refText := blockRef.ChildByType(ast.NodeBlockRefText)
if nil != refText {
return
}
refText = blockRef.ChildByType(ast.NodeBlockRefDynamicText)
if nil != refText {
return
}
// 动态解析渲染 ((id)) 的锚文本
// 现行版本已经不存在该语法情况,这里保留是为了迁移历史数据
refID := blockRef.ChildByType(ast.NodeBlockRefID)
text := sql.GetRefText(refID.TokensStr())
if Conf.Editor.BlockRefDynamicAnchorTextMaxLen < utf8.RuneCountInString(text) {
text = gulu.Str.SubStr(text, Conf.Editor.BlockRefDynamicAnchorTextMaxLen) + "..."
}
blockRef.AppendChild(&ast.Node{Type: ast.NodeBlockRefDynamicText, Tokens: gulu.Str.ToBytes(text)})
}

Some files were not shown because too many files have changed in this diff Show more