200 lines
5.4 KiB
Go
200 lines
5.4 KiB
Go
// 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
|
|
}
|