sync.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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. "errors"
  19. "fmt"
  20. "os"
  21. "os/exec"
  22. "path/filepath"
  23. "strings"
  24. "sync"
  25. "time"
  26. "unicode/utf8"
  27. "github.com/88250/gulu"
  28. "github.com/dustin/go-humanize"
  29. "github.com/siyuan-note/dejavu"
  30. "github.com/siyuan-note/siyuan/kernel/sql"
  31. "github.com/siyuan-note/siyuan/kernel/treenode"
  32. "github.com/siyuan-note/siyuan/kernel/util"
  33. )
  34. var (
  35. syncSameCount = 0
  36. syncDownloadErrCount = 0
  37. fixSyncInterval = 5 * time.Minute
  38. syncPlanTime = time.Now().Add(fixSyncInterval)
  39. BootSyncSucc = -1 // -1:未执行,0:执行成功,1:执行失败
  40. ExitSyncSucc = -1
  41. )
  42. func AutoSync() {
  43. for {
  44. time.Sleep(5 * time.Second)
  45. if time.Now().After(syncPlanTime) {
  46. SyncData(false, false, false)
  47. }
  48. }
  49. }
  50. func SyncData(boot, exit, byHand bool) {
  51. defer util.Recover()
  52. if !boot && !exit && 2 == Conf.Sync.Mode && !byHand {
  53. return
  54. }
  55. if util.IsMutexLocked(&syncLock) {
  56. util.LogWarnf("sync is in progress")
  57. planSyncAfter(30 * time.Second)
  58. return
  59. }
  60. if boot {
  61. util.IncBootProgress(3, "Syncing data from the cloud...")
  62. BootSyncSucc = 0
  63. }
  64. if exit {
  65. ExitSyncSucc = 0
  66. }
  67. if !IsSubscriber() || !Conf.Sync.Enabled || "" == Conf.Sync.CloudName {
  68. if byHand {
  69. if "" == Conf.Sync.CloudName {
  70. util.PushMsg(Conf.Language(123), 5000)
  71. } else if !Conf.Sync.Enabled {
  72. util.PushMsg(Conf.Language(124), 5000)
  73. }
  74. }
  75. return
  76. }
  77. if !IsValidCloudDirName(Conf.Sync.CloudName) {
  78. return
  79. }
  80. if boot {
  81. util.LogInfof("sync before boot")
  82. }
  83. if exit {
  84. util.LogInfof("sync before exit")
  85. util.PushMsg(Conf.Language(81), 1000*60*15)
  86. }
  87. if 7 < syncDownloadErrCount && !byHand {
  88. util.LogErrorf("sync download error too many times, cancel auto sync, try to sync by hand")
  89. util.PushErrMsg(Conf.Language(125), 1000*60*60)
  90. planSyncAfter(64 * time.Minute)
  91. return
  92. }
  93. now := util.CurrentTimeMillis()
  94. Conf.Sync.Synced = now
  95. util.BroadcastByType("main", "syncing", 0, Conf.Language(81), nil)
  96. defer func() {
  97. synced := util.Millisecond2Time(Conf.Sync.Synced).Format("2006-01-02 15:04:05") + "\n\n" + Conf.Sync.Stat
  98. msg := fmt.Sprintf(Conf.Language(82), synced)
  99. Conf.Sync.Stat = msg
  100. Conf.Save()
  101. util.BroadcastByType("main", "syncing", 1, msg, nil)
  102. }()
  103. syncRepo(boot, exit, byHand)
  104. return
  105. }
  106. // incReindex 增量重建索引。
  107. func incReindex(upserts, removes []string) {
  108. needPushRemoveProgress := 32 < len(removes)
  109. needPushUpsertProgress := 32 < len(upserts)
  110. // 先执行 remove,否则移动文档时 upsert 会被忽略,导致未被索引
  111. for _, removeFile := range removes {
  112. if !strings.HasSuffix(removeFile, ".sy") {
  113. continue
  114. }
  115. id := strings.TrimSuffix(filepath.Base(removeFile), ".sy")
  116. block := treenode.GetBlockTree(id)
  117. if nil != block {
  118. treenode.RemoveBlockTreesByRootID(block.RootID)
  119. sql.RemoveTreeQueue(block.BoxID, block.RootID)
  120. msg := fmt.Sprintf("Sync remove tree [%s]", block.RootID)
  121. util.PushStatusBar(msg)
  122. if needPushRemoveProgress {
  123. util.PushEndlessProgress(msg)
  124. }
  125. }
  126. }
  127. for _, upsertFile := range upserts {
  128. if !strings.HasSuffix(upsertFile, ".sy") {
  129. continue
  130. }
  131. upsertFile = filepath.ToSlash(upsertFile)
  132. if strings.HasPrefix(upsertFile, "/") {
  133. upsertFile = upsertFile[1:]
  134. }
  135. idx := strings.Index(upsertFile, "/")
  136. if 0 > idx {
  137. // .sy 直接出现在 data 文件夹下,没有出现在笔记本文件夹下的情况
  138. continue
  139. }
  140. box := upsertFile[:idx]
  141. p := strings.TrimPrefix(upsertFile, box)
  142. tree, err0 := LoadTree(box, p)
  143. if nil != err0 {
  144. continue
  145. }
  146. treenode.ReindexBlockTree(tree)
  147. sql.UpsertTreeQueue(tree)
  148. msg := fmt.Sprintf("Sync reindex tree [%s]", tree.ID)
  149. util.PushStatusBar(msg)
  150. if needPushUpsertProgress {
  151. util.PushEndlessProgress(msg)
  152. }
  153. }
  154. if needPushRemoveProgress || needPushUpsertProgress {
  155. util.PushClearProgress()
  156. }
  157. }
  158. func SetCloudSyncDir(name string) {
  159. if Conf.Sync.CloudName == name {
  160. return
  161. }
  162. syncLock.Lock()
  163. defer syncLock.Unlock()
  164. Conf.Sync.CloudName = name
  165. Conf.Save()
  166. }
  167. func SetSyncEnable(b bool) (err error) {
  168. syncLock.Lock()
  169. defer syncLock.Unlock()
  170. Conf.Sync.Enabled = b
  171. Conf.Save()
  172. return
  173. }
  174. func SetSyncMode(mode int) (err error) {
  175. syncLock.Lock()
  176. defer syncLock.Unlock()
  177. Conf.Sync.Mode = mode
  178. Conf.Save()
  179. return
  180. }
  181. var syncLock = sync.Mutex{}
  182. func CreateCloudSyncDir(name string) (err error) {
  183. syncLock.Lock()
  184. defer syncLock.Unlock()
  185. name = strings.TrimSpace(name)
  186. name = gulu.Str.RemoveInvisible(name)
  187. if !IsValidCloudDirName(name) {
  188. return errors.New(Conf.Language(37))
  189. }
  190. var cloudInfo *dejavu.CloudInfo
  191. cloudInfo, err = buildCloudInfo()
  192. if nil != err {
  193. return
  194. }
  195. err = dejavu.CreateCloudRepo(name, cloudInfo)
  196. return
  197. }
  198. func RemoveCloudSyncDir(name string) (err error) {
  199. syncLock.Lock()
  200. defer syncLock.Unlock()
  201. if "" == name {
  202. return
  203. }
  204. var cloudInfo *dejavu.CloudInfo
  205. cloudInfo, err = buildCloudInfo()
  206. if nil != err {
  207. return
  208. }
  209. err = dejavu.RemoveCloudRepo(name, cloudInfo)
  210. if nil != err {
  211. return
  212. }
  213. if Conf.Sync.CloudName == name {
  214. Conf.Sync.CloudName = "main"
  215. Conf.Save()
  216. util.PushMsg(Conf.Language(155), 5000)
  217. }
  218. return
  219. }
  220. func ListCloudSyncDir() (syncDirs []*Sync, hSize string, err error) {
  221. syncDirs = []*Sync{}
  222. var dirs []map[string]interface{}
  223. var size int64
  224. var cloudInfo *dejavu.CloudInfo
  225. cloudInfo, err = buildCloudInfo()
  226. if nil != err {
  227. return
  228. }
  229. dirs, size, err = dejavu.GetCloudRepos(cloudInfo)
  230. if nil != err {
  231. return
  232. }
  233. for _, d := range dirs {
  234. dirSize := int64(d["size"].(float64))
  235. syncDirs = append(syncDirs, &Sync{
  236. Size: dirSize,
  237. HSize: humanize.Bytes(uint64(dirSize)),
  238. Updated: d["updated"].(string),
  239. CloudName: d["name"].(string),
  240. })
  241. }
  242. hSize = humanize.Bytes(uint64(size))
  243. return
  244. }
  245. func formatErrorMsg(err error) string {
  246. msg := err.Error()
  247. if strings.Contains(msg, "Permission denied") || strings.Contains(msg, "Access is denied") {
  248. msg = Conf.Language(33) + " " + err.Error()
  249. } else if strings.Contains(msg, "Device or resource busy") {
  250. msg = Conf.Language(85) + " " + err.Error()
  251. } else if strings.Contains(msg, "cipher: message authentication failed") {
  252. msg = Conf.Language(172) + " " + err.Error()
  253. }
  254. msg = msg + " v" + util.Ver
  255. return msg
  256. }
  257. func IsValidCloudDirName(cloudDirName string) bool {
  258. if 16 < utf8.RuneCountInString(cloudDirName) || 1 > utf8.RuneCountInString(cloudDirName) {
  259. return false
  260. }
  261. chars := []byte{'~', '`', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', '=',
  262. '[', ']', '{', '}', '\\', '|', ';', ':', '\'', '"', '<', ',', '>', '.', '?', '/', ' '}
  263. var charsStr string
  264. for _, char := range chars {
  265. charsStr += string(char)
  266. }
  267. if strings.ContainsAny(cloudDirName, charsStr) {
  268. return false
  269. }
  270. return true
  271. }
  272. func getIgnoreLines() (ret []string) {
  273. ignore := filepath.Join(util.DataDir, ".siyuan", "syncignore")
  274. err := os.MkdirAll(filepath.Dir(ignore), 0755)
  275. if nil != err {
  276. return
  277. }
  278. if !gulu.File.IsExist(ignore) {
  279. if err = gulu.File.WriteFileSafer(ignore, nil, 0644); nil != err {
  280. util.LogErrorf("create syncignore [%s] failed: %s", ignore, err)
  281. return
  282. }
  283. }
  284. data, err := os.ReadFile(ignore)
  285. if nil != err {
  286. util.LogErrorf("read syncignore [%s] failed: %s", ignore, err)
  287. return
  288. }
  289. dataStr := string(data)
  290. dataStr = strings.ReplaceAll(dataStr, "\r\n", "\n")
  291. ret = strings.Split(dataStr, "\n")
  292. // 默认忽略帮助文档
  293. ret = append(ret, "20210808180117-6v0mkxr/**/*")
  294. ret = append(ret, "20210808180117-czj9bvb/**/*")
  295. ret = append(ret, "20211226090932-5lcq56f/**/*")
  296. ret = gulu.Str.RemoveDuplicatedElem(ret)
  297. return
  298. }
  299. func IncSync() {
  300. syncSameCount = 0
  301. planSyncAfter(30 * time.Second)
  302. }
  303. func stableCopy(src, dest string) (err error) {
  304. if gulu.OS.IsWindows() {
  305. robocopy := "robocopy"
  306. cmd := exec.Command(robocopy, src, dest, "/DCOPY:T", "/E", "/IS", "/R:0", "/NFL", "/NDL", "/NJH", "/NJS", "/NP", "/NS", "/NC")
  307. util.CmdAttr(cmd)
  308. var output []byte
  309. output, err = cmd.CombinedOutput()
  310. if strings.Contains(err.Error(), "exit status 16") {
  311. // 某些版本的 Windows 无法同步 https://github.com/siyuan-note/siyuan/issues/4197
  312. return gulu.File.Copy(src, dest)
  313. }
  314. if nil != err && strings.Contains(err.Error(), exec.ErrNotFound.Error()) {
  315. robocopy = os.Getenv("SystemRoot") + "\\System32\\" + "robocopy"
  316. cmd = exec.Command(robocopy, src, dest, "/DCOPY:T", "/E", "/IS", "/R:0", "/NFL", "/NDL", "/NJH", "/NJS", "/NP", "/NS", "/NC")
  317. util.CmdAttr(cmd)
  318. output, err = cmd.CombinedOutput()
  319. }
  320. if nil == err ||
  321. strings.Contains(err.Error(), "exit status 3") ||
  322. strings.Contains(err.Error(), "exit status 1") ||
  323. strings.Contains(err.Error(), "exit status 2") ||
  324. strings.Contains(err.Error(), "exit status 5") ||
  325. strings.Contains(err.Error(), "exit status 6") ||
  326. strings.Contains(err.Error(), "exit status 7") {
  327. return nil
  328. }
  329. util.LogErrorf("robocopy data from [%s] to [%s] failed: %s %s", src, dest, string(output), err)
  330. }
  331. return gulu.File.Copy(src, dest)
  332. }
  333. func planSyncAfter(d time.Duration) {
  334. syncPlanTime = time.Now().Add(d)
  335. }