You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

path.go 8.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  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. if p == "" {
  37. return ""
  38. } else if p == "/" {
  39. return "."
  40. } else {
  41. return p[1:]
  42. }
  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 statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) {
  134. dir, err := os.Open(dirPath)
  135. if err != nil {
  136. return nil, err
  137. }
  138. defer dir.Close()
  139. fis, err := dir.Readdir(0)
  140. if err != nil {
  141. return nil, err
  142. }
  143. statList := make([]string, 0)
  144. for _, fi := range fis {
  145. if CommonSkip(fi.Name()) {
  146. continue
  147. }
  148. relPath := path.Join(recPath, fi.Name())
  149. curPath := path.Join(dirPath, fi.Name())
  150. if fi.IsDir() {
  151. if includeDir {
  152. statList = append(statList, relPath+"/")
  153. }
  154. s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
  155. if err != nil {
  156. return nil, err
  157. }
  158. statList = append(statList, s...)
  159. } else if !isDirOnly {
  160. statList = append(statList, relPath)
  161. } else if followSymlinks && fi.Mode()&os.ModeSymlink != 0 {
  162. link, err := os.Readlink(curPath)
  163. if err != nil {
  164. return nil, err
  165. }
  166. isDir, err := IsDir(link)
  167. if err != nil {
  168. return nil, err
  169. }
  170. if isDir {
  171. if includeDir {
  172. statList = append(statList, relPath+"/")
  173. }
  174. s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
  175. if err != nil {
  176. return nil, err
  177. }
  178. statList = append(statList, s...)
  179. }
  180. }
  181. }
  182. return statList, nil
  183. }
  184. // StatDir gathers information of given directory by depth-first.
  185. // It returns slice of file list and includes subdirectories if enabled;
  186. // it returns error and nil slice when error occurs in underlying functions,
  187. // or given path is not a directory or does not exist.
  188. //
  189. // Slice does not include given path itself.
  190. // If subdirectories is enabled, they will have suffix '/'.
  191. func StatDir(rootPath string, includeDir ...bool) ([]string, error) {
  192. if isDir, err := IsDir(rootPath); err != nil {
  193. return nil, err
  194. } else if !isDir {
  195. return nil, errors.New("not a directory or does not exist: " + rootPath)
  196. }
  197. isIncludeDir := false
  198. if len(includeDir) != 0 {
  199. isIncludeDir = includeDir[0]
  200. }
  201. return statDir(rootPath, "", isIncludeDir, false, false)
  202. }
  203. func isOSWindows() bool {
  204. return runtime.GOOS == "windows"
  205. }
  206. var driveLetterRegexp = regexp.MustCompile("/[A-Za-z]:/")
  207. // FileURLToPath extracts the path information from a file://... url.
  208. func FileURLToPath(u *url.URL) (string, error) {
  209. if u.Scheme != "file" {
  210. return "", errors.New("URL scheme is not 'file': " + u.String())
  211. }
  212. path := u.Path
  213. if !isOSWindows() {
  214. return path, nil
  215. }
  216. // If it looks like there's a Windows drive letter at the beginning, strip off the leading slash.
  217. if driveLetterRegexp.MatchString(path) {
  218. return path[1:], nil
  219. }
  220. return path, nil
  221. }
  222. // HomeDir returns path of '~'(in Linux) on Windows,
  223. // it returns error when the variable does not exist.
  224. func HomeDir() (home string, err error) {
  225. // TODO: some users run Gitea with mismatched uid and "HOME=xxx" (they set HOME=xxx by environment manually)
  226. // TODO: when running gitea as a sub command inside git, the HOME directory is not the user's home directory
  227. // so at the moment we can not use `user.Current().HomeDir`
  228. if isOSWindows() {
  229. home = os.Getenv("USERPROFILE")
  230. if home == "" {
  231. home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
  232. }
  233. } else {
  234. home = os.Getenv("HOME")
  235. }
  236. if home == "" {
  237. return "", errors.New("cannot get home directory")
  238. }
  239. return home, nil
  240. }
  241. // CommonSkip will check a provided name to see if it represents file or directory that should not be watched
  242. func CommonSkip(name string) bool {
  243. if name == "" {
  244. return true
  245. }
  246. switch name[0] {
  247. case '.':
  248. return true
  249. case 't', 'T':
  250. return name[1:] == "humbs.db"
  251. case 'd', 'D':
  252. return name[1:] == "esktop.ini"
  253. }
  254. return false
  255. }
  256. // IsReadmeFileName reports whether name looks like a README file
  257. // based on its name.
  258. func IsReadmeFileName(name string) bool {
  259. name = strings.ToLower(name)
  260. if len(name) < 6 {
  261. return false
  262. } else if len(name) == 6 {
  263. return name == "readme"
  264. }
  265. return name[:7] == "readme."
  266. }
  267. // IsReadmeFileExtension reports whether name looks like a README file
  268. // based on its name. It will look through the provided extensions and check if the file matches
  269. // one of the extensions and provide the index in the extension list.
  270. // If the filename is `readme.` with an unmatched extension it will match with the index equaling
  271. // the length of the provided extension list.
  272. // Note that the '.' should be provided in ext, e.g ".md"
  273. func IsReadmeFileExtension(name string, ext ...string) (int, bool) {
  274. name = strings.ToLower(name)
  275. if len(name) < 6 || name[:6] != "readme" {
  276. return 0, false
  277. }
  278. for i, extension := range ext {
  279. extension = strings.ToLower(extension)
  280. if name[6:] == extension {
  281. return i, true
  282. }
  283. }
  284. if name[6] == '.' {
  285. return len(ext), true
  286. }
  287. return 0, false
  288. }