A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
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
17package util
18
19import (
20 "bytes"
21 "fmt"
22 "io"
23 "io/fs"
24 "math/rand"
25 "os"
26 "path/filepath"
27 "runtime"
28 "runtime/debug"
29 "strings"
30 "sync"
31 "sync/atomic"
32 "time"
33
34 "github.com/88250/gulu"
35 "github.com/denisbrodbeck/machineid"
36 "github.com/go-ole/go-ole"
37 "github.com/go-ole/go-ole/oleutil"
38 "github.com/jaypipes/ghw"
39 "github.com/siyuan-note/httpclient"
40 "github.com/siyuan-note/logging"
41)
42
43var DisabledFeatures []string
44
45func DisableFeature(feature string) {
46 DisabledFeatures = append(DisabledFeatures, feature)
47 DisabledFeatures = gulu.Str.RemoveDuplicatedElem(DisabledFeatures)
48}
49
50var (
51 UseSingleLineSave = true // UseSingleLineSave 是否使用单行保存 .sy 和数据库 .json 文件。
52 LargeFileWarningSize = 8 // LargeFileWarningSize 大文件警告大小,单位:MB
53)
54
55func ExceedLargeFileWarningSize(fileSize int) bool {
56 return fileSize > LargeFileWarningSize*1024*1024
57}
58
59// IsUILoaded 是否已经加载了 UI。
60var IsUILoaded = false
61
62func WaitForUILoaded() {
63 start := time.Now()
64 for !IsUILoaded {
65 time.Sleep(200 * time.Millisecond)
66 if time.Since(start) > 30*time.Second {
67 logging.LogErrorf("wait for ui loaded timeout: %s", logging.ShortStack())
68 break
69 }
70 }
71}
72
73func HookUILoaded() {
74 for !IsUILoaded {
75 if 0 < len(SessionsByType("main")) {
76 IsUILoaded = true
77 return
78 }
79 time.Sleep(200 * time.Millisecond)
80 }
81}
82
83// IsExiting 是否正在退出程序。
84var IsExiting = atomic.Bool{}
85
86// MobileOSVer 移动端操作系统版本。
87var MobileOSVer string
88
89// DatabaseVer 数据库版本。修改表结构的话需要修改这里。
90const DatabaseVer = "20220501"
91
92func logBootInfo() {
93 plat := GetOSPlatform()
94 logging.LogInfof("kernel is booting:\n"+
95 " * ver [%s]\n"+
96 " * arch [%s]\n"+
97 " * os [%s]\n"+
98 " * pid [%d]\n"+
99 " * runtime mode [%s]\n"+
100 " * working directory [%s]\n"+
101 " * read only [%v]\n"+
102 " * container [%s]\n"+
103 " * database [ver=%s]\n"+
104 " * workspace directory [%s]",
105 Ver, runtime.GOARCH, plat, os.Getpid(), Mode, WorkingDir, ReadOnly, Container, DatabaseVer, WorkspaceDir)
106 if 0 < len(DisabledFeatures) {
107 logging.LogInfof("disabled features [%s]", strings.Join(DisabledFeatures, ", "))
108 }
109
110 go func() {
111 driveType := getWorkspaceDriveType()
112 if "" == driveType {
113 return
114 }
115
116 if ghw.DriveTypeSSD.String() != driveType {
117 logging.LogWarnf("workspace dir [%s] is not in SSD drive, performance may be affected", WorkspaceDir)
118 WaitForUILoaded()
119 time.Sleep(3 * time.Second)
120 PushErrMsg(Langs[Lang][278], 15000)
121 }
122 }()
123}
124
125func getWorkspaceDriveType() string {
126 if gulu.OS.IsDarwin() {
127 return ghw.DriveTypeSSD.String()
128 }
129
130 if ContainerAndroid == Container || ContainerIOS == Container || ContainerHarmony == Container {
131 return ghw.DriveTypeSSD.String()
132 }
133
134 if gulu.OS.IsWindows() {
135 block, err := ghw.Block()
136 if err != nil {
137 logging.LogWarnf("get block storage info failed: %s", err)
138 return ""
139 }
140
141 part := filepath.VolumeName(WorkspaceDir)
142 for _, disk := range block.Disks {
143 for _, partition := range disk.Partitions {
144 if partition.MountPoint == part {
145 return partition.Disk.DriveType.String()
146 }
147 }
148 }
149 } else if gulu.OS.IsLinux() {
150 block, err := ghw.Block()
151 if err != nil {
152 logging.LogWarnf("get block storage info failed: %s", err)
153 return ""
154 }
155
156 for _, disk := range block.Disks {
157 for _, partition := range disk.Partitions {
158 if strings.HasPrefix(WorkspaceDir, partition.MountPoint) {
159 return partition.Disk.DriveType.String()
160 }
161 }
162 }
163 }
164 return ""
165}
166
167func RandomSleep(minMills, maxMills int) {
168 r := gulu.Rand.Int(minMills, maxMills)
169 time.Sleep(time.Duration(r) * time.Millisecond)
170}
171
172func GetDeviceID() string {
173 if ContainerStd == Container {
174 machineID, err := machineid.ID()
175 if err != nil {
176 return gulu.Rand.String(12)
177 }
178 return machineID
179 }
180 return gulu.Rand.String(12)
181}
182
183func GetDeviceName() string {
184 ret, err := os.Hostname()
185 if err != nil {
186 return "unknown"
187 }
188 return ret
189}
190
191func SetNetworkProxy(proxyURL string) {
192 if err := os.Setenv("HTTPS_PROXY", proxyURL); err != nil {
193 logging.LogErrorf("set env [HTTPS_PROXY] failed: %s", err)
194 }
195 if err := os.Setenv("HTTP_PROXY", proxyURL); err != nil {
196 logging.LogErrorf("set env [HTTP_PROXY] failed: %s", err)
197 }
198
199 if "" != proxyURL {
200 logging.LogInfof("use network proxy [%s]", proxyURL)
201 } else {
202 logging.LogInfof("use network proxy [system]")
203 }
204
205 httpclient.CloseIdleConnections()
206}
207
208const (
209 // SQLFlushInterval 为数据库事务队列写入间隔。
210 SQLFlushInterval = 3000 * time.Millisecond
211)
212
213var (
214 Langs = map[string]map[int]string{}
215 TimeLangs = map[string]map[string]interface{}{}
216 TaskActionLangs = map[string]map[string]interface{}{}
217 TrayMenuLangs = map[string]map[string]interface{}{}
218 AttrViewLangs = map[string]map[string]interface{}{}
219)
220
221var (
222 thirdPartySyncCheckTicker = time.NewTicker(time.Minute * 10)
223)
224
225func ReportFileSysFatalError(err error) {
226 stack := debug.Stack()
227 output := string(stack)
228 if 5 < strings.Count(output, "\n") {
229 lines := strings.Split(output, "\n")
230 output = strings.Join(lines[5:], "\n")
231 }
232 logging.LogErrorf("check file system status failed: %s, %s", err, output)
233 os.Exit(logging.ExitCodeFileSysErr)
234}
235
236var checkFileSysStatusLock = sync.Mutex{}
237
238func CheckFileSysStatus() {
239 if ContainerStd != Container {
240 return
241 }
242
243 for {
244 <-thirdPartySyncCheckTicker.C
245 checkFileSysStatus()
246 }
247}
248
249func checkFileSysStatus() {
250 defer logging.Recover()
251
252 if !checkFileSysStatusLock.TryLock() {
253 logging.LogWarnf("check file system status is locked, skip")
254 return
255 }
256 defer checkFileSysStatusLock.Unlock()
257
258 const fileSysStatusCheckFile = ".siyuan/filesys_status_check"
259 if IsCloudDrivePath(WorkspaceDir) {
260 ReportFileSysFatalError(fmt.Errorf("workspace dir [%s] is in third party sync dir", WorkspaceDir))
261 return
262 }
263
264 dir := filepath.Join(DataDir, fileSysStatusCheckFile)
265 if err := os.RemoveAll(dir); err != nil {
266 ReportFileSysFatalError(err)
267 return
268 }
269
270 if err := os.MkdirAll(dir, 0755); err != nil {
271 ReportFileSysFatalError(err)
272 return
273 }
274
275 for i := 0; i < 7; i++ {
276 tmp := filepath.Join(dir, "check_consistency")
277 data := make([]byte, 1024*4)
278 _, err := rand.Read(data)
279 if err != nil {
280 ReportFileSysFatalError(err)
281 return
282 }
283
284 if err = os.WriteFile(tmp, data, 0644); err != nil {
285 ReportFileSysFatalError(err)
286 return
287 }
288
289 time.Sleep(5 * time.Second)
290
291 for j := 0; j < 32; j++ {
292 renamed := tmp + "_renamed"
293 if err = os.Rename(tmp, renamed); err != nil {
294 ReportFileSysFatalError(err)
295 break
296 }
297
298 RandomSleep(500, 1000)
299
300 f, err := os.Open(renamed)
301 if err != nil {
302 ReportFileSysFatalError(err)
303 return
304 }
305
306 if err = f.Close(); err != nil {
307 ReportFileSysFatalError(err)
308 return
309 }
310
311 if err = os.Rename(renamed, tmp); err != nil {
312 ReportFileSysFatalError(err)
313 return
314 }
315
316 entries, err := os.ReadDir(dir)
317 if err != nil {
318 ReportFileSysFatalError(err)
319 return
320 }
321
322 checkFilenames := bytes.Buffer{}
323 for _, entry := range entries {
324 if !entry.IsDir() && strings.Contains(entry.Name(), "check_") {
325 checkFilenames.WriteString(entry.Name())
326 checkFilenames.WriteString("\n")
327 }
328 }
329 lines := strings.Split(strings.TrimSpace(checkFilenames.String()), "\n")
330 if 1 < len(lines) {
331 buf := bytes.Buffer{}
332 for _, line := range lines {
333 buf.WriteString(" ")
334 buf.WriteString(line)
335 buf.WriteString("\n")
336 }
337 output := buf.String()
338 ReportFileSysFatalError(fmt.Errorf("dir [%s] has more than 1 file:\n%s", dir, output))
339 return
340 }
341 }
342
343 if err = os.RemoveAll(tmp); err != nil {
344 ReportFileSysFatalError(err)
345 return
346 }
347 }
348}
349
350func IsCloudDrivePath(workspaceAbsPath string) bool {
351 if isICloudPath(workspaceAbsPath) {
352 return true
353 }
354
355 if isKnownCloudDrivePath(workspaceAbsPath) {
356 return true
357 }
358
359 if existAvailabilityStatus(workspaceAbsPath) {
360 return true
361 }
362
363 return false
364}
365
366func isKnownCloudDrivePath(workspaceAbsPath string) bool {
367 workspaceAbsPathLower := strings.ToLower(workspaceAbsPath)
368 return strings.Contains(workspaceAbsPathLower, "onedrive") || strings.Contains(workspaceAbsPathLower, "dropbox") ||
369 strings.Contains(workspaceAbsPathLower, "google drive") || strings.Contains(workspaceAbsPathLower, "pcloud") ||
370 strings.Contains(workspaceAbsPathLower, "坚果云") ||
371 strings.Contains(workspaceAbsPathLower, "天翼云")
372}
373
374func isICloudPath(workspaceAbsPath string) (ret bool) {
375 if !gulu.OS.IsDarwin() {
376 return false
377 }
378
379 workspaceAbsPathLower := strings.ToLower(workspaceAbsPath)
380
381 // macOS 端对工作空间放置在 iCloud 路径下做检查 https://github.com/siyuan-note/siyuan/issues/7747
382 iCloudRoot := filepath.Join(HomeDir, "Library", "Mobile Documents")
383 WalkWithSymlinks(iCloudRoot, func(path string, d fs.DirEntry, err error) error {
384 if !d.IsDir() {
385 return nil
386 }
387
388 if strings.HasPrefix(workspaceAbsPathLower, strings.ToLower(path)) {
389 ret = true
390 logging.LogWarnf("workspace [%s] is in iCloud path [%s]", workspaceAbsPath, path)
391 return io.EOF
392 }
393 return nil
394 })
395 return
396}
397
398func existAvailabilityStatus(workspaceAbsPath string) bool {
399 if !gulu.OS.IsWindows() {
400 return false
401 }
402
403 if !gulu.File.IsExist(workspaceAbsPath) {
404 return false
405 }
406
407 // 改进 Windows 端第三方同步盘检测 https://github.com/siyuan-note/siyuan/issues/7777
408
409 defer logging.Recover()
410
411 checkAbsPath := filepath.Join(workspaceAbsPath, "data")
412 if !gulu.File.IsExist(checkAbsPath) {
413 checkAbsPath = workspaceAbsPath
414 }
415 if !gulu.File.IsExist(checkAbsPath) {
416 logging.LogWarnf("check path [%s] not exist", checkAbsPath)
417 return false
418 }
419
420 runtime.LockOSThread()
421 defer runtime.LockOSThread()
422 if err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED); err != nil {
423 logging.LogWarnf("initialize ole failed: %s", err)
424 return false
425 }
426 defer ole.CoUninitialize()
427 dir, file := filepath.Split(checkAbsPath)
428 unknown, err := oleutil.CreateObject("Shell.Application")
429 if err != nil {
430 logging.LogWarnf("create shell application failed: %s", err)
431 return false
432 }
433 shell, err := unknown.QueryInterface(ole.IID_IDispatch)
434 if err != nil {
435 logging.LogWarnf("query shell interface failed: %s", err)
436 return false
437 }
438 defer shell.Release()
439
440 result, err := oleutil.CallMethod(shell, "NameSpace", dir)
441 if err != nil {
442 logging.LogWarnf("call shell [NameSpace] failed: %s", err)
443 return false
444 }
445 folderObj := result.ToIDispatch()
446
447 result, err = oleutil.CallMethod(folderObj, "ParseName", file)
448 if err != nil {
449 logging.LogWarnf("call shell [ParseName] failed: %s", err)
450 return false
451 }
452 fileObj := result.ToIDispatch()
453 if nil == fileObj {
454 logging.LogWarnf("call shell [ParseName] file is nil [%s]", checkAbsPath)
455 return false
456 }
457
458 result, err = oleutil.CallMethod(folderObj, "GetDetailsOf", fileObj, 303)
459 if err != nil {
460 logging.LogWarnf("call shell [GetDetailsOf] failed: %s", err)
461 return false
462 }
463 value := result
464 if nil == value {
465 return false
466 }
467 status := strings.ToLower(value.ToString())
468 if "" == status || "availability status" == status || "可用性状态" == status {
469 return false
470 }
471
472 if strings.Contains(status, "sync") || strings.Contains(status, "同步") ||
473 strings.Contains(status, "available on this device") || strings.Contains(status, "在此设备上可用") ||
474 strings.Contains(status, "available when online") || strings.Contains(status, "联机时可用") {
475 logging.LogErrorf("[%s] third party sync status [%s]", checkAbsPath, status)
476 return true
477 }
478 return false
479}
480
481const (
482 EvtConfPandocInitialized = "conf.pandoc.initialized"
483
484 EvtSQLHistoryRebuild = "sql.history.rebuild"
485 EvtSQLAssetContentRebuild = "sql.assetContent.rebuild"
486)
487
488var SearchCaseSensitive bool