gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package util
  4. import (
  5. "errors"
  6. "fmt"
  7. "net/url"
  8. "os"
  9. "path"
  10. "path/filepath"
  11. "regexp"
  12. "runtime"
  13. "strings"
  14. )
  15. // PathJoinRel joins the path elements into a single path, each element is cleaned by path.Clean separately.
  16. // It only returns the following values (like path.Join), any redundant part (empty, relative dots, slashes) is removed.
  17. // It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
  18. //
  19. // empty => ``
  20. // `` => ``
  21. // `..` => `.`
  22. // `dir` => `dir`
  23. // `/dir/` => `dir`
  24. // `foo\..\bar` => `foo\..\bar`
  25. // {`foo`, ``, `bar`} => `foo/bar`
  26. // {`foo`, `..`, `bar`} => `foo/bar`
  27. func PathJoinRel(elem ...string) string {
  28. elems := make([]string, len(elem))
  29. for i, e := range elem {
  30. if e == "" {
  31. continue
  32. }
  33. elems[i] = path.Clean("/" + e)
  34. }
  35. p := path.Join(elems...)
  36. switch p {
  37. case "":
  38. return ""
  39. case "/":
  40. return "."
  41. }
  42. return p[1:]
  43. }
  44. // PathJoinRelX joins the path elements into a single path like PathJoinRel,
  45. // and covert all backslashes to slashes. (X means "extended", also means the combination of `\` and `/`).
  46. // It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
  47. // It returns similar results as PathJoinRel except:
  48. //
  49. // `foo\..\bar` => `bar` (because it's processed as `foo/../bar`)
  50. //
  51. // All backslashes are handled as slashes, the result only contains slashes.
  52. func PathJoinRelX(elem ...string) string {
  53. elems := make([]string, len(elem))
  54. for i, e := range elem {
  55. if e == "" {
  56. continue
  57. }
  58. elems[i] = path.Clean("/" + strings.ReplaceAll(e, "\\", "/"))
  59. }
  60. return PathJoinRel(elems...)
  61. }
  62. const pathSeparator = string(os.PathSeparator)
  63. // FilePathJoinAbs joins the path elements into a single file path, each element is cleaned by filepath.Clean separately.
  64. // All slashes/backslashes are converted to path separators before cleaning, the result only contains path separators.
  65. // The first element must be an absolute path, caller should prepare the base path.
  66. // It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
  67. // Like PathJoinRel, any redundant part (empty, relative dots, slashes) is removed.
  68. //
  69. // {`/foo`, ``, `bar`} => `/foo/bar`
  70. // {`/foo`, `..`, `bar`} => `/foo/bar`
  71. func FilePathJoinAbs(base string, sub ...string) string {
  72. elems := make([]string, 1, len(sub)+1)
  73. // POSIX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators
  74. // to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/`
  75. if isOSWindows() {
  76. elems[0] = filepath.Clean(base)
  77. } else {
  78. elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", pathSeparator))
  79. }
  80. if !filepath.IsAbs(elems[0]) {
  81. // This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead
  82. panic(fmt.Sprintf("FilePathJoinAbs: %q (for path %v) is not absolute, do not guess a relative path based on current working directory", elems[0], elems))
  83. }
  84. for _, s := range sub {
  85. if s == "" {
  86. continue
  87. }
  88. if isOSWindows() {
  89. elems = append(elems, filepath.Clean(pathSeparator+s))
  90. } else {
  91. elems = append(elems, filepath.Clean(pathSeparator+strings.ReplaceAll(s, "\\", pathSeparator)))
  92. }
  93. }
  94. // the elems[0] must be an absolute path, just join them together
  95. return filepath.Join(elems...)
  96. }
  97. // IsDir returns true if given path is a directory,
  98. // or returns false when it's a file or does not exist.
  99. func IsDir(dir string) (bool, error) {
  100. f, err := os.Stat(dir)
  101. if err == nil {
  102. return f.IsDir(), nil
  103. }
  104. if os.IsNotExist(err) {
  105. return false, nil
  106. }
  107. return false, err
  108. }
  109. // IsFile returns true if given path is a file,
  110. // or returns false when it's a directory or does not exist.
  111. func IsFile(filePath string) (bool, error) {
  112. f, err := os.Stat(filePath)
  113. if err == nil {
  114. return !f.IsDir(), nil
  115. }
  116. if os.IsNotExist(err) {
  117. return false, nil
  118. }
  119. return false, err
  120. }
  121. // IsExist checks whether a file or directory exists.
  122. // It returns false when the file or directory does not exist.
  123. func IsExist(path string) (bool, error) {
  124. _, err := os.Stat(path)
  125. if err == nil || os.IsExist(err) {
  126. return true, nil
  127. }
  128. if os.IsNotExist(err) {
  129. return false, nil
  130. }
  131. return false, err
  132. }
  133. func listDirRecursively(result *[]string, fsDir, recordParentPath string, opts *ListDirOptions) error {
  134. dir, err := os.Open(fsDir)
  135. if err != nil {
  136. return err
  137. }
  138. defer dir.Close()
  139. fis, err := dir.Readdir(0)
  140. if err != nil {
  141. return err
  142. }
  143. for _, fi := range fis {
  144. if opts.SkipCommonHiddenNames && IsCommonHiddenFileName(fi.Name()) {
  145. continue
  146. }
  147. relPath := path.Join(recordParentPath, fi.Name())
  148. curPath := filepath.Join(fsDir, fi.Name())
  149. if fi.IsDir() {
  150. if opts.IncludeDir {
  151. *result = append(*result, relPath+"/")
  152. }
  153. if err = listDirRecursively(result, curPath, relPath, opts); err != nil {
  154. return err
  155. }
  156. } else {
  157. *result = append(*result, relPath)
  158. }
  159. }
  160. return nil
  161. }
  162. type ListDirOptions struct {
  163. IncludeDir bool // subdirectories are also included with suffix slash
  164. SkipCommonHiddenNames bool
  165. }
  166. // ListDirRecursively gathers information of given directory by depth-first.
  167. // The paths are always in "dir/slash/file" format (not "\\" even in Windows)
  168. // Slice does not include given path itself.
  169. func ListDirRecursively(rootDir string, opts *ListDirOptions) (res []string, err error) {
  170. if err = listDirRecursively(&res, rootDir, "", opts); err != nil {
  171. return nil, err
  172. }
  173. return res, nil
  174. }
  175. func isOSWindows() bool {
  176. return runtime.GOOS == "windows"
  177. }
  178. var driveLetterRegexp = regexp.MustCompile("/[A-Za-z]:/")
  179. // FileURLToPath extracts the path information from a file://... url.
  180. // It returns an error only if the URL is not a file URL.
  181. func FileURLToPath(u *url.URL) (string, error) {
  182. if u.Scheme != "file" {
  183. return "", errors.New("URL scheme is not 'file': " + u.String())
  184. }
  185. path := u.Path
  186. if !isOSWindows() {
  187. return path, nil
  188. }
  189. // If it looks like there's a Windows drive letter at the beginning, strip off the leading slash.
  190. if driveLetterRegexp.MatchString(path) {
  191. return path[1:], nil
  192. }
  193. return path, nil
  194. }
  195. // HomeDir returns path of '~'(in Linux) on Windows,
  196. // it returns error when the variable does not exist.
  197. func HomeDir() (home string, err error) {
  198. // TODO: some users run Gitea with mismatched uid and "HOME=xxx" (they set HOME=xxx by environment manually)
  199. // TODO: when running gitea as a sub command inside git, the HOME directory is not the user's home directory
  200. // so at the moment we can not use `user.Current().HomeDir`
  201. if isOSWindows() {
  202. home = os.Getenv("USERPROFILE")
  203. if home == "" {
  204. home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
  205. }
  206. } else {
  207. home = os.Getenv("HOME")
  208. }
  209. if home == "" {
  210. return "", errors.New("cannot get home directory")
  211. }
  212. return home, nil
  213. }
  214. // IsCommonHiddenFileName will check a provided name to see if it represents file or directory that should not be watched
  215. func IsCommonHiddenFileName(name string) bool {
  216. if name == "" {
  217. return true
  218. }
  219. switch name[0] {
  220. case '.':
  221. return true
  222. case 't', 'T':
  223. return name[1:] == "humbs.db" // macOS
  224. case 'd', 'D':
  225. return name[1:] == "esktop.ini" // Windows
  226. }
  227. return false
  228. }
  229. // IsReadmeFileName reports whether name looks like a README file
  230. // based on its name.
  231. func IsReadmeFileName(name string) bool {
  232. name = strings.ToLower(name)
  233. if len(name) < 6 {
  234. return false
  235. } else if len(name) == 6 {
  236. return name == "readme"
  237. }
  238. return name[:7] == "readme."
  239. }
  240. // IsReadmeFileExtension reports whether name looks like a README file
  241. // based on its name. It will look through the provided extensions and check if the file matches
  242. // one of the extensions and provide the index in the extension list.
  243. // If the filename is `readme.` with an unmatched extension it will match with the index equaling
  244. // the length of the provided extension list.
  245. // Note that the '.' should be provided in ext, e.g ".md"
  246. func IsReadmeFileExtension(name string, ext ...string) (int, bool) {
  247. name = strings.ToLower(name)
  248. if len(name) < 6 || name[:6] != "readme" {
  249. return 0, false
  250. }
  251. for i, extension := range ext {
  252. extension = strings.ToLower(extension)
  253. if name[6:] == extension {
  254. return i, true
  255. }
  256. }
  257. if name[6] == '.' {
  258. return len(ext), true
  259. }
  260. return 0, false
  261. }