file.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  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. package util
  17. import (
  18. "bytes"
  19. "io"
  20. "io/fs"
  21. "mime"
  22. "os"
  23. "path"
  24. "path/filepath"
  25. "strconv"
  26. "strings"
  27. "unicode/utf8"
  28. "github.com/88250/gulu"
  29. "github.com/88250/lute/ast"
  30. "github.com/gabriel-vasile/mimetype"
  31. "github.com/siyuan-note/filelock"
  32. "github.com/siyuan-note/logging"
  33. )
  34. func GetFilePathsByExts(dirPath string, exts []string) (ret []string) {
  35. filelock.Walk(dirPath, func(path string, d fs.DirEntry, err error) error {
  36. if err != nil {
  37. logging.LogErrorf("get file paths by ext failed: %s", err)
  38. return err
  39. }
  40. if d.IsDir() {
  41. return nil
  42. }
  43. for _, ext := range exts {
  44. if strings.HasSuffix(path, ext) {
  45. ret = append(ret, path)
  46. break
  47. }
  48. }
  49. return nil
  50. })
  51. return
  52. }
  53. func GetUniqueFilename(filePath string) string {
  54. if !gulu.File.IsExist(filePath) {
  55. return filePath
  56. }
  57. ext := filepath.Ext(filePath)
  58. base := strings.TrimSuffix(filepath.Base(filePath), ext)
  59. dir := filepath.Dir(filePath)
  60. i := 1
  61. for {
  62. newPath := filepath.Join(dir, base+" ("+strconv.Itoa(i)+")"+ext)
  63. if !gulu.File.IsExist(newPath) {
  64. return newPath
  65. }
  66. i++
  67. }
  68. }
  69. func GetMimeTypeByExt(filePath string) (ret string) {
  70. ret = mime.TypeByExtension(filepath.Ext(filePath))
  71. if "" == ret {
  72. m, err := mimetype.DetectFile(filePath)
  73. if err != nil {
  74. logging.LogErrorf("detect mime type of [%s] failed: %s", filePath, err)
  75. return
  76. }
  77. if nil != m {
  78. ret = m.String()
  79. }
  80. }
  81. return
  82. }
  83. func IsSymlinkPath(absPath string) bool {
  84. fi, err := os.Lstat(absPath)
  85. if err != nil {
  86. return false
  87. }
  88. return 0 != fi.Mode()&os.ModeSymlink
  89. }
  90. func IsEmptyDir(p string) bool {
  91. if !gulu.File.IsDir(p) {
  92. return false
  93. }
  94. files, err := os.ReadDir(p)
  95. if err != nil {
  96. return false
  97. }
  98. return 1 > len(files)
  99. }
  100. func IsSymlink(dir fs.DirEntry) bool {
  101. return dir.Type() == fs.ModeSymlink
  102. }
  103. func IsDirRegularOrSymlink(dir fs.DirEntry) bool {
  104. return dir.IsDir() || IsSymlink(dir)
  105. }
  106. func IsPathRegularDirOrSymlinkDir(path string) bool {
  107. fio, err := os.Stat(path)
  108. if os.IsNotExist(err) {
  109. return false
  110. }
  111. if err != nil {
  112. return false
  113. }
  114. return fio.IsDir()
  115. }
  116. func RemoveID(name string) string {
  117. ext := path.Ext(name)
  118. name = strings.TrimSuffix(name, ext)
  119. if 23 < len(name) {
  120. if id := name[len(name)-22:]; ast.IsNodeIDPattern(id) {
  121. name = name[:len(name)-23]
  122. }
  123. }
  124. return name + ext
  125. }
  126. func AssetName(name string) string {
  127. _, id := LastID(name)
  128. ext := path.Ext(name)
  129. name = name[0 : len(name)-len(ext)]
  130. if !ast.IsNodeIDPattern(id) {
  131. id = ast.NewNodeID()
  132. name = name + "-" + id + ext
  133. } else {
  134. if !ast.IsNodeIDPattern(name) {
  135. name = name[:len(name)-len(id)-1] + "-" + id + ext
  136. } else {
  137. name = name + ext
  138. }
  139. }
  140. return name
  141. }
  142. func LastID(p string) (name, id string) {
  143. name = path.Base(p)
  144. ext := path.Ext(name)
  145. id = strings.TrimSuffix(name, ext)
  146. if 22 < len(id) {
  147. id = id[len(id)-22:]
  148. }
  149. return
  150. }
  151. func IsCorruptedSYData(data []byte) bool {
  152. if 64 > len(data) || '{' != data[0] {
  153. return true
  154. }
  155. return false
  156. }
  157. func FilterUploadFileName(name string) string {
  158. ret := FilterFileName(name)
  159. // 插入资源文件时去除 `[`、`(` 等符号 https://github.com/siyuan-note/siyuan/issues/6708
  160. ret = strings.ReplaceAll(ret, "~", "")
  161. //ret = strings.ReplaceAll(ret, "_", "") // 插入资源文件时允许下划线 https://github.com/siyuan-note/siyuan/issues/3534
  162. ret = strings.ReplaceAll(ret, "[", "")
  163. ret = strings.ReplaceAll(ret, "]", "")
  164. ret = strings.ReplaceAll(ret, "(", "")
  165. ret = strings.ReplaceAll(ret, ")", "")
  166. ret = strings.ReplaceAll(ret, "!", "")
  167. ret = strings.ReplaceAll(ret, "`", "")
  168. ret = strings.ReplaceAll(ret, "&", "")
  169. ret = strings.ReplaceAll(ret, "{", "")
  170. ret = strings.ReplaceAll(ret, "}", "")
  171. ret = strings.ReplaceAll(ret, "=", "")
  172. ret = strings.ReplaceAll(ret, "#", "")
  173. ret = strings.ReplaceAll(ret, "%", "")
  174. ret = strings.ReplaceAll(ret, "$", "")
  175. ret = TruncateLenFileName(ret)
  176. return ret
  177. }
  178. func TruncateLenFileName(name string) (ret string) {
  179. // 插入资源文件时文件名长度最大限制 189 字节 https://github.com/siyuan-note/siyuan/issues/7099
  180. ext := filepath.Ext(name)
  181. var byteCount int
  182. truncated := false
  183. buf := bytes.Buffer{}
  184. for _, r := range name {
  185. byteCount += utf8.RuneLen(r)
  186. if 189-len(ext) < byteCount {
  187. truncated = true
  188. break
  189. }
  190. buf.WriteRune(r)
  191. }
  192. if truncated {
  193. buf.WriteString(ext)
  194. }
  195. ret = buf.String()
  196. return
  197. }
  198. func FilterFilePath(p string) (ret string) {
  199. parts := strings.Split(p, "/")
  200. var filteredParts []string
  201. for _, part := range parts {
  202. filteredParts = append(filteredParts, FilterFileName(part))
  203. }
  204. ret = strings.Join(filteredParts, "/")
  205. return
  206. }
  207. func FilterFileName(name string) string {
  208. name = strings.ReplaceAll(name, "\\", "")
  209. name = strings.ReplaceAll(name, "/", "")
  210. name = strings.ReplaceAll(name, ":", "")
  211. name = strings.ReplaceAll(name, "*", "")
  212. name = strings.ReplaceAll(name, "?", "")
  213. name = strings.ReplaceAll(name, "\"", "")
  214. name = strings.ReplaceAll(name, "'", "")
  215. name = strings.ReplaceAll(name, "<", "")
  216. name = strings.ReplaceAll(name, ">", "")
  217. name = strings.ReplaceAll(name, "|", "")
  218. name = strings.TrimSpace(name)
  219. name = gulu.Str.RemoveInvisible(name) // Remove invisible characters from file names when uploading assets https://github.com/siyuan-note/siyuan/issues/11683
  220. return name
  221. }
  222. func IsSubPath(absPath, toCheckPath string) bool {
  223. if 1 > len(absPath) || 1 > len(toCheckPath) {
  224. return false
  225. }
  226. if absPath == toCheckPath { // 相同路径时不认为是子路径
  227. return false
  228. }
  229. if gulu.OS.IsWindows() {
  230. if filepath.IsAbs(absPath) && filepath.IsAbs(toCheckPath) {
  231. if strings.ToLower(absPath)[0] != strings.ToLower(toCheckPath)[0] {
  232. // 不在一个盘
  233. return false
  234. }
  235. }
  236. }
  237. up := ".." + string(os.PathSeparator)
  238. rel, err := filepath.Rel(absPath, toCheckPath)
  239. if err != nil {
  240. return false
  241. }
  242. if !strings.HasPrefix(rel, up) && rel != ".." {
  243. return true
  244. }
  245. return false
  246. }
  247. func SizeOfDirectory(path string) (size int64, err error) {
  248. err = filelock.Walk(path, func(path string, d fs.DirEntry, err error) error {
  249. if err != nil {
  250. return err
  251. }
  252. info, err := d.Info()
  253. if err != nil {
  254. logging.LogErrorf("size of dir [%s] failed: %s", path, err)
  255. return err
  256. }
  257. if !info.IsDir() {
  258. size += info.Size()
  259. } else {
  260. size += 4096
  261. }
  262. return nil
  263. })
  264. if err != nil {
  265. logging.LogErrorf("size of dir [%s] failed: %s", path, err)
  266. }
  267. return
  268. }
  269. func DataSize() (dataSize, assetsSize int64) {
  270. filelock.Walk(DataDir, func(path string, d fs.DirEntry, err error) error {
  271. if err != nil {
  272. if os.IsNotExist(err) {
  273. return nil
  274. }
  275. logging.LogErrorf("size of data failed: %s", err)
  276. return io.EOF
  277. }
  278. info, err := d.Info()
  279. if err != nil {
  280. logging.LogErrorf("size of data failed: %s", err)
  281. return nil
  282. }
  283. if !info.IsDir() {
  284. s := info.Size()
  285. dataSize += s
  286. if strings.Contains(strings.TrimPrefix(path, DataDir), "assets") {
  287. assetsSize += s
  288. }
  289. } else {
  290. dataSize += 4096
  291. }
  292. return nil
  293. })
  294. return
  295. }
  296. func CeilSize(size int64) int64 {
  297. if 100*1024*1024 > size {
  298. return 100 * 1024 * 1024
  299. }
  300. for i := int64(1); i < 40; i++ {
  301. if 1024*1024*200*i > size {
  302. return 1024 * 1024 * 200 * i
  303. }
  304. }
  305. return 1024*1024*200*40 + 1
  306. }
  307. func IsReservedFilename(baseName string) bool {
  308. return "assets" == baseName || "templates" == baseName || "widgets" == baseName || "emojis" == baseName || ".siyuan" == baseName || strings.HasPrefix(baseName, ".")
  309. }
  310. func WalkWithSymlinks(root string, fn fs.WalkDirFunc) error {
  311. // 感谢 https://github.com/edwardrf/symwalk/blob/main/symwalk.go
  312. rr, err := filepath.EvalSymlinks(root) // Find real base if there is any symlinks in the path
  313. if err != nil {
  314. return err
  315. }
  316. visitedDirs := make(map[string]struct{})
  317. return filelock.Walk(rr, getWalkFn(visitedDirs, fn))
  318. }
  319. func getWalkFn(visitedDirs map[string]struct{}, fn fs.WalkDirFunc) fs.WalkDirFunc {
  320. return func(path string, d fs.DirEntry, err error) error {
  321. if err != nil {
  322. return fn(path, d, err)
  323. }
  324. if d.IsDir() {
  325. if _, ok := visitedDirs[path]; ok {
  326. return filepath.SkipDir
  327. }
  328. visitedDirs[path] = struct{}{}
  329. }
  330. if err := fn(path, d, err); err != nil {
  331. return err
  332. }
  333. info, err := d.Info()
  334. if nil != err {
  335. return err
  336. }
  337. if info.Mode()&os.ModeSymlink == 0 {
  338. return nil
  339. }
  340. // path is a symlink
  341. rp, err := filepath.EvalSymlinks(path)
  342. if err != nil {
  343. return err
  344. }
  345. ri, err := os.Stat(rp)
  346. if err != nil {
  347. return err
  348. }
  349. if ri.IsDir() {
  350. return filelock.Walk(rp, getWalkFn(visitedDirs, fn))
  351. }
  352. return nil
  353. }
  354. }