history.go 24 KB

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