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 "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}