summaryrefslogtreecommitdiffstats
path: root/modules/util/path.go
diff options
context:
space:
mode:
Diffstat (limited to 'modules/util/path.go')
-rw-r--r--modules/util/path.go322
1 files changed, 322 insertions, 0 deletions
diff --git a/modules/util/path.go b/modules/util/path.go
new file mode 100644
index 0000000..185e7cf
--- /dev/null
+++ b/modules/util/path.go
@@ -0,0 +1,322 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package util
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "strings"
+)
+
+// PathJoinRel joins the path elements into a single path, each element is cleaned by path.Clean separately.
+// It only returns the following values (like path.Join), any redundant part (empty, relative dots, slashes) is removed.
+// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
+//
+// empty => ``
+// `` => ``
+// `..` => `.`
+// `dir` => `dir`
+// `/dir/` => `dir`
+// `foo\..\bar` => `foo\..\bar`
+// {`foo`, ``, `bar`} => `foo/bar`
+// {`foo`, `..`, `bar`} => `foo/bar`
+func PathJoinRel(elem ...string) string {
+ elems := make([]string, len(elem))
+ for i, e := range elem {
+ if e == "" {
+ continue
+ }
+ elems[i] = path.Clean("/" + e)
+ }
+ p := path.Join(elems...)
+ if p == "" {
+ return ""
+ } else if p == "/" {
+ return "."
+ }
+ return p[1:]
+}
+
+// PathJoinRelX joins the path elements into a single path like PathJoinRel,
+// and convert all backslashes to slashes. (X means "extended", also means the combination of `\` and `/`).
+// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
+// It returns similar results as PathJoinRel except:
+//
+// `foo\..\bar` => `bar` (because it's processed as `foo/../bar`)
+//
+// All backslashes are handled as slashes, the result only contains slashes.
+func PathJoinRelX(elem ...string) string {
+ elems := make([]string, len(elem))
+ for i, e := range elem {
+ if e == "" {
+ continue
+ }
+ elems[i] = path.Clean("/" + strings.ReplaceAll(e, "\\", "/"))
+ }
+ return PathJoinRel(elems...)
+}
+
+const pathSeparator = string(os.PathSeparator)
+
+// FilePathJoinAbs joins the path elements into a single file path, each element is cleaned by filepath.Clean separately.
+// All slashes/backslashes are converted to path separators before cleaning, the result only contains path separators.
+// The first element must be an absolute path, caller should prepare the base path.
+// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
+// Like PathJoinRel, any redundant part (empty, relative dots, slashes) is removed.
+//
+// {`/foo`, ``, `bar`} => `/foo/bar`
+// {`/foo`, `..`, `bar`} => `/foo/bar`
+func FilePathJoinAbs(base string, sub ...string) string {
+ elems := make([]string, 1, len(sub)+1)
+
+ // POSIX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators
+ // to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/`
+ if isOSWindows() {
+ elems[0] = filepath.Clean(base)
+ } else {
+ elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", pathSeparator))
+ }
+ if !filepath.IsAbs(elems[0]) {
+ // This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead
+ 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))
+ }
+ for _, s := range sub {
+ if s == "" {
+ continue
+ }
+ if isOSWindows() {
+ elems = append(elems, filepath.Clean(pathSeparator+s))
+ } else {
+ elems = append(elems, filepath.Clean(pathSeparator+strings.ReplaceAll(s, "\\", pathSeparator)))
+ }
+ }
+ // the elems[0] must be an absolute path, just join them together
+ return filepath.Join(elems...)
+}
+
+// IsDir returns true if given path is a directory,
+// or returns false when it's a file or does not exist.
+func IsDir(dir string) (bool, error) {
+ f, err := os.Stat(dir)
+ if err == nil {
+ return f.IsDir(), nil
+ }
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+ return false, err
+}
+
+// IsFile returns true if given path is a file,
+// or returns false when it's a directory or does not exist.
+func IsFile(filePath string) (bool, error) {
+ f, err := os.Stat(filePath)
+ if err == nil {
+ return !f.IsDir(), nil
+ }
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+ return false, err
+}
+
+// IsExist checks whether a file or directory exists.
+// It returns false when the file or directory does not exist.
+func IsExist(path string) (bool, error) {
+ _, err := os.Stat(path)
+ if err == nil || os.IsExist(err) {
+ return true, nil
+ }
+ if os.IsNotExist(err) {
+ return false, nil
+ }
+ return false, err
+}
+
+func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) {
+ dir, err := os.Open(dirPath)
+ if err != nil {
+ return nil, err
+ }
+ defer dir.Close()
+
+ fis, err := dir.Readdir(0)
+ if err != nil {
+ return nil, err
+ }
+
+ statList := make([]string, 0)
+ for _, fi := range fis {
+ if CommonSkip(fi.Name()) {
+ continue
+ }
+
+ relPath := path.Join(recPath, fi.Name())
+ curPath := path.Join(dirPath, fi.Name())
+ if fi.IsDir() {
+ if includeDir {
+ statList = append(statList, relPath+"/")
+ }
+ s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
+ if err != nil {
+ return nil, err
+ }
+ statList = append(statList, s...)
+ } else if !isDirOnly {
+ statList = append(statList, relPath)
+ } else if followSymlinks && fi.Mode()&os.ModeSymlink != 0 {
+ link, err := os.Readlink(curPath)
+ if err != nil {
+ return nil, err
+ }
+
+ isDir, err := IsDir(link)
+ if err != nil {
+ return nil, err
+ }
+ if isDir {
+ if includeDir {
+ statList = append(statList, relPath+"/")
+ }
+ s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
+ if err != nil {
+ return nil, err
+ }
+ statList = append(statList, s...)
+ }
+ }
+ }
+ return statList, nil
+}
+
+// StatDir gathers information of given directory by depth-first.
+// It returns slice of file list and includes subdirectories if enabled;
+// it returns error and nil slice when error occurs in underlying functions,
+// or given path is not a directory or does not exist.
+//
+// Slice does not include given path itself.
+// If subdirectories is enabled, they will have suffix '/'.
+func StatDir(rootPath string, includeDir ...bool) ([]string, error) {
+ if isDir, err := IsDir(rootPath); err != nil {
+ return nil, err
+ } else if !isDir {
+ return nil, errors.New("not a directory or does not exist: " + rootPath)
+ }
+
+ isIncludeDir := false
+ if len(includeDir) != 0 {
+ isIncludeDir = includeDir[0]
+ }
+ return statDir(rootPath, "", isIncludeDir, false, false)
+}
+
+func isOSWindows() bool {
+ return runtime.GOOS == "windows"
+}
+
+var driveLetterRegexp = regexp.MustCompile("/[A-Za-z]:/")
+
+// FileURLToPath extracts the path information from a file://... url.
+// It returns an error only if the URL is not a file URL.
+func FileURLToPath(u *url.URL) (string, error) {
+ if u.Scheme != "file" {
+ return "", errors.New("URL scheme is not 'file': " + u.String())
+ }
+
+ path := u.Path
+
+ if !isOSWindows() {
+ return path, nil
+ }
+
+ // If it looks like there's a Windows drive letter at the beginning, strip off the leading slash.
+ if driveLetterRegexp.MatchString(path) {
+ return path[1:], nil
+ }
+ return path, nil
+}
+
+// HomeDir returns path of '~'(in Linux) on Windows,
+// it returns error when the variable does not exist.
+func HomeDir() (home string, err error) {
+ // TODO: some users run Gitea with mismatched uid and "HOME=xxx" (they set HOME=xxx by environment manually)
+ // TODO: when running gitea as a sub command inside git, the HOME directory is not the user's home directory
+ // so at the moment we can not use `user.Current().HomeDir`
+ if isOSWindows() {
+ home = os.Getenv("USERPROFILE")
+ if home == "" {
+ home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
+ }
+ } else {
+ home = os.Getenv("HOME")
+ }
+
+ if home == "" {
+ return "", errors.New("cannot get home directory")
+ }
+
+ return home, nil
+}
+
+// CommonSkip will check a provided name to see if it represents file or directory that should not be watched
+func CommonSkip(name string) bool {
+ if name == "" {
+ return true
+ }
+
+ switch name[0] {
+ case '.':
+ return true
+ case 't', 'T':
+ return name[1:] == "humbs.db"
+ case 'd', 'D':
+ return name[1:] == "esktop.ini"
+ }
+
+ return false
+}
+
+// IsReadmeFileName reports whether name looks like a README file
+// based on its name.
+func IsReadmeFileName(name string) bool {
+ name = strings.ToLower(name)
+ if len(name) < 6 {
+ return false
+ } else if len(name) == 6 {
+ return name == "readme"
+ }
+ return name[:7] == "readme."
+}
+
+// IsReadmeFileExtension reports whether name looks like a README file
+// based on its name. It will look through the provided extensions and check if the file matches
+// one of the extensions and provide the index in the extension list.
+// If the filename is `readme.` with an unmatched extension it will match with the index equaling
+// the length of the provided extension list.
+// Note that the '.' should be provided in ext, e.g ".md"
+func IsReadmeFileExtension(name string, ext ...string) (int, bool) {
+ name = strings.ToLower(name)
+ if len(name) < 6 || name[:6] != "readme" {
+ return 0, false
+ }
+
+ for i, extension := range ext {
+ extension = strings.ToLower(extension)
+ if name[6:] == extension {
+ return i, true
+ }
+ }
+
+ if name[6] == '.' {
+ return len(ext), true
+ }
+
+ return 0, false
+}