A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 488 lines 12 kB view raw
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