history.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. // SiYuan - Build Your Eternal Digital Garden
  2. // Copyright (c) 2020-present, b3log.org
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. package model
  17. import (
  18. "encoding/json"
  19. "io"
  20. "io/fs"
  21. "os"
  22. "path/filepath"
  23. "sort"
  24. "strings"
  25. "time"
  26. "github.com/88250/gulu"
  27. "github.com/88250/protyle"
  28. "github.com/siyuan-note/siyuan/kernel/conf"
  29. "github.com/siyuan-note/siyuan/kernel/filesys"
  30. "github.com/siyuan-note/siyuan/kernel/treenode"
  31. "github.com/siyuan-note/siyuan/kernel/util"
  32. )
  33. var historyTicker = time.NewTicker(time.Minute * 10)
  34. func AutoGenerateDocHistory() {
  35. ChangeHistoryTick(Conf.Editor.GenerateHistoryInterval)
  36. for {
  37. <-historyTicker.C
  38. generateDocHistory()
  39. }
  40. }
  41. func generateDocHistory() {
  42. if 1 > Conf.Editor.GenerateHistoryInterval {
  43. return
  44. }
  45. WaitForWritingFiles()
  46. for _, box := range Conf.GetOpenedBoxes() {
  47. box.generateDocHistory0()
  48. }
  49. historyDir := filepath.Join(util.WorkspaceDir, "history")
  50. clearOutdatedHistoryDir(historyDir)
  51. // 以下部分是老版本的清理逻辑,暂时保留
  52. for _, box := range Conf.GetBoxes() {
  53. historyDir = filepath.Join(util.DataDir, box.ID, ".siyuan", "history")
  54. clearOutdatedHistoryDir(historyDir)
  55. }
  56. historyDir = filepath.Join(util.DataDir, "assets", ".siyuan", "history")
  57. clearOutdatedHistoryDir(historyDir)
  58. historyDir = filepath.Join(util.DataDir, ".siyuan", "history")
  59. clearOutdatedHistoryDir(historyDir)
  60. }
  61. func ChangeHistoryTick(minutes int) {
  62. if 0 >= minutes {
  63. minutes = 3600
  64. }
  65. historyTicker.Reset(time.Minute * time.Duration(minutes))
  66. }
  67. func ClearWorkspaceHistory() (err error) {
  68. historyDir := filepath.Join(util.WorkspaceDir, "history")
  69. if gulu.File.IsDir(historyDir) {
  70. if err = os.RemoveAll(historyDir); nil != err {
  71. util.LogErrorf("remove workspace history dir [%s] failed: %s", historyDir, err)
  72. return
  73. }
  74. util.LogInfof("removed workspace history dir [%s]", historyDir)
  75. }
  76. // 以下部分是老版本的清理逻辑,暂时保留
  77. notebooks, err := ListNotebooks()
  78. if nil != err {
  79. return
  80. }
  81. for _, notebook := range notebooks {
  82. boxID := notebook.ID
  83. historyDir := filepath.Join(util.DataDir, boxID, ".siyuan", "history")
  84. if !gulu.File.IsDir(historyDir) {
  85. continue
  86. }
  87. if err = os.RemoveAll(historyDir); nil != err {
  88. util.LogErrorf("remove notebook history dir [%s] failed: %s", historyDir, err)
  89. return
  90. }
  91. util.LogInfof("removed notebook history dir [%s]", historyDir)
  92. }
  93. historyDir = filepath.Join(util.DataDir, ".siyuan", "history")
  94. if gulu.File.IsDir(historyDir) {
  95. if err = os.RemoveAll(historyDir); nil != err {
  96. util.LogErrorf("remove data history dir [%s] failed: %s", historyDir, err)
  97. return
  98. }
  99. util.LogInfof("removed data history dir [%s]", historyDir)
  100. }
  101. historyDir = filepath.Join(util.DataDir, "assets", ".siyuan", "history")
  102. if gulu.File.IsDir(historyDir) {
  103. if err = os.RemoveAll(historyDir); nil != err {
  104. util.LogErrorf("remove assets history dir [%s] failed: %s", historyDir, err)
  105. return
  106. }
  107. util.LogInfof("removed assets history dir [%s]", historyDir)
  108. }
  109. return
  110. }
  111. func GetDocHistoryContent(historyPath string) (content string, err error) {
  112. if !gulu.File.IsExist(historyPath) {
  113. return
  114. }
  115. data, err := filesys.NoLockFileRead(historyPath)
  116. if nil != err {
  117. util.LogErrorf("read file [%s] failed: %s", historyPath, err)
  118. return
  119. }
  120. luteEngine := NewLute()
  121. historyTree, err := protyle.ParseJSONWithoutFix(luteEngine, data)
  122. if nil != err {
  123. util.LogErrorf("parse tree from file [%s] failed, remove it", historyPath)
  124. os.RemoveAll(historyPath)
  125. return
  126. }
  127. content = renderBlockMarkdown(historyTree.Root)
  128. return
  129. }
  130. func RollbackDocHistory(boxID, historyPath string) (err error) {
  131. if !gulu.File.IsExist(historyPath) {
  132. return
  133. }
  134. WaitForWritingFiles()
  135. writingDataLock.Lock()
  136. srcPath := historyPath
  137. var destPath string
  138. baseName := filepath.Base(historyPath)
  139. id := strings.TrimSuffix(baseName, ".sy")
  140. filesys.ReleaseFileLocks(filepath.Join(util.DataDir, boxID))
  141. workingDoc := treenode.GetBlockTree(id)
  142. if nil != workingDoc {
  143. if err = os.RemoveAll(filepath.Join(util.DataDir, boxID, workingDoc.Path)); nil != err {
  144. writingDataLock.Unlock()
  145. return
  146. }
  147. }
  148. destPath, err = getRollbackDockPath(boxID, historyPath)
  149. if nil != err {
  150. writingDataLock.Unlock()
  151. return
  152. }
  153. if err = gulu.File.Copy(srcPath, destPath); nil != err {
  154. writingDataLock.Unlock()
  155. return
  156. }
  157. writingDataLock.Unlock()
  158. RefreshFileTree()
  159. IncWorkspaceDataVer()
  160. return nil
  161. }
  162. func getRollbackDockPath(boxID, historyPath string) (destPath string, err error) {
  163. baseName := filepath.Base(historyPath)
  164. parentID := strings.TrimSuffix(filepath.Base(filepath.Dir(historyPath)), ".sy")
  165. parentWorkingDoc := treenode.GetBlockTree(parentID)
  166. if nil != parentWorkingDoc {
  167. // 父路径如果是文档,则恢复到父路径下
  168. parentDir := strings.TrimSuffix(parentWorkingDoc.Path, ".sy")
  169. parentDir = filepath.Join(util.DataDir, boxID, parentDir)
  170. if err = os.MkdirAll(parentDir, 0755); nil != err {
  171. return
  172. }
  173. destPath = filepath.Join(parentDir, baseName)
  174. } else {
  175. // 父路径如果不是文档,则恢复到笔记本根路径下
  176. destPath = filepath.Join(util.DataDir, boxID, baseName)
  177. }
  178. return
  179. }
  180. func RollbackAssetsHistory(historyPath string) (err error) {
  181. historyPath = filepath.Join(util.WorkspaceDir, historyPath)
  182. if !gulu.File.IsExist(historyPath) {
  183. return
  184. }
  185. from := historyPath
  186. to := filepath.Join(util.DataDir, "assets", filepath.Base(historyPath))
  187. if err = gulu.File.Copy(from, to); nil != err {
  188. util.LogErrorf("copy file [%s] to [%s] failed: %s", from, to, err)
  189. return
  190. }
  191. IncWorkspaceDataVer()
  192. return nil
  193. }
  194. func RollbackNotebookHistory(historyPath string) (err error) {
  195. if !gulu.File.IsExist(historyPath) {
  196. return
  197. }
  198. from := historyPath
  199. to := filepath.Join(util.DataDir, filepath.Base(historyPath))
  200. if err = gulu.File.Copy(from, to); nil != err {
  201. util.LogErrorf("copy file [%s] to [%s] failed: %s", from, to, err)
  202. return
  203. }
  204. RefreshFileTree()
  205. IncWorkspaceDataVer()
  206. return nil
  207. }
  208. type History struct {
  209. Time string `json:"time"`
  210. Items []*HistoryItem `json:"items"`
  211. }
  212. type HistoryItem struct {
  213. Title string `json:"title"`
  214. Path string `json:"path"`
  215. }
  216. const maxHistory = 32
  217. func GetDocHistory(boxID string) (ret []*History, err error) {
  218. ret = []*History{}
  219. historyDir := filepath.Join(util.WorkspaceDir, "history")
  220. if !gulu.File.IsDir(historyDir) {
  221. return
  222. }
  223. historyBoxDirs, err := filepath.Glob(historyDir + "/*/" + boxID)
  224. if nil != err {
  225. util.LogErrorf("read dir [%s] failed: %s", historyDir, err)
  226. return
  227. }
  228. sort.Slice(historyBoxDirs, func(i, j int) bool {
  229. return historyBoxDirs[i] > historyBoxDirs[j]
  230. })
  231. luteEngine := NewLute()
  232. count := 0
  233. for _, historyBoxDir := range historyBoxDirs {
  234. var docs []*HistoryItem
  235. itemCount := 0
  236. filepath.Walk(historyBoxDir, func(path string, info fs.FileInfo, err error) error {
  237. if info.IsDir() {
  238. return nil
  239. }
  240. if !strings.HasSuffix(info.Name(), ".sy") {
  241. return nil
  242. }
  243. data, err := filesys.NoLockFileRead(path)
  244. if nil != err {
  245. util.LogErrorf("read file [%s] failed: %s", path, err)
  246. return nil
  247. }
  248. historyTree, err := protyle.ParseJSONWithoutFix(luteEngine, data)
  249. if nil != err {
  250. util.LogErrorf("parse tree from file [%s] failed, remove it", path)
  251. os.RemoveAll(path)
  252. return nil
  253. }
  254. historyName := historyTree.Root.IALAttr("title")
  255. if "" == historyName {
  256. historyName = info.Name()
  257. }
  258. docs = append(docs, &HistoryItem{
  259. Title: historyTree.Root.IALAttr("title"),
  260. Path: path,
  261. })
  262. itemCount++
  263. if maxHistory < itemCount {
  264. return io.EOF
  265. }
  266. return nil
  267. })
  268. if 1 > len(docs) {
  269. continue
  270. }
  271. timeDir := filepath.Base(filepath.Dir(historyBoxDir))
  272. t := timeDir[:strings.LastIndex(timeDir, "-")]
  273. if ti, parseErr := time.Parse("2006-01-02-150405", t); nil == parseErr {
  274. t = ti.Format("2006-01-02 15:04:05")
  275. }
  276. ret = append(ret, &History{
  277. Time: t,
  278. Items: docs,
  279. })
  280. count++
  281. if maxHistory <= count {
  282. break
  283. }
  284. }
  285. sort.Slice(ret, func(i, j int) bool {
  286. return ret[i].Time > ret[j].Time
  287. })
  288. return
  289. }
  290. func GetNotebookHistory() (ret []*History, err error) {
  291. ret = []*History{}
  292. historyDir := filepath.Join(util.WorkspaceDir, "history")
  293. if !gulu.File.IsDir(historyDir) {
  294. return
  295. }
  296. historyNotebookConfs, err := filepath.Glob(historyDir + "/*-delete/*/.siyuan/conf.json")
  297. if nil != err {
  298. util.LogErrorf("read dir [%s] failed: %s", historyDir, err)
  299. return
  300. }
  301. sort.Slice(historyNotebookConfs, func(i, j int) bool {
  302. iTimeDir := filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(historyNotebookConfs[i]))))
  303. jTimeDir := filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(historyNotebookConfs[j]))))
  304. return iTimeDir > jTimeDir
  305. })
  306. historyCount := 0
  307. for _, historyNotebookConf := range historyNotebookConfs {
  308. timeDir := filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(historyNotebookConf))))
  309. t := timeDir[:strings.LastIndex(timeDir, "-")]
  310. if ti, parseErr := time.Parse("2006-01-02-150405", t); nil == parseErr {
  311. t = ti.Format("2006-01-02 15:04:05")
  312. }
  313. var c conf.BoxConf
  314. data, readErr := os.ReadFile(historyNotebookConf)
  315. if nil != readErr {
  316. util.LogErrorf("read notebook conf [%s] failed: %s", historyNotebookConf, readErr)
  317. continue
  318. }
  319. if err = json.Unmarshal(data, &c); nil != err {
  320. util.LogErrorf("parse notebook conf [%s] failed: %s", historyNotebookConf, err)
  321. continue
  322. }
  323. ret = append(ret, &History{
  324. Time: t,
  325. Items: []*HistoryItem{
  326. {
  327. Title: c.Name,
  328. Path: filepath.Dir(filepath.Dir(historyNotebookConf)),
  329. },
  330. },
  331. })
  332. historyCount++
  333. if maxHistory <= historyCount {
  334. break
  335. }
  336. }
  337. sort.Slice(ret, func(i, j int) bool {
  338. return ret[i].Time > ret[j].Time
  339. })
  340. return
  341. }
  342. func GetAssetsHistory() (ret []*History, err error) {
  343. ret = []*History{}
  344. historyDir := filepath.Join(util.WorkspaceDir, "history")
  345. if !gulu.File.IsDir(historyDir) {
  346. return
  347. }
  348. historyAssetsDirs, err := filepath.Glob(historyDir + "/*/assets")
  349. if nil != err {
  350. util.LogErrorf("read dir [%s] failed: %s", historyDir, err)
  351. return
  352. }
  353. sort.Slice(historyAssetsDirs, func(i, j int) bool {
  354. return historyAssetsDirs[i] > historyAssetsDirs[j]
  355. })
  356. historyCount := 0
  357. for _, historyAssetsDir := range historyAssetsDirs {
  358. var assets []*HistoryItem
  359. itemCount := 0
  360. filepath.Walk(historyAssetsDir, func(path string, info fs.FileInfo, err error) error {
  361. if isSkipFile(info.Name()) {
  362. if info.IsDir() {
  363. return filepath.SkipDir
  364. }
  365. return nil
  366. }
  367. if info.IsDir() {
  368. return nil
  369. }
  370. assets = append(assets, &HistoryItem{
  371. Title: info.Name(),
  372. Path: filepath.ToSlash(strings.TrimPrefix(path, util.WorkspaceDir)),
  373. })
  374. itemCount++
  375. if maxHistory < itemCount {
  376. return io.EOF
  377. }
  378. return nil
  379. })
  380. if 1 > len(assets) {
  381. continue
  382. }
  383. timeDir := filepath.Base(filepath.Dir(historyAssetsDir))
  384. t := timeDir[:strings.LastIndex(timeDir, "-")]
  385. if ti, parseErr := time.Parse("2006-01-02-150405", t); nil == parseErr {
  386. t = ti.Format("2006-01-02 15:04:05")
  387. }
  388. ret = append(ret, &History{
  389. Time: t,
  390. Items: assets,
  391. })
  392. historyCount++
  393. if maxHistory <= historyCount {
  394. break
  395. }
  396. }
  397. sort.Slice(ret, func(i, j int) bool {
  398. return ret[i].Time > ret[j].Time
  399. })
  400. return
  401. }
  402. func (box *Box) generateDocHistory0() {
  403. files := box.recentModifiedDocs()
  404. if 1 > len(files) {
  405. return
  406. }
  407. historyDir, err := util.GetHistoryDir("update")
  408. if nil != err {
  409. util.LogErrorf("get history dir failed: %s", err)
  410. return
  411. }
  412. for _, file := range files {
  413. historyPath := filepath.Join(historyDir, box.ID, strings.TrimPrefix(file, filepath.Join(util.DataDir, box.ID)))
  414. if err = os.MkdirAll(filepath.Dir(historyPath), 0755); nil != err {
  415. util.LogErrorf("generate history failed: %s", err)
  416. return
  417. }
  418. var data []byte
  419. if data, err = filesys.NoLockFileRead(file); err != nil {
  420. util.LogErrorf("generate history failed: %s", err)
  421. return
  422. }
  423. if err = gulu.File.WriteFileSafer(historyPath, data, 0644); err != nil {
  424. util.LogErrorf("generate history failed: %s", err)
  425. return
  426. }
  427. }
  428. return
  429. }
  430. func clearOutdatedHistoryDir(historyDir string) {
  431. if !gulu.File.IsExist(historyDir) {
  432. return
  433. }
  434. dirs, err := os.ReadDir(historyDir)
  435. if nil != err {
  436. util.LogErrorf("clear history [%s] failed: %s", historyDir, err)
  437. return
  438. }
  439. now := time.Now()
  440. var removes []string
  441. for _, dir := range dirs {
  442. dirInfo, err := dir.Info()
  443. if nil != err {
  444. util.LogErrorf("read history dir [%s] failed: %s", dir.Name(), err)
  445. continue
  446. }
  447. if Conf.Editor.HistoryRetentionDays < int(now.Sub(dirInfo.ModTime()).Hours()/24) {
  448. removes = append(removes, filepath.Join(historyDir, dir.Name()))
  449. }
  450. }
  451. for _, dir := range removes {
  452. if err = os.RemoveAll(dir); nil != err {
  453. util.LogErrorf("remove history dir [%s] failed: %s", err)
  454. continue
  455. }
  456. //util.LogInfof("auto removed history dir [%s]", dir)
  457. }
  458. }
  459. var boxLatestHistoryTime = map[string]time.Time{}
  460. func (box *Box) recentModifiedDocs() (ret []string) {
  461. latestHistoryTime := boxLatestHistoryTime[box.ID]
  462. filepath.Walk(filepath.Join(util.DataDir, box.ID), func(path string, info fs.FileInfo, err error) error {
  463. if nil == info {
  464. return nil
  465. }
  466. if isSkipFile(info.Name()) {
  467. if info.IsDir() {
  468. return filepath.SkipDir
  469. }
  470. return nil
  471. }
  472. if info.IsDir() {
  473. return nil
  474. }
  475. if info.ModTime().After(latestHistoryTime) {
  476. ret = append(ret, filepath.Join(path))
  477. }
  478. return nil
  479. })
  480. box.UpdateHistoryGenerated()
  481. return
  482. }