A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 407 lines 9.9 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 "io/fs" 22 "os" 23 "path" 24 "path/filepath" 25 "runtime" 26 "sort" 27 "strings" 28 "time" 29 30 "github.com/88250/gulu" 31 "github.com/gabriel-vasile/mimetype" 32 "github.com/siyuan-note/filelock" 33 "github.com/siyuan-note/logging" 34) 35 36var ( 37 SSL = false 38 UserAgent = "SiYuan/" + Ver 39) 40 41func TrimSpaceInPath(p string) string { 42 parts := strings.Split(p, "/") 43 for i, part := range parts { 44 parts[i] = strings.TrimSpace(part) 45 } 46 return strings.Join(parts, "/") 47} 48 49func GetTreeID(treePath string) string { 50 if strings.Contains(treePath, "\\") { 51 return strings.TrimSuffix(filepath.Base(treePath), ".sy") 52 } 53 return strings.TrimSuffix(path.Base(treePath), ".sy") 54} 55 56func ShortPathForBootingDisplay(p string) string { 57 if 25 > len(p) { 58 return p 59 } 60 p = strings.TrimSuffix(p, ".sy") 61 p = path.Base(p) 62 return p 63} 64 65var LocalIPs []string 66 67func GetServerAddrs() (ret []string) { 68 if ContainerAndroid != Container && ContainerHarmony != Container { 69 ret = GetPrivateIPv4s() 70 } else { 71 // Android/鸿蒙上用不了 net.InterfaceAddrs() https://github.com/golang/go/issues/40569,所以前面使用启动内核传入的参数 localIPs 72 ret = LocalIPs 73 } 74 75 ret = append(ret, LocalHost) 76 ret = gulu.Str.RemoveDuplicatedElem(ret) 77 78 for i, _ := range ret { 79 ret[i] = "http://" + ret[i] + ":" + ServerPort 80 } 81 return 82} 83 84func isRunningInDockerContainer() bool { 85 if _, runInContainer := os.LookupEnv("RUN_IN_CONTAINER"); runInContainer { 86 return true 87 } 88 if _, err := os.Stat("/.dockerenv"); err == nil { 89 return true 90 } 91 return false 92} 93 94func IsRelativePath(dest string) bool { 95 if 1 > len(dest) { 96 return true 97 } 98 99 if '/' == dest[0] { 100 return false 101 } 102 103 // 检查特定协议前缀 104 lowerDest := strings.ToLower(dest) 105 if strings.HasPrefix(lowerDest, "mailto:") || 106 strings.HasPrefix(lowerDest, "tel:") || 107 strings.HasPrefix(lowerDest, "sms:") { 108 return false 109 } 110 return !strings.Contains(dest, ":/") && !strings.Contains(dest, ":\\") 111} 112 113func TimeFromID(id string) (ret string) { 114 if 14 > len(id) { 115 logging.LogWarnf("invalid id [%s], stack [\n%s]", id, logging.ShortStack()) 116 return time.Now().Format("20060102150405") 117 } 118 ret = id[:14] 119 return 120} 121 122func GetChildDocDepth(treeAbsPath string) (ret int) { 123 dir := strings.TrimSuffix(treeAbsPath, ".sy") 124 if !gulu.File.IsDir(dir) { 125 return 126 } 127 128 baseDepth := strings.Count(filepath.ToSlash(treeAbsPath), "/") 129 depth := 1 130 filelock.Walk(dir, func(path string, d fs.DirEntry, err error) error { 131 p := filepath.ToSlash(path) 132 currentDepth := strings.Count(p, "/") 133 if depth < currentDepth { 134 depth = currentDepth 135 } 136 return nil 137 }) 138 ret = depth - baseDepth 139 return 140} 141 142func NormalizeConcurrentReqs(concurrentReqs int, provider int) int { 143 switch provider { 144 case 0: // SiYuan 145 switch { 146 case concurrentReqs < 1: 147 concurrentReqs = 8 148 case concurrentReqs > 16: 149 concurrentReqs = 16 150 default: 151 } 152 case 2: // S3 153 switch { 154 case concurrentReqs < 1: 155 concurrentReqs = 8 156 case concurrentReqs > 16: 157 concurrentReqs = 16 158 default: 159 } 160 case 3: // WebDAV 161 switch { 162 case concurrentReqs < 1: 163 concurrentReqs = 1 164 case concurrentReqs > 16: 165 concurrentReqs = 16 166 default: 167 } 168 case 4: // Local File System 169 switch { 170 case concurrentReqs < 1: 171 concurrentReqs = 16 172 case concurrentReqs > 1024: 173 concurrentReqs = 1024 174 default: 175 } 176 } 177 return concurrentReqs 178} 179 180func NormalizeTimeout(timeout int) int { 181 if 7 > timeout { 182 if 1 > timeout { 183 return 60 184 } 185 return 7 186 } 187 if 300 < timeout { 188 return 300 189 } 190 return timeout 191} 192 193func NormalizeEndpoint(endpoint string) string { 194 endpoint = strings.TrimSpace(endpoint) 195 if "" == endpoint { 196 return "" 197 } 198 if !strings.HasPrefix(endpoint, "http://") && !strings.HasPrefix(endpoint, "https://") { 199 endpoint = "http://" + endpoint 200 } 201 if !strings.HasSuffix(endpoint, "/") { 202 endpoint = endpoint + "/" 203 } 204 return endpoint 205} 206 207func NormalizeLocalPath(endpoint string) string { 208 endpoint = strings.TrimSpace(endpoint) 209 if "" == endpoint { 210 return "" 211 } 212 endpoint = filepath.ToSlash(filepath.Clean(endpoint)) 213 if !strings.HasSuffix(endpoint, "/") { 214 endpoint = endpoint + "/" 215 } 216 return endpoint 217} 218 219func FilterMoveDocFromPaths(fromPaths []string, toPath string) (ret []string) { 220 tmp := FilterSelfChildDocs(fromPaths) 221 for _, fromPath := range tmp { 222 fromDir := strings.TrimSuffix(fromPath, ".sy") 223 if strings.HasPrefix(toPath, fromDir) { 224 continue 225 } 226 ret = append(ret, fromPath) 227 } 228 return 229} 230 231func FilterSelfChildDocs(paths []string) (ret []string) { 232 sort.Slice(paths, func(i, j int) bool { return strings.Count(paths[i], "/") < strings.Count(paths[j], "/") }) 233 234 dirs := map[string]string{} 235 for _, fromPath := range paths { 236 dir := strings.TrimSuffix(fromPath, ".sy") 237 existParent := false 238 for d, _ := range dirs { 239 if strings.HasPrefix(fromPath, d) { 240 existParent = true 241 break 242 } 243 } 244 if existParent { 245 continue 246 } 247 dirs[dir] = fromPath 248 ret = append(ret, fromPath) 249 } 250 return 251} 252 253func IsAssetLinkDest(dest []byte, includeServePath bool) bool { 254 return bytes.HasPrefix(dest, []byte("assets/")) || 255 (includeServePath && (bytes.HasPrefix(dest, []byte("emojis/")) || 256 bytes.HasPrefix(dest, []byte("plugins/")) || 257 bytes.HasPrefix(dest, []byte("public/")) || 258 bytes.HasPrefix(dest, []byte("widgets/")))) 259} 260 261var ( 262 SiYuanAssetsImage = []string{".apng", ".ico", ".cur", ".jpg", ".jpe", ".jpeg", ".jfif", ".pjp", ".pjpeg", ".png", ".gif", ".webp", ".bmp", ".svg", ".avif"} 263 SiYuanAssetsAudio = []string{".mp3", ".wav", ".ogg", ".m4a", ".flac"} 264 SiYuanAssetsVideo = []string{".mov", ".weba", ".mkv", ".mp4", ".webm"} 265) 266 267func IsAssetsImage(assetPath string) bool { 268 ext := strings.ToLower(filepath.Ext(assetPath)) 269 if "" == ext { 270 absPath := filepath.Join(DataDir, assetPath) 271 f, err := filelock.OpenFile(absPath, os.O_RDONLY, 0644) 272 if err != nil { 273 logging.LogErrorf("open file [%s] failed: %s", absPath, err) 274 return false 275 } 276 defer filelock.CloseFile(f) 277 m, err := mimetype.DetectReader(f) 278 if nil != err { 279 logging.LogWarnf("detect file [%s] mimetype failed: %v", absPath, err) 280 return false 281 } 282 283 ext = m.Extension() 284 } 285 return gulu.Str.Contains(ext, SiYuanAssetsImage) 286} 287 288func IsDisplayableAsset(p string) bool { 289 ext := strings.ToLower(filepath.Ext(p)) 290 if "" == ext { 291 return false 292 } 293 if gulu.Str.Contains(ext, SiYuanAssetsImage) { 294 return true 295 } 296 if gulu.Str.Contains(ext, SiYuanAssetsAudio) { 297 return true 298 } 299 if gulu.Str.Contains(ext, SiYuanAssetsVideo) { 300 return true 301 } 302 return false 303} 304 305func GetAbsPathInWorkspace(relPath string) (string, error) { 306 absPath := filepath.Join(WorkspaceDir, relPath) 307 absPath = filepath.Clean(absPath) 308 if WorkspaceDir == absPath { 309 return absPath, nil 310 } 311 312 if IsSubPath(WorkspaceDir, absPath) { 313 return absPath, nil 314 } 315 return "", os.ErrPermission 316} 317 318func IsAbsPathInWorkspace(absPath string) bool { 319 return IsSubPath(WorkspaceDir, absPath) 320} 321 322// IsWorkspaceDir 判断指定目录是否是工作空间目录。 323func IsWorkspaceDir(dir string) bool { 324 conf := filepath.Join(dir, "conf", "conf.json") 325 data, err := os.ReadFile(conf) 326 if nil != err { 327 return false 328 } 329 return strings.Contains(string(data), "kernelVersion") 330} 331 332// IsPartitionRootPath checks if the given path is a partition root path. 333func IsPartitionRootPath(path string) bool { 334 if path == "" { 335 return false 336 } 337 338 // Clean the path to remove any trailing slashes 339 cleanPath := filepath.Clean(path) 340 341 // Check if the path is the root path based on the operating system 342 if runtime.GOOS == "windows" { 343 // On Windows, root paths are like "C:\", "D:\", etc. 344 return len(cleanPath) == 3 && cleanPath[1] == ':' && cleanPath[2] == '\\' 345 } else { 346 // On Unix-like systems, the root path is "/" 347 return cleanPath == "/" 348 } 349} 350 351// IsSensitivePath 对传入路径做统一的敏感性检测。 352func IsSensitivePath(p string) bool { 353 if p == "" { 354 return false 355 } 356 pp := filepath.Clean(strings.ToLower(p)) 357 358 // 敏感目录前缀(UNIX 风格) 359 prefixes := []string{ 360 "/etc/ssh", 361 "/root", 362 "/etc", 363 "/var/lib/", 364 "/.", 365 } 366 for _, pre := range prefixes { 367 if strings.HasPrefix(pp, pre) { 368 return true 369 } 370 } 371 372 // Windows 常见敏感目录(小写比较) 373 winPrefixes := []string{ 374 `c:\windows\system32`, 375 `c:\windows\system`, 376 } 377 for _, wp := range winPrefixes { 378 if strings.HasPrefix(pp, strings.ToLower(wp)) { 379 return true 380 } 381 } 382 383 // Windows 开始启动菜单路径(小写比较) 384 startMenuPrefixes := []string{ 385 strings.ToLower(filepath.Join(os.Getenv("APPDATA"), "Microsoft", "Windows", "Start Menu")), 386 strings.ToLower(filepath.Join(os.Getenv("ProgramData"), "Microsoft", "Windows", "Start Menu")), 387 } 388 for _, sp := range startMenuPrefixes { 389 if strings.HasPrefix(pp, sp) { 390 return true 391 } 392 } 393 394 homePrefixes := []string{ 395 strings.ToLower(filepath.Join(HomeDir, ".ssh")), 396 strings.ToLower(filepath.Join(HomeDir, ".config")), 397 strings.ToLower(filepath.Join(HomeDir, ".bashrc")), 398 strings.ToLower(filepath.Join(HomeDir, ".zshrc")), 399 strings.ToLower(filepath.Join(HomeDir, ".profile")), 400 } 401 for _, hp := range homePrefixes { 402 if strings.HasPrefix(pp, hp) { 403 return true 404 } 405 } 406 return false 407}