A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 485 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 "io" 22 "io/fs" 23 "mime" 24 "os" 25 "path" 26 "path/filepath" 27 "regexp" 28 "strconv" 29 "strings" 30 "unicode/utf8" 31 32 "github.com/88250/gulu" 33 "github.com/88250/lute/ast" 34 "github.com/gabriel-vasile/mimetype" 35 "github.com/siyuan-note/filelock" 36 "github.com/siyuan-note/logging" 37) 38 39func GetFilePathsByExts(dirPath string, exts []string) (ret []string) { 40 filelock.Walk(dirPath, func(path string, d fs.DirEntry, err error) error { 41 if err != nil { 42 logging.LogErrorf("get file paths by ext failed: %s", err) 43 return err 44 } 45 46 if d.IsDir() { 47 return nil 48 } 49 50 for _, ext := range exts { 51 if strings.HasSuffix(path, ext) { 52 ret = append(ret, path) 53 break 54 } 55 } 56 return nil 57 }) 58 return 59} 60 61func GetUniqueFilename(filePath string) string { 62 if !gulu.File.IsExist(filePath) { 63 return filePath 64 } 65 66 ext := filepath.Ext(filePath) 67 base := strings.TrimSuffix(filepath.Base(filePath), ext) 68 dir := filepath.Dir(filePath) 69 i := 1 70 for { 71 newPath := filepath.Join(dir, base+" ("+strconv.Itoa(i)+")"+ext) 72 if !gulu.File.IsExist(newPath) { 73 return newPath 74 } 75 i++ 76 } 77} 78 79func GetMimeTypeByExt(filePath string) (ret string) { 80 ret = mime.TypeByExtension(filepath.Ext(filePath)) 81 if "" == ret { 82 f, err := filelock.OpenFile(filePath, os.O_RDONLY, 0644) 83 if err != nil { 84 logging.LogErrorf("open file [%s] failed: %s", filePath, err) 85 return 86 } 87 defer filelock.CloseFile(f) 88 m, err := mimetype.DetectReader(f) 89 if err != nil { 90 logging.LogErrorf("detect mime type of [%s] failed: %s", filePath, err) 91 return 92 } 93 if nil != m { 94 ret = m.String() 95 } 96 } 97 return 98} 99 100func IsSymlinkPath(absPath string) bool { 101 fi, err := os.Lstat(absPath) 102 if err != nil { 103 return false 104 } 105 return 0 != fi.Mode()&os.ModeSymlink 106} 107 108func IsEmptyDir(p string) bool { 109 if !gulu.File.IsDir(p) { 110 return false 111 } 112 113 files, err := os.ReadDir(p) 114 if err != nil { 115 return false 116 } 117 return 1 > len(files) 118} 119 120func IsSymlink(dir fs.DirEntry) bool { 121 return dir.Type() == fs.ModeSymlink 122} 123 124func IsDirRegularOrSymlink(dir fs.DirEntry) bool { 125 return dir.IsDir() || IsSymlink(dir) 126} 127 128func IsPathRegularDirOrSymlinkDir(path string) bool { 129 fio, err := os.Stat(path) 130 if os.IsNotExist(err) { 131 return false 132 } 133 134 if err != nil { 135 return false 136 } 137 138 return fio.IsDir() 139} 140 141func RemoveID(name string) string { 142 ext := Ext(name) 143 name = strings.TrimSuffix(name, ext) 144 if 23 < len(name) { 145 if id := name[len(name)-22:]; ast.IsNodeIDPattern(id) { 146 name = name[:len(name)-23] 147 } 148 } 149 return name + ext 150} 151 152var commonSuffixes = []string{ 153 ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".tif", ".tiff", 154 ".txt", ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".md", ".rtf", 155 ".zip", ".rar", ".7z", ".tar", ".gz", ".bz2", 156 ".mp3", ".wav", ".aac", ".flac", ".ogg", ".m4a", 157 ".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", 158 ".exe", ".bat", ".sh", ".app", 159 ".js", ".ts", ".html", ".css", ".go", ".py", ".java", ".c", ".cpp", ".json", ".xml", ".yaml", ".toml", 160 ".sql", ".db", ".sqlite", ".csv", 161 ".iso", ".dmg", ".apk", ".bin", 162} 163 164func IsCommonExt(ext string) bool { 165 return strings.HasPrefix(ext, ".") && gulu.Str.Contains(strings.ToLower(ext), commonSuffixes) 166} 167 168func Ext(name string) (ret string) { 169 ret = path.Ext(name) 170 if "." == ret { 171 ret = "" 172 } 173 return 174} 175 176func AssetName(name, newID string) string { 177 _, id := LastID(name) 178 ext := Ext(name) 179 name = name[0 : len(name)-len(ext)] 180 if !ast.IsNodeIDPattern(id) { 181 id = newID 182 name = name + "-" + id + ext 183 } else { 184 if !ast.IsNodeIDPattern(name) { 185 name = name[:len(name)-len(id)-1] + "-" + id + ext 186 } else { 187 name = name + ext 188 } 189 } 190 return name 191} 192 193func LastID(p string) (name, id string) { 194 name = path.Base(p) 195 ext := Ext(name) 196 id = strings.TrimSuffix(name, ext) 197 if 22 < len(id) { 198 id = id[len(id)-22:] 199 } 200 return 201} 202 203func IsCorruptedSYData(data []byte) bool { 204 if 64 > len(data) || '{' != data[0] { 205 return true 206 } 207 return false 208} 209 210func IsValidUploadFileName(name string) bool { 211 return name == FilterUploadFileName(name) 212} 213 214func FilterUploadEmojiFileName(name string) string { 215 if strings.HasPrefix(name, "api/icon/") { 216 // 忽略动态图标 https://github.com/siyuan-note/siyuan/issues/15139 217 return name 218 } 219 220 name = strings.ReplaceAll(name, "/", "_@slash@_") 221 name = FilterUploadFileName(name) 222 name = strings.ReplaceAll(name, "_@slash@_", "/") 223 return name 224} 225 226func FilterUploadFileName(name string) string { 227 ret := FilterFileName(name) 228 229 // 插入资源文件时去除 `[`、`(` 等符号 https://github.com/siyuan-note/siyuan/issues/6708 230 ret = strings.ReplaceAll(ret, "~", "") 231 //ret = strings.ReplaceAll(ret, "_", "") // 插入资源文件时允许下划线 https://github.com/siyuan-note/siyuan/issues/3534 232 ret = strings.ReplaceAll(ret, "[", "") 233 ret = strings.ReplaceAll(ret, "]", "") 234 ret = strings.ReplaceAll(ret, "(", "") 235 ret = strings.ReplaceAll(ret, ")", "") 236 ret = strings.ReplaceAll(ret, "!", "") 237 ret = strings.ReplaceAll(ret, "`", "") 238 ret = strings.ReplaceAll(ret, "&", "") 239 ret = strings.ReplaceAll(ret, "{", "") 240 ret = strings.ReplaceAll(ret, "}", "") 241 ret = strings.ReplaceAll(ret, "=", "") 242 ret = strings.ReplaceAll(ret, "#", "") 243 ret = strings.ReplaceAll(ret, "%", "") 244 ret = strings.ReplaceAll(ret, "$", "") 245 ret = strings.ReplaceAll(ret, ";", "") 246 ret = TruncateLenFileName(ret) 247 return ret 248} 249 250func TruncateLenFileName(name string) (ret string) { 251 // 插入资源文件时文件名长度最大限制 189 字节 https://github.com/siyuan-note/siyuan/issues/7099 252 ext := filepath.Ext(name) 253 extLen := len(ext) 254 var byteCount int 255 truncated := false 256 buf := bytes.Buffer{} 257 maxLen := 189 - extLen 258 var pdfAnnoPngPart string 259 if ".png" == ext { 260 // PNG 图片可能是 PDF 标注的截图,包含页面和旋转角度(name--P1--270-id.png),所以允许的长度更短一些 261 // https://github.com/siyuan-note/siyuan/pull/16714#issuecomment-3737987302 262 263 pdfAnnoPngPattern := "-{0,1}P{0,1}[0-9]{0,4}-{0,1}[0-9]{1,3}-[0-9]{14}-[0-9a-zA-Z]{7}\\.png$" 264 regx := regexp.MustCompile(pdfAnnoPngPattern) 265 pdfAnnoPngPart = regx.FindString(name) 266 if "" != pdfAnnoPngPart { 267 maxLen -= len(pdfAnnoPngPart) + len(".png") 268 name = strings.TrimSuffix(name, pdfAnnoPngPart) 269 } 270 } 271 272 // 深入理解计算机系统原书第3版彩色扫描 -- 美兰德尔 E_布莱恩特Randal,E_·Bryant,等 龚奕利,贺莲 -- 计算机科学丛书, 3rd, 2016 -- 机械工业出版社123-P57-90-20260113113402-prc0u4k.png 273 274 for _, r := range name { 275 byteCount += utf8.RuneLen(r) 276 if maxLen < byteCount { 277 truncated = true 278 break 279 } 280 buf.WriteRune(r) 281 } 282 if truncated { 283 if "" != pdfAnnoPngPart { 284 buf.WriteString(pdfAnnoPngPart) 285 } else { 286 buf.WriteString(ext) 287 } 288 } else { 289 if "" != pdfAnnoPngPart { 290 buf.WriteString(pdfAnnoPngPart) 291 } 292 } 293 ret = buf.String() 294 return 295} 296 297func FilterFilePath(p string) (ret string) { 298 parts := strings.Split(p, "/") 299 var filteredParts []string 300 for _, part := range parts { 301 filteredParts = append(filteredParts, FilterFileName(part)) 302 } 303 ret = strings.Join(filteredParts, "/") 304 return 305} 306 307func FilterFileName(name string) string { 308 name = strings.ReplaceAll(name, "\\", "_") 309 name = strings.ReplaceAll(name, "/", "_") 310 name = strings.ReplaceAll(name, ":", "_") 311 name = strings.ReplaceAll(name, "*", "_") 312 name = strings.ReplaceAll(name, "?", "_") 313 name = strings.ReplaceAll(name, "\"", "_") 314 name = strings.ReplaceAll(name, "'", "_") 315 name = strings.ReplaceAll(name, "<", "_") 316 name = strings.ReplaceAll(name, ">", "_") 317 name = strings.ReplaceAll(name, "|", "_") 318 name = strings.TrimSpace(name) 319 name = strings.TrimSuffix(name, ".") 320 name = RemoveInvalid(name) // Remove invisible characters from file names when uploading assets https://github.com/siyuan-note/siyuan/issues/11683 321 return name 322} 323 324func IsSubPath(absPath, toCheckPath string) bool { 325 if 1 > len(absPath) || 1 > len(toCheckPath) { 326 return false 327 } 328 if absPath == toCheckPath { // 相同路径时不认为是子路径 329 return false 330 } 331 332 if gulu.OS.IsWindows() { 333 if filepath.IsAbs(absPath) && filepath.IsAbs(toCheckPath) { 334 if strings.ToLower(absPath)[0] != strings.ToLower(toCheckPath)[0] { 335 // 不在一个盘 336 return false 337 } 338 } 339 } 340 341 up := ".." + string(os.PathSeparator) 342 rel, err := filepath.Rel(absPath, toCheckPath) 343 if err != nil { 344 return false 345 } 346 if !strings.HasPrefix(rel, up) && rel != ".." { 347 return true 348 } 349 return false 350} 351 352func IsCompressibleAssetImage(p string) bool { 353 lowerName := strings.ToLower(p) 354 return strings.HasPrefix(lowerName, "assets/") && 355 (strings.HasSuffix(lowerName, ".png") || strings.HasSuffix(lowerName, ".jpg") || strings.HasSuffix(lowerName, ".jpeg")) 356} 357 358func SizeOfDirectory(path string) (size int64, err error) { 359 err = filelock.Walk(path, func(path string, d fs.DirEntry, err error) error { 360 if err != nil { 361 return err 362 } 363 364 info, err := d.Info() 365 if err != nil { 366 logging.LogErrorf("size of dir [%s] failed: %s", path, err) 367 return err 368 } 369 370 if !info.IsDir() { 371 size += info.Size() 372 } else { 373 size += 4096 374 } 375 return nil 376 }) 377 if err != nil { 378 logging.LogErrorf("size of dir [%s] failed: %s", path, err) 379 } 380 return 381} 382 383func DataSize() (dataSize, assetsSize int64) { 384 filelock.Walk(DataDir, func(path string, d fs.DirEntry, err error) error { 385 if err != nil { 386 if os.IsNotExist(err) { 387 return nil 388 } 389 logging.LogErrorf("size of data failed: %s", err) 390 return io.EOF 391 } 392 393 info, err := d.Info() 394 if err != nil { 395 logging.LogErrorf("size of data failed: %s", err) 396 return nil 397 } 398 399 if !info.IsDir() { 400 s := info.Size() 401 dataSize += s 402 403 if strings.Contains(strings.TrimPrefix(path, DataDir), "assets") { 404 assetsSize += s 405 } 406 } else { 407 dataSize += 4096 408 } 409 return nil 410 }) 411 return 412} 413 414func CeilSize(size int64) int64 { 415 if 100*1024*1024 > size { 416 return 100 * 1024 * 1024 417 } 418 419 for i := int64(1); i < 40; i++ { 420 if 1024*1024*200*i > size { 421 return 1024 * 1024 * 200 * i 422 } 423 } 424 return 1024*1024*200*40 + 1 425} 426 427func IsReservedFilename(baseName string) bool { 428 return "assets" == baseName || "templates" == baseName || "widgets" == baseName || "emojis" == baseName || ".siyuan" == baseName || strings.HasPrefix(baseName, ".") 429} 430 431func WalkWithSymlinks(root string, fn fs.WalkDirFunc) error { 432 // 感谢 https://github.com/edwardrf/symwalk/blob/main/symwalk.go 433 434 rr, err := filepath.EvalSymlinks(root) // Find real base if there is any symlinks in the path 435 if err != nil { 436 return err 437 } 438 439 visitedDirs := make(map[string]struct{}) 440 return filelock.Walk(rr, getWalkFn(visitedDirs, fn)) 441} 442 443func getWalkFn(visitedDirs map[string]struct{}, fn fs.WalkDirFunc) fs.WalkDirFunc { 444 return func(path string, d fs.DirEntry, err error) error { 445 if err != nil { 446 return fn(path, d, err) 447 } 448 449 if d.IsDir() { 450 if _, ok := visitedDirs[path]; ok { 451 return filepath.SkipDir 452 } 453 visitedDirs[path] = struct{}{} 454 } 455 456 if err := fn(path, d, err); err != nil { 457 return err 458 } 459 460 info, err := d.Info() 461 if nil != err { 462 return err 463 } 464 if info.Mode()&os.ModeSymlink == 0 { 465 return nil 466 } 467 468 // path is a symlink 469 rp, err := filepath.EvalSymlinks(path) 470 if err != nil { 471 return err 472 } 473 474 ri, err := os.Stat(rp) 475 if err != nil { 476 return err 477 } 478 479 if ri.IsDir() { 480 return filelock.Walk(rp, getWalkFn(visitedDirs, fn)) 481 } 482 483 return nil 484 } 485}