mirror of
https://github.com/siyuan-note/siyuan.git
synced 2025-05-02 23:29:27 +08:00
565 lines
14 KiB
Go
565 lines
14 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 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/filelock"
|
|
"github.com/siyuan-note/logging"
|
|
"github.com/siyuan-note/siyuan/kernel/conf"
|
|
"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()
|
|
for _, box := range Conf.GetOpenedBoxes() {
|
|
box.generateDocHistory0()
|
|
}
|
|
|
|
historyDir := util.HistoryDir
|
|
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 := util.HistoryDir
|
|
if gulu.File.IsDir(historyDir) {
|
|
if err = os.RemoveAll(historyDir); nil != err {
|
|
logging.LogErrorf("remove workspace history dir [%s] failed: %s", historyDir, err)
|
|
return
|
|
}
|
|
logging.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 {
|
|
logging.LogErrorf("remove notebook history dir [%s] failed: %s", historyDir, err)
|
|
return
|
|
}
|
|
logging.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 {
|
|
logging.LogErrorf("remove data history dir [%s] failed: %s", historyDir, err)
|
|
return
|
|
}
|
|
logging.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 {
|
|
logging.LogErrorf("remove assets history dir [%s] failed: %s", historyDir, err)
|
|
return
|
|
}
|
|
logging.LogInfof("removed assets history dir [%s]", historyDir)
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetDocHistoryContent(historyPath string) (content string, err error) {
|
|
if !gulu.File.IsExist(historyPath) {
|
|
return
|
|
}
|
|
|
|
data, err := filelock.NoLockFileRead(historyPath)
|
|
if nil != err {
|
|
logging.LogErrorf("read file [%s] failed: %s", historyPath, err)
|
|
return
|
|
}
|
|
luteEngine := NewLute()
|
|
historyTree, err := protyle.ParseJSONWithoutFix(luteEngine, data)
|
|
if nil != err {
|
|
logging.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()
|
|
writingDataLock.Lock()
|
|
|
|
srcPath := historyPath
|
|
var destPath string
|
|
baseName := filepath.Base(historyPath)
|
|
id := strings.TrimSuffix(baseName, ".sy")
|
|
|
|
filelock.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 {
|
|
writingDataLock.Unlock()
|
|
return
|
|
}
|
|
}
|
|
|
|
destPath, err = getRollbackDockPath(boxID, historyPath)
|
|
if nil != err {
|
|
writingDataLock.Unlock()
|
|
return
|
|
}
|
|
|
|
if err = gulu.File.Copy(srcPath, destPath); nil != err {
|
|
writingDataLock.Unlock()
|
|
return
|
|
}
|
|
writingDataLock.Unlock()
|
|
|
|
RefreshFileTree()
|
|
IncSync()
|
|
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 {
|
|
logging.LogErrorf("copy file [%s] to [%s] failed: %s", from, to, err)
|
|
return
|
|
}
|
|
IncSync()
|
|
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 {
|
|
logging.LogErrorf("copy file [%s] to [%s] failed: %s", from, to, err)
|
|
return
|
|
}
|
|
|
|
RefreshFileTree()
|
|
IncSync()
|
|
return nil
|
|
}
|
|
|
|
type History struct {
|
|
HCreated string `json:"hCreated"`
|
|
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 := util.HistoryDir
|
|
if !gulu.File.IsDir(historyDir) {
|
|
return
|
|
}
|
|
|
|
historyBoxDirs, err := filepath.Glob(historyDir + "/*/" + boxID)
|
|
if nil != err {
|
|
logging.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 := filelock.NoLockFileRead(path)
|
|
if nil != err {
|
|
logging.LogErrorf("read file [%s] failed: %s", path, err)
|
|
return nil
|
|
}
|
|
historyTree, err := protyle.ParseJSONWithoutFix(luteEngine, data)
|
|
if nil != err {
|
|
logging.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{
|
|
HCreated: t,
|
|
Items: docs,
|
|
})
|
|
|
|
count++
|
|
if maxHistory <= count {
|
|
break
|
|
}
|
|
}
|
|
|
|
sort.Slice(ret, func(i, j int) bool {
|
|
return ret[i].HCreated > ret[j].HCreated
|
|
})
|
|
return
|
|
}
|
|
|
|
func GetNotebookHistory() (ret []*History, err error) {
|
|
ret = []*History{}
|
|
|
|
historyDir := util.HistoryDir
|
|
if !gulu.File.IsDir(historyDir) {
|
|
return
|
|
}
|
|
|
|
historyNotebookConfs, err := filepath.Glob(historyDir + "/*-delete/*/.siyuan/conf.json")
|
|
if nil != err {
|
|
logging.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 {
|
|
logging.LogErrorf("read notebook conf [%s] failed: %s", historyNotebookConf, readErr)
|
|
continue
|
|
}
|
|
if err = json.Unmarshal(data, &c); nil != err {
|
|
logging.LogErrorf("parse notebook conf [%s] failed: %s", historyNotebookConf, err)
|
|
continue
|
|
}
|
|
|
|
ret = append(ret, &History{
|
|
HCreated: 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].HCreated > ret[j].HCreated
|
|
})
|
|
return
|
|
}
|
|
|
|
func GetAssetsHistory() (ret []*History, err error) {
|
|
ret = []*History{}
|
|
|
|
historyDir := util.HistoryDir
|
|
if !gulu.File.IsDir(historyDir) {
|
|
return
|
|
}
|
|
|
|
historyAssetsDirs, err := filepath.Glob(historyDir + "/*/assets")
|
|
if nil != err {
|
|
logging.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{
|
|
HCreated: t,
|
|
Items: assets,
|
|
})
|
|
|
|
historyCount++
|
|
if maxHistory <= historyCount {
|
|
break
|
|
}
|
|
}
|
|
|
|
sort.Slice(ret, func(i, j int) bool {
|
|
return ret[i].HCreated > ret[j].HCreated
|
|
})
|
|
return
|
|
}
|
|
|
|
func (box *Box) generateDocHistory0() {
|
|
files := box.recentModifiedDocs()
|
|
if 1 > len(files) {
|
|
return
|
|
}
|
|
|
|
historyDir, err := util.GetHistoryDir("update")
|
|
if nil != err {
|
|
logging.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 {
|
|
logging.LogErrorf("generate history failed: %s", err)
|
|
return
|
|
}
|
|
|
|
var data []byte
|
|
if data, err = filelock.NoLockFileRead(file); err != nil {
|
|
logging.LogErrorf("generate history failed: %s", err)
|
|
return
|
|
}
|
|
|
|
if err = gulu.File.WriteFileSafer(historyPath, data, 0644); err != nil {
|
|
logging.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 {
|
|
logging.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 {
|
|
logging.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 {
|
|
logging.LogErrorf("remove history dir [%s] failed: %s", dir, err)
|
|
continue
|
|
}
|
|
//logging.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
|
|
}
|