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