Something went wrong. Try again.
A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489// SiYuan - Refactor your thinking// Copyright (c) 2020-present, b3log.org//// This program is free software: you can redistribute it and/or modify// it under the terms of the GNU Affero General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// This program is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU Affero General Public License for more details.//// You should have received a copy of the GNU Affero General Public License// along with this program. If not, see <https://www.gnu.org/licenses/>.
package util
import ( "bytes" "fmt" "io" "io/fs" "math/rand" "os" "path/filepath" "runtime" "runtime/debug" "strings" "sync" "sync/atomic" "time"
"github.com/88250/gulu" "github.com/denisbrodbeck/machineid" "github.com/go-ole/go-ole" "github.com/go-ole/go-ole/oleutil" "github.com/jaypipes/ghw" "github.com/siyuan-note/httpclient" "github.com/siyuan-note/logging")
var DisabledFeatures []string
func DisableFeature(feature string) { DisabledFeatures = append(DisabledFeatures, feature) DisabledFeatures = gulu.Str.RemoveDuplicatedElem(DisabledFeatures)}
var ( UseSingleLineSave = true // UseSingleLineSave 是否使用单行保存 .sy 和数据库 .json 文件。 LargeFileWarningSize = 8 // LargeFileWarningSize 大文件警告大小,单位:MB)
func ExceedLargeFileWarningSize(fileSize int) bool { return fileSize > LargeFileWarningSize*1024*1024}
// IsUILoaded 是否已经加载了 UI。var IsUILoaded = false
func WaitForUILoaded() { start := time.Now() for !IsUILoaded { time.Sleep(200 * time.Millisecond) if time.Since(start) > 30*time.Second { logging.LogErrorf("wait for ui loaded timeout: %s", logging.ShortStack()) break } }}
func HookUILoaded() { for !IsUILoaded { if 0 < len(SessionsByType("main")) { IsUILoaded = true return } time.Sleep(200 * time.Millisecond) }}
// IsExiting 是否正在退出程序。var IsExiting = atomic.Bool{}
// MobileOSVer 移动端操作系统版本。var MobileOSVer string
// DatabaseVer 数据库版本。修改表结构的话需要修改这里。const DatabaseVer = "20220501"
func logBootInfo() { plat := GetOSPlatform() logging.LogInfof("kernel is booting:\n"+ " * ver [%s]\n"+ " * arch [%s]\n"+ " * os [%s]\n"+ " * pid [%d]\n"+ " * runtime mode [%s]\n"+ " * working directory [%s]\n"+ " * read only [%v]\n"+ " * container [%s]\n"+ " * database [ver=%s]\n"+ " * workspace directory [%s]", Ver, runtime.GOARCH, plat, os.Getpid(), Mode, WorkingDir, ReadOnly, Container, DatabaseVer, WorkspaceDir) if 0 < len(DisabledFeatures) { logging.LogInfof("disabled features [%s]", strings.Join(DisabledFeatures, ", ")) }
go func() { driveType := getWorkspaceDriveType() if "" == driveType { return }
if ghw.DriveTypeSSD.String() != driveType { logging.LogWarnf("workspace dir [%s] is not in SSD drive, performance may be affected", WorkspaceDir) WaitForUILoaded() time.Sleep(3 * time.Second) PushErrMsg(Langs[Lang][278], 15000) } }()}
func getWorkspaceDriveType() string { if gulu.OS.IsDarwin() { return ghw.DriveTypeSSD.String() }
if ContainerAndroid == Container || ContainerIOS == Container || ContainerHarmony == Container { return ghw.DriveTypeSSD.String() }
if gulu.OS.IsWindows() { block, err := ghw.Block() if err != nil { logging.LogWarnf("get block storage info failed: %s", err) return "" }
part := filepath.VolumeName(WorkspaceDir) for _, disk := range block.Disks { for _, partition := range disk.Partitions { if partition.MountPoint == part { return partition.Disk.DriveType.String() } } } } else if gulu.OS.IsLinux() { block, err := ghw.Block() if err != nil { logging.LogWarnf("get block storage info failed: %s", err) return "" }
for _, disk := range block.Disks { for _, partition := range disk.Partitions { if strings.HasPrefix(WorkspaceDir, partition.MountPoint) { return partition.Disk.DriveType.String() } } } } return ""}
func RandomSleep(minMills, maxMills int) { r := gulu.Rand.Int(minMills, maxMills) time.Sleep(time.Duration(r) * time.Millisecond)}
func GetDeviceID() string { if ContainerStd == Container { machineID, err := machineid.ID() if err != nil { return gulu.Rand.String(12) } return machineID } return gulu.Rand.String(12)}
func GetDeviceName() string { ret, err := os.Hostname() if err != nil { return "unknown" } return ret}
func SetNetworkProxy(proxyURL string) { if err := os.Setenv("HTTPS_PROXY", proxyURL); err != nil { logging.LogErrorf("set env [HTTPS_PROXY] failed: %s", err) } if err := os.Setenv("HTTP_PROXY", proxyURL); err != nil { logging.LogErrorf("set env [HTTP_PROXY] failed: %s", err) }
if "" != proxyURL { logging.LogInfof("use network proxy [%s]", proxyURL) } else { logging.LogInfof("use network proxy [system]") }
httpclient.CloseIdleConnections()}
const ( // SQLFlushInterval 为数据库事务队列写入间隔。 SQLFlushInterval = 3000 * time.Millisecond)
var ( Langs = map[string]map[int]string{} TimeLangs = map[string]map[string]interface{}{} TaskActionLangs = map[string]map[string]interface{}{} TrayMenuLangs = map[string]map[string]interface{}{} AttrViewLangs = map[string]map[string]interface{}{})
var ( thirdPartySyncCheckTicker = time.NewTicker(time.Minute * 10))
func ReportFileSysFatalError(err error) { stack := debug.Stack() output := string(stack) if 5 < strings.Count(output, "\n") { lines := strings.Split(output, "\n") output = strings.Join(lines[5:], "\n") } logging.LogErrorf("check file system status failed: %s, %s", err, output) os.Exit(logging.ExitCodeFileSysErr)}
var checkFileSysStatusLock = sync.Mutex{}
func CheckFileSysStatus() { if ContainerStd != Container { return }
for { <-thirdPartySyncCheckTicker.C checkFileSysStatus() }}
func checkFileSysStatus() { defer logging.Recover()
if !checkFileSysStatusLock.TryLock() { logging.LogWarnf("check file system status is locked, skip") return } defer checkFileSysStatusLock.Unlock()
const fileSysStatusCheckFile = ".siyuan/filesys_status_check" if IsCloudDrivePath(WorkspaceDir) { ReportFileSysFatalError(fmt.Errorf("workspace dir [%s] is in third party sync dir", WorkspaceDir)) return }
dir := filepath.Join(DataDir, fileSysStatusCheckFile) if err := os.RemoveAll(dir); err != nil { ReportFileSysFatalError(err) return }
if err := os.MkdirAll(dir, 0755); err != nil { ReportFileSysFatalError(err) return }
for i := 0; i < 7; i++ { tmp := filepath.Join(dir, "check_consistency") data := make([]byte, 1024*4) _, err := rand.Read(data) if err != nil { ReportFileSysFatalError(err) return }
if err = os.WriteFile(tmp, data, 0644); err != nil { ReportFileSysFatalError(err) return }
time.Sleep(5 * time.Second)
for j := 0; j < 32; j++ { renamed := tmp + "_renamed" if err = os.Rename(tmp, renamed); err != nil { ReportFileSysFatalError(err) break }
RandomSleep(500, 1000)
f, err := os.Open(renamed) if err != nil { ReportFileSysFatalError(err) return }
if err = f.Close(); err != nil { ReportFileSysFatalError(err) return }
if err = os.Rename(renamed, tmp); err != nil { ReportFileSysFatalError(err) return }
entries, err := os.ReadDir(dir) if err != nil { ReportFileSysFatalError(err) return }
checkFilenames := bytes.Buffer{} for _, entry := range entries { if !entry.IsDir() && strings.Contains(entry.Name(), "check_") { checkFilenames.WriteString(entry.Name()) checkFilenames.WriteString("\n") } } lines := strings.Split(strings.TrimSpace(checkFilenames.String()), "\n") if 1 < len(lines) { buf := bytes.Buffer{} for _, line := range lines { buf.WriteString(" ") buf.WriteString(line) buf.WriteString("\n") } output := buf.String() ReportFileSysFatalError(fmt.Errorf("dir [%s] has more than 1 file:\n%s", dir, output)) return } }
if err = os.RemoveAll(tmp); err != nil { ReportFileSysFatalError(err) return } }}
func IsCloudDrivePath(workspaceAbsPath string) bool { if isICloudPath(workspaceAbsPath) { return true }
if isKnownCloudDrivePath(workspaceAbsPath) { return true }
if existAvailabilityStatus(workspaceAbsPath) { return true }
return false}
func isKnownCloudDrivePath(workspaceAbsPath string) bool { workspaceAbsPathLower := strings.ToLower(workspaceAbsPath) return strings.Contains(workspaceAbsPathLower, "onedrive") || strings.Contains(workspaceAbsPathLower, "dropbox") || strings.Contains(workspaceAbsPathLower, "google drive") || strings.Contains(workspaceAbsPathLower, "pcloud") || strings.Contains(workspaceAbsPathLower, "坚果云") || strings.Contains(workspaceAbsPathLower, "天翼云")}
func isICloudPath(workspaceAbsPath string) (ret bool) { if !gulu.OS.IsDarwin() { return false }
workspaceAbsPathLower := strings.ToLower(workspaceAbsPath)
// macOS 端对工作空间放置在 iCloud 路径下做检查 https://github.com/siyuan-note/siyuan/issues/7747 iCloudRoot := filepath.Join(HomeDir, "Library", "Mobile Documents") WalkWithSymlinks(iCloudRoot, func(path string, d fs.DirEntry, err error) error { if !d.IsDir() { return nil }
if strings.HasPrefix(workspaceAbsPathLower, strings.ToLower(path)) { ret = true logging.LogWarnf("workspace [%s] is in iCloud path [%s]", workspaceAbsPath, path) return io.EOF } return nil }) return}
func existAvailabilityStatus(workspaceAbsPath string) bool { if !gulu.OS.IsWindows() { return false }
if !gulu.File.IsExist(workspaceAbsPath) { return false }
// 改进 Windows 端第三方同步盘检测 https://github.com/siyuan-note/siyuan/issues/7777
defer logging.Recover()
checkAbsPath := filepath.Join(workspaceAbsPath, "data") if !gulu.File.IsExist(checkAbsPath) { checkAbsPath = workspaceAbsPath } if !gulu.File.IsExist(checkAbsPath) { logging.LogWarnf("check path [%s] not exist", checkAbsPath) return false }
runtime.LockOSThread() defer runtime.LockOSThread() if err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED); err != nil { logging.LogWarnf("initialize ole failed: %s", err) return false } defer ole.CoUninitialize() dir, file := filepath.Split(checkAbsPath) unknown, err := oleutil.CreateObject("Shell.Application") if err != nil { logging.LogWarnf("create shell application failed: %s", err) return false } shell, err := unknown.QueryInterface(ole.IID_IDispatch) if err != nil { logging.LogWarnf("query shell interface failed: %s", err) return false } defer shell.Release()
result, err := oleutil.CallMethod(shell, "NameSpace", dir) if err != nil { logging.LogWarnf("call shell [NameSpace] failed: %s", err) return false } folderObj := result.ToIDispatch()
result, err = oleutil.CallMethod(folderObj, "ParseName", file) if err != nil { logging.LogWarnf("call shell [ParseName] failed: %s", err) return false } fileObj := result.ToIDispatch() if nil == fileObj { logging.LogWarnf("call shell [ParseName] file is nil [%s]", checkAbsPath) return false }
result, err = oleutil.CallMethod(folderObj, "GetDetailsOf", fileObj, 303) if err != nil { logging.LogWarnf("call shell [GetDetailsOf] failed: %s", err) return false } value := result if nil == value { return false } status := strings.ToLower(value.ToString()) if "" == status || "availability status" == status || "可用性状态" == status { return false }
if strings.Contains(status, "sync") || strings.Contains(status, "同步") || strings.Contains(status, "available on this device") || strings.Contains(status, "在此设备上可用") || strings.Contains(status, "available when online") || strings.Contains(status, "联机时可用") { logging.LogErrorf("[%s] third party sync status [%s]", checkAbsPath, status) return true } return false}
const ( EvtConfPandocInitialized = "conf.pandoc.initialized"
EvtSQLHistoryRebuild = "sql.history.rebuild" EvtSQLAssetContentRebuild = "sql.assetContent.rebuild")
var SearchCaseSensitive bool