history.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. // SiYuan - Refactor your thinking
  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. "fmt"
  20. "io/fs"
  21. "math"
  22. "os"
  23. "path/filepath"
  24. "sort"
  25. "strconv"
  26. "strings"
  27. "time"
  28. "github.com/88250/gulu"
  29. "github.com/88250/lute"
  30. "github.com/88250/lute/ast"
  31. "github.com/88250/lute/parse"
  32. "github.com/88250/lute/render"
  33. "github.com/siyuan-note/eventbus"
  34. "github.com/siyuan-note/filelock"
  35. "github.com/siyuan-note/logging"
  36. "github.com/siyuan-note/siyuan/kernel/conf"
  37. "github.com/siyuan-note/siyuan/kernel/filesys"
  38. "github.com/siyuan-note/siyuan/kernel/search"
  39. "github.com/siyuan-note/siyuan/kernel/sql"
  40. "github.com/siyuan-note/siyuan/kernel/task"
  41. "github.com/siyuan-note/siyuan/kernel/treenode"
  42. "github.com/siyuan-note/siyuan/kernel/util"
  43. )
  44. var historyTicker = time.NewTicker(time.Minute * 10)
  45. func AutoGenerateDocHistory() {
  46. ChangeHistoryTick(Conf.Editor.GenerateHistoryInterval)
  47. for {
  48. <-historyTicker.C
  49. task.AppendTask(task.HistoryGenerateDoc, generateDocHistory)
  50. }
  51. }
  52. func generateDocHistory() {
  53. defer logging.Recover()
  54. if 1 > Conf.Editor.GenerateHistoryInterval {
  55. return
  56. }
  57. WaitForWritingFiles()
  58. for _, box := range Conf.GetOpenedBoxes() {
  59. box.generateDocHistory0()
  60. }
  61. historyDir := util.HistoryDir
  62. clearOutdatedHistoryDir(historyDir)
  63. // 以下部分是老版本的历史数据,不再保留
  64. for _, box := range Conf.GetBoxes() {
  65. historyDir = filepath.Join(util.DataDir, box.ID, ".siyuan", "history")
  66. os.RemoveAll(historyDir)
  67. }
  68. historyDir = filepath.Join(util.DataDir, "assets", ".siyuan", "history")
  69. os.RemoveAll(historyDir)
  70. historyDir = filepath.Join(util.DataDir, ".siyuan", "history")
  71. os.RemoveAll(historyDir)
  72. }
  73. func ChangeHistoryTick(minutes int) {
  74. if 0 >= minutes {
  75. minutes = 3600
  76. }
  77. historyTicker.Reset(time.Minute * time.Duration(minutes))
  78. }
  79. func ClearWorkspaceHistory() (err error) {
  80. historyDir := util.HistoryDir
  81. if gulu.File.IsDir(historyDir) {
  82. if err = os.RemoveAll(historyDir); nil != err {
  83. logging.LogErrorf("remove workspace history dir [%s] failed: %s", historyDir, err)
  84. return
  85. }
  86. logging.LogInfof("removed workspace history dir [%s]", historyDir)
  87. }
  88. sql.InitHistoryDatabase(true)
  89. // 以下部分是老版本的清理逻辑,暂时保留
  90. notebooks, err := ListNotebooks()
  91. if nil != err {
  92. return
  93. }
  94. for _, notebook := range notebooks {
  95. boxID := notebook.ID
  96. historyDir := filepath.Join(util.DataDir, boxID, ".siyuan", "history")
  97. if !gulu.File.IsDir(historyDir) {
  98. continue
  99. }
  100. if err = os.RemoveAll(historyDir); nil != err {
  101. logging.LogErrorf("remove notebook history dir [%s] failed: %s", historyDir, err)
  102. return
  103. }
  104. logging.LogInfof("removed notebook history dir [%s]", historyDir)
  105. }
  106. historyDir = filepath.Join(util.DataDir, ".siyuan", "history")
  107. if gulu.File.IsDir(historyDir) {
  108. if err = os.RemoveAll(historyDir); nil != err {
  109. logging.LogErrorf("remove data history dir [%s] failed: %s", historyDir, err)
  110. return
  111. }
  112. logging.LogInfof("removed data history dir [%s]", historyDir)
  113. }
  114. historyDir = filepath.Join(util.DataDir, "assets", ".siyuan", "history")
  115. if gulu.File.IsDir(historyDir) {
  116. if err = os.RemoveAll(historyDir); nil != err {
  117. logging.LogErrorf("remove assets history dir [%s] failed: %s", historyDir, err)
  118. return
  119. }
  120. logging.LogInfof("removed assets history dir [%s]", historyDir)
  121. }
  122. return
  123. }
  124. func GetDocHistoryContent(historyPath, keyword string) (id, rootID, content string, isLargeDoc bool, err error) {
  125. if !gulu.File.IsExist(historyPath) {
  126. return
  127. }
  128. data, err := filelock.ReadFile(historyPath)
  129. if nil != err {
  130. logging.LogErrorf("read file [%s] failed: %s", historyPath, err)
  131. return
  132. }
  133. isLargeDoc = 1024*1024*1 <= len(data)
  134. luteEngine := NewLute()
  135. historyTree, err := filesys.ParseJSONWithoutFix(data, luteEngine.ParseOptions)
  136. if nil != err {
  137. logging.LogErrorf("parse tree from file [%s] failed, remove it", historyPath)
  138. os.RemoveAll(historyPath)
  139. return
  140. }
  141. id = historyTree.Root.ID
  142. rootID = historyTree.Root.ID
  143. if !isLargeDoc {
  144. renderTree := &parse.Tree{Root: &ast.Node{Type: ast.NodeDocument}}
  145. keyword = strings.Join(strings.Split(keyword, " "), search.TermSep)
  146. keywords := search.SplitKeyword(keyword)
  147. var unlinks []*ast.Node
  148. ast.Walk(historyTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
  149. if !entering {
  150. return ast.WalkContinue
  151. }
  152. // 数据历史浏览时忽略内容块折叠状态 https://github.com/siyuan-note/siyuan/issues/5778
  153. n.RemoveIALAttr("heading-fold")
  154. n.RemoveIALAttr("fold")
  155. if 0 < len(keywords) {
  156. if markReplaceSpan(n, &unlinks, keywords, search.MarkDataType, luteEngine) {
  157. return ast.WalkContinue
  158. }
  159. }
  160. return ast.WalkContinue
  161. })
  162. for _, unlink := range unlinks {
  163. unlink.Unlink()
  164. }
  165. var appends []*ast.Node
  166. for n := historyTree.Root.FirstChild; nil != n; n = n.Next {
  167. appends = append(appends, n)
  168. }
  169. for _, n := range appends {
  170. renderTree.Root.AppendChild(n)
  171. }
  172. historyTree = renderTree
  173. }
  174. // 禁止文档历史内容可编辑 https://github.com/siyuan-note/siyuan/issues/6580
  175. luteEngine.RenderOptions.ProtyleContenteditable = false
  176. if isLargeDoc {
  177. util.PushMsg(Conf.Language(36), 5000)
  178. formatRenderer := render.NewFormatRenderer(historyTree, luteEngine.RenderOptions)
  179. content = gulu.Str.FromBytes(formatRenderer.Render())
  180. } else {
  181. content = luteEngine.Tree2BlockDOM(historyTree, luteEngine.RenderOptions)
  182. }
  183. return
  184. }
  185. func RollbackDocHistory(boxID, historyPath string) (err error) {
  186. if !gulu.File.IsExist(historyPath) {
  187. return
  188. }
  189. WaitForWritingFiles()
  190. srcPath := historyPath
  191. var destPath string
  192. baseName := filepath.Base(historyPath)
  193. id := strings.TrimSuffix(baseName, ".sy")
  194. workingDoc := treenode.GetBlockTree(id)
  195. if nil != workingDoc {
  196. if err = filelock.Remove(filepath.Join(util.DataDir, boxID, workingDoc.Path)); nil != err {
  197. return
  198. }
  199. }
  200. destPath, err = getRollbackDockPath(boxID, historyPath)
  201. if nil != err {
  202. return
  203. }
  204. if err = filelock.CopyNewtimes(srcPath, destPath); nil != err {
  205. return
  206. }
  207. util.ReloadUI()
  208. FullReindex()
  209. IncSync()
  210. return nil
  211. }
  212. func getRollbackDockPath(boxID, historyPath string) (destPath string, err error) {
  213. baseName := filepath.Base(historyPath)
  214. parentID := strings.TrimSuffix(filepath.Base(filepath.Dir(historyPath)), ".sy")
  215. parentWorkingDoc := treenode.GetBlockTree(parentID)
  216. if nil != parentWorkingDoc {
  217. // 父路径如果是文档,则恢复到父路径下
  218. parentDir := strings.TrimSuffix(parentWorkingDoc.Path, ".sy")
  219. parentDir = filepath.Join(util.DataDir, boxID, parentDir)
  220. if err = os.MkdirAll(parentDir, 0755); nil != err {
  221. return
  222. }
  223. destPath = filepath.Join(parentDir, baseName)
  224. } else {
  225. // 父路径如果不是文档,则恢复到笔记本根路径下
  226. destPath = filepath.Join(util.DataDir, boxID, baseName)
  227. }
  228. return
  229. }
  230. func RollbackAssetsHistory(historyPath string) (err error) {
  231. historyPath = filepath.Join(util.WorkspaceDir, historyPath)
  232. if !gulu.File.IsExist(historyPath) {
  233. return
  234. }
  235. from := historyPath
  236. to := filepath.Join(util.DataDir, "assets", filepath.Base(historyPath))
  237. if err = filelock.CopyNewtimes(from, to); nil != err {
  238. logging.LogErrorf("copy file [%s] to [%s] failed: %s", from, to, err)
  239. return
  240. }
  241. IncSync()
  242. util.PushMsg(Conf.Language(102), 3000)
  243. return nil
  244. }
  245. func RollbackNotebookHistory(historyPath string) (err error) {
  246. if !gulu.File.IsExist(historyPath) {
  247. return
  248. }
  249. from := historyPath
  250. to := filepath.Join(util.DataDir, filepath.Base(historyPath))
  251. if err = filelock.CopyNewtimes(from, to); nil != err {
  252. logging.LogErrorf("copy file [%s] to [%s] failed: %s", from, to, err)
  253. return
  254. }
  255. util.ReloadUI()
  256. FullReindex()
  257. IncSync()
  258. return nil
  259. }
  260. type History struct {
  261. HCreated string `json:"hCreated"`
  262. Items []*HistoryItem `json:"items"`
  263. }
  264. type HistoryItem struct {
  265. Title string `json:"title"`
  266. Path string `json:"path"`
  267. }
  268. const fileHistoryPageSize = 32
  269. func FullTextSearchHistory(query, box, op string, typ, page int) (ret []string, pageCount, totalCount int) {
  270. query = gulu.Str.RemoveInvisible(query)
  271. if "" != query && HistoryTypeDocID != typ {
  272. query = stringQuery(query)
  273. }
  274. offset := (page - 1) * fileHistoryPageSize
  275. table := "histories_fts_case_insensitive"
  276. stmt := "SELECT DISTINCT created FROM " + table + " WHERE "
  277. stmt += buildSearchHistoryQueryFilter(query, op, box, table, typ)
  278. countStmt := strings.ReplaceAll(stmt, "SELECT DISTINCT created", "SELECT COUNT(DISTINCT created) AS total")
  279. stmt += " ORDER BY created DESC LIMIT " + strconv.Itoa(fileHistoryPageSize) + " OFFSET " + strconv.Itoa(offset)
  280. result, err := sql.QueryHistory(stmt)
  281. if nil != err {
  282. return
  283. }
  284. for _, row := range result {
  285. ret = append(ret, row["created"].(string))
  286. }
  287. result, err = sql.QueryHistory(countStmt)
  288. if nil != err {
  289. return
  290. }
  291. if 1 > len(ret) {
  292. ret = []string{}
  293. }
  294. if 1 > len(result) {
  295. return
  296. }
  297. totalCount = int(result[0]["total"].(int64))
  298. pageCount = int(math.Ceil(float64(totalCount) / float64(fileHistoryPageSize)))
  299. return
  300. }
  301. func FullTextSearchHistoryItems(created, query, box, op string, typ int) (ret []*HistoryItem) {
  302. query = gulu.Str.RemoveInvisible(query)
  303. if "" != query && HistoryTypeDocID != typ {
  304. query = stringQuery(query)
  305. }
  306. table := "histories_fts_case_insensitive"
  307. stmt := "SELECT * FROM " + table + " WHERE "
  308. stmt += buildSearchHistoryQueryFilter(query, op, box, table, typ)
  309. stmt += " AND created = '" + created + "' ORDER BY created DESC LIMIT " + fmt.Sprintf("%d", fileHistoryPageSize)
  310. sqlHistories := sql.SelectHistoriesRawStmt(stmt)
  311. ret = fromSQLHistories(sqlHistories)
  312. return
  313. }
  314. func buildSearchHistoryQueryFilter(query, op, box, table string, typ int) (stmt string) {
  315. if "" != query {
  316. switch typ {
  317. case HistoryTypeDocName:
  318. stmt += table + " MATCH '{title}:(" + query + ")'"
  319. case HistoryTypeDoc:
  320. stmt += table + " MATCH '{title content}:(" + query + ")'"
  321. case HistoryTypeDocID:
  322. stmt += " id = '" + query + "'"
  323. case HistoryTypeAsset:
  324. stmt += table + " MATCH '{title content}:(" + query + ")'"
  325. }
  326. } else {
  327. stmt += "1=1"
  328. }
  329. if HistoryTypeDocName == typ || HistoryTypeDoc == typ || HistoryTypeDocID == typ {
  330. if "all" != op {
  331. stmt += " AND op = '" + op + "'"
  332. }
  333. if HistoryTypeDocName == typ || HistoryTypeDoc == typ {
  334. stmt += " AND path LIKE '%/" + box + "/%' AND path LIKE '%.sy'"
  335. }
  336. } else if HistoryTypeAsset == typ {
  337. stmt += " AND path LIKE '%/assets/%'"
  338. }
  339. return
  340. }
  341. func GetNotebookHistory() (ret []*History, err error) {
  342. ret = []*History{}
  343. historyDir := util.HistoryDir
  344. if !gulu.File.IsDir(historyDir) {
  345. return
  346. }
  347. historyNotebookConfs, err := filepath.Glob(historyDir + "/*-delete/*/.siyuan/conf.json")
  348. if nil != err {
  349. logging.LogErrorf("read dir [%s] failed: %s", historyDir, err)
  350. return
  351. }
  352. sort.Slice(historyNotebookConfs, func(i, j int) bool {
  353. iTimeDir := filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(historyNotebookConfs[i]))))
  354. jTimeDir := filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(historyNotebookConfs[j]))))
  355. return iTimeDir > jTimeDir
  356. })
  357. for _, historyNotebookConf := range historyNotebookConfs {
  358. timeDir := filepath.Base(filepath.Dir(filepath.Dir(filepath.Dir(historyNotebookConf))))
  359. t := timeDir[:strings.LastIndex(timeDir, "-")]
  360. if ti, parseErr := time.Parse("2006-01-02-150405", t); nil == parseErr {
  361. t = ti.Format("2006-01-02 15:04:05")
  362. }
  363. var c conf.BoxConf
  364. data, readErr := os.ReadFile(historyNotebookConf)
  365. if nil != readErr {
  366. logging.LogErrorf("read notebook conf [%s] failed: %s", historyNotebookConf, readErr)
  367. continue
  368. }
  369. if err = json.Unmarshal(data, &c); nil != err {
  370. logging.LogErrorf("parse notebook conf [%s] failed: %s", historyNotebookConf, err)
  371. continue
  372. }
  373. ret = append(ret, &History{
  374. HCreated: t,
  375. Items: []*HistoryItem{{
  376. Title: c.Name,
  377. Path: filepath.Dir(filepath.Dir(historyNotebookConf)),
  378. }},
  379. })
  380. }
  381. sort.Slice(ret, func(i, j int) bool {
  382. return ret[i].HCreated > ret[j].HCreated
  383. })
  384. return
  385. }
  386. func (box *Box) generateDocHistory0() {
  387. files := box.recentModifiedDocs()
  388. if 1 > len(files) {
  389. return
  390. }
  391. historyDir, err := GetHistoryDir(HistoryOpUpdate)
  392. if nil != err {
  393. logging.LogErrorf("get history dir failed: %s", err)
  394. return
  395. }
  396. for _, file := range files {
  397. historyPath := filepath.Join(historyDir, box.ID, strings.TrimPrefix(file, filepath.Join(util.DataDir, box.ID)))
  398. if err = os.MkdirAll(filepath.Dir(historyPath), 0755); nil != err {
  399. logging.LogErrorf("generate history failed: %s", err)
  400. return
  401. }
  402. var data []byte
  403. if data, err = filelock.ReadFile(file); err != nil {
  404. logging.LogErrorf("generate history failed: %s", err)
  405. return
  406. }
  407. if err = gulu.File.WriteFileSafer(historyPath, data, 0644); err != nil {
  408. logging.LogErrorf("generate history failed: %s", err)
  409. return
  410. }
  411. }
  412. indexHistoryDir(filepath.Base(historyDir), util.NewLute())
  413. return
  414. }
  415. func clearOutdatedHistoryDir(historyDir string) {
  416. if !gulu.File.IsExist(historyDir) {
  417. return
  418. }
  419. dirs, err := os.ReadDir(historyDir)
  420. if nil != err {
  421. logging.LogErrorf("clear history [%s] failed: %s", historyDir, err)
  422. return
  423. }
  424. now := time.Now()
  425. var removes []string
  426. for _, dir := range dirs {
  427. dirInfo, err := dir.Info()
  428. if nil != err {
  429. logging.LogErrorf("read history dir [%s] failed: %s", dir.Name(), err)
  430. continue
  431. }
  432. if Conf.Editor.HistoryRetentionDays < int(now.Sub(dirInfo.ModTime()).Hours()/24) {
  433. removes = append(removes, filepath.Join(historyDir, dir.Name()))
  434. }
  435. }
  436. for _, dir := range removes {
  437. if err = os.RemoveAll(dir); nil != err {
  438. logging.LogWarnf("remove history dir [%s] failed: %s", dir, err)
  439. continue
  440. }
  441. //logging.LogInfof("auto removed history dir [%s]", dir)
  442. // 清理历史库
  443. sql.DeleteHistoriesByPathPrefixQueue(dir)
  444. }
  445. }
  446. var boxLatestHistoryTime = map[string]time.Time{}
  447. func (box *Box) recentModifiedDocs() (ret []string) {
  448. latestHistoryTime := boxLatestHistoryTime[box.ID]
  449. filepath.Walk(filepath.Join(util.DataDir, box.ID), func(path string, info fs.FileInfo, err error) error {
  450. if nil == info {
  451. return nil
  452. }
  453. if isSkipFile(info.Name()) {
  454. if info.IsDir() {
  455. return filepath.SkipDir
  456. }
  457. return nil
  458. }
  459. if info.IsDir() {
  460. return nil
  461. }
  462. if info.ModTime().After(latestHistoryTime) {
  463. ret = append(ret, filepath.Join(path))
  464. }
  465. return nil
  466. })
  467. box.UpdateHistoryGenerated()
  468. return
  469. }
  470. const (
  471. HistoryOpClean = "clean"
  472. HistoryOpUpdate = "update"
  473. HistoryOpDelete = "delete"
  474. HistoryOpFormat = "format"
  475. HistoryOpSync = "sync"
  476. HistoryOpReplace = "replace"
  477. )
  478. func GetHistoryDir(suffix string) (ret string, err error) {
  479. return getHistoryDir(suffix, time.Now())
  480. }
  481. func getHistoryDir(suffix string, t time.Time) (ret string, err error) {
  482. ret = filepath.Join(util.HistoryDir, t.Format("2006-01-02-150405")+"-"+suffix)
  483. if err = os.MkdirAll(ret, 0755); nil != err {
  484. logging.LogErrorf("make history dir failed: %s", err)
  485. return
  486. }
  487. return
  488. }
  489. func ReindexHistory() {
  490. task.AppendTask(task.HistoryDatabaseIndexFull, fullReindexHistory)
  491. return
  492. }
  493. func fullReindexHistory() {
  494. historyDirs, err := os.ReadDir(util.HistoryDir)
  495. if nil != err {
  496. logging.LogErrorf("read history dir [%s] failed: %s", util.HistoryDir, err)
  497. return
  498. }
  499. util.PushMsg(Conf.Language(192), 7*1000)
  500. sql.InitHistoryDatabase(true)
  501. lutEngine := util.NewLute()
  502. for _, historyDir := range historyDirs {
  503. if !historyDir.IsDir() {
  504. continue
  505. }
  506. name := historyDir.Name()
  507. indexHistoryDir(name, lutEngine)
  508. }
  509. return
  510. }
  511. var validOps = []string{HistoryOpClean, HistoryOpUpdate, HistoryOpDelete, HistoryOpFormat, HistoryOpSync, HistoryOpReplace}
  512. const (
  513. HistoryTypeDocName = 0 // Search docs by doc name
  514. HistoryTypeDoc = 1 // Search docs by doc name and content
  515. HistoryTypeAsset = 2 // Search assets
  516. HistoryTypeDocID = 3 // Search docs by doc id
  517. )
  518. func indexHistoryDir(name string, luteEngine *lute.Lute) {
  519. defer logging.Recover()
  520. op := name[strings.LastIndex(name, "-")+1:]
  521. if !gulu.Str.Contains(op, validOps) {
  522. logging.LogWarnf("invalid history op [%s]", op)
  523. return
  524. }
  525. t := name[:strings.LastIndex(name, "-")]
  526. tt, parseErr := time.ParseInLocation("2006-01-02-150405", t, time.Local)
  527. if nil != parseErr {
  528. logging.LogWarnf("parse history dir time [%s] failed: %s", t, parseErr)
  529. return
  530. }
  531. created := fmt.Sprintf("%d", tt.Unix())
  532. entryPath := filepath.Join(util.HistoryDir, name)
  533. var docs, assets []string
  534. filepath.Walk(entryPath, func(path string, info os.FileInfo, err error) error {
  535. if strings.HasSuffix(info.Name(), ".sy") {
  536. docs = append(docs, path)
  537. } else if strings.Contains(path, "assets"+string(os.PathSeparator)) {
  538. assets = append(assets, path)
  539. }
  540. return nil
  541. })
  542. var histories []*sql.History
  543. for _, doc := range docs {
  544. tree, loadErr := loadTree(doc, luteEngine)
  545. if nil != loadErr {
  546. logging.LogErrorf("load tree [%s] failed: %s", doc, loadErr)
  547. continue
  548. }
  549. title := tree.Root.IALAttr("title")
  550. if "" == title {
  551. title = "Untitled"
  552. }
  553. content := tree.Root.Content()
  554. p := strings.TrimPrefix(doc, util.HistoryDir)
  555. p = filepath.ToSlash(p[1:])
  556. histories = append(histories, &sql.History{
  557. ID: tree.Root.ID,
  558. Type: HistoryTypeDoc,
  559. Op: op,
  560. Title: title,
  561. Content: content,
  562. Path: p,
  563. Created: created,
  564. })
  565. }
  566. for _, asset := range assets {
  567. p := strings.TrimPrefix(asset, util.HistoryDir)
  568. p = filepath.ToSlash(p[1:])
  569. _, id := util.LastID(p)
  570. if !ast.IsNodeIDPattern(id) {
  571. id = ""
  572. }
  573. histories = append(histories, &sql.History{
  574. ID: id,
  575. Type: HistoryTypeAsset,
  576. Op: op,
  577. Title: filepath.Base(asset),
  578. Path: p,
  579. Created: created,
  580. })
  581. }
  582. sql.IndexHistoriesQueue(histories)
  583. return
  584. }
  585. func fromSQLHistories(sqlHistories []*sql.History) (ret []*HistoryItem) {
  586. if 1 > len(sqlHistories) {
  587. ret = []*HistoryItem{}
  588. return
  589. }
  590. for _, sqlHistory := range sqlHistories {
  591. item := &HistoryItem{
  592. Title: sqlHistory.Title,
  593. Path: filepath.Join(util.HistoryDir, sqlHistory.Path),
  594. }
  595. if HistoryTypeAsset == sqlHistory.Type {
  596. item.Path = filepath.ToSlash(strings.TrimPrefix(item.Path, util.WorkspaceDir))
  597. }
  598. ret = append(ret, item)
  599. }
  600. return
  601. }
  602. func init() {
  603. subscribeSQLHistoryEvents()
  604. }
  605. func subscribeSQLHistoryEvents() {
  606. eventbus.Subscribe(util.EvtSQLHistoryRebuild, func() {
  607. ReindexHistory()
  608. })
  609. }