1
// Copyright 2017 The Gitea Authors. All rights reserved.
2
// SPDX-License-Identifier: MIT
18
// PathJoinRel joins the path elements into a single path, each element is cleaned by path.Clean separately.
19
// It only returns the following values (like path.Join), any redundant part (empty, relative dots, slashes) is removed.
20
// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
27
// `foo\..\bar` => `foo\..\bar`
28
// {`foo`, ``, `bar`} => `foo/bar`
29
// {`foo`, `..`, `bar`} => `foo/bar`
30
func PathJoinRel(elem ...string) string {
31
elems := make([]string, len(elem))
32
for i, e := range elem {
36
elems[i] = path.Clean("/" + e)
38
p := path.Join(elems...)
47
// PathJoinRelX joins the path elements into a single path like PathJoinRel,
48
// and covert all backslashes to slashes. (X means "extended", also means the combination of `\` and `/`).
49
// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
50
// It returns similar results as PathJoinRel except:
52
// `foo\..\bar` => `bar` (because it's processed as `foo/../bar`)
54
// All backslashes are handled as slashes, the result only contains slashes.
55
func PathJoinRelX(elem ...string) string {
56
elems := make([]string, len(elem))
57
for i, e := range elem {
61
elems[i] = path.Clean("/" + strings.ReplaceAll(e, "\\", "/"))
63
return PathJoinRel(elems...)
66
const pathSeparator = string(os.PathSeparator)
68
// FilePathJoinAbs joins the path elements into a single file path, each element is cleaned by filepath.Clean separately.
69
// All slashes/backslashes are converted to path separators before cleaning, the result only contains path separators.
70
// The first element must be an absolute path, caller should prepare the base path.
71
// It's caller's duty to make every element not bypass its own directly level, to avoid security issues.
72
// Like PathJoinRel, any redundant part (empty, relative dots, slashes) is removed.
74
// {`/foo`, ``, `bar`} => `/foo/bar`
75
// {`/foo`, `..`, `bar`} => `/foo/bar`
76
func FilePathJoinAbs(base string, sub ...string) string {
77
elems := make([]string, 1, len(sub)+1)
79
// POSIX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators
80
// to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/`
82
elems[0] = filepath.Clean(base)
84
elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", pathSeparator))
86
if !filepath.IsAbs(elems[0]) {
87
// This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead
88
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))
90
for _, s := range sub {
95
elems = append(elems, filepath.Clean(pathSeparator+s))
97
elems = append(elems, filepath.Clean(pathSeparator+strings.ReplaceAll(s, "\\", pathSeparator)))
100
// the elems[0] must be an absolute path, just join them together
101
return filepath.Join(elems...)
104
// IsDir returns true if given path is a directory,
105
// or returns false when it's a file or does not exist.
106
func IsDir(dir string) (bool, error) {
107
f, err := os.Stat(dir)
109
return f.IsDir(), nil
111
if os.IsNotExist(err) {
117
// IsFile returns true if given path is a file,
118
// or returns false when it's a directory or does not exist.
119
func IsFile(filePath string) (bool, error) {
120
f, err := os.Stat(filePath)
122
return !f.IsDir(), nil
124
if os.IsNotExist(err) {
130
// IsExist checks whether a file or directory exists.
131
// It returns false when the file or directory does not exist.
132
func IsExist(path string) (bool, error) {
133
_, err := os.Stat(path)
134
if err == nil || os.IsExist(err) {
137
if os.IsNotExist(err) {
143
func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) {
144
dir, err := os.Open(dirPath)
150
fis, err := dir.Readdir(0)
155
statList := make([]string, 0)
156
for _, fi := range fis {
157
if CommonSkip(fi.Name()) {
161
relPath := path.Join(recPath, fi.Name())
162
curPath := path.Join(dirPath, fi.Name())
165
statList = append(statList, relPath+"/")
167
s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
171
statList = append(statList, s...)
172
} else if !isDirOnly {
173
statList = append(statList, relPath)
174
} else if followSymlinks && fi.Mode()&os.ModeSymlink != 0 {
175
link, err := os.Readlink(curPath)
180
isDir, err := IsDir(link)
186
statList = append(statList, relPath+"/")
188
s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks)
192
statList = append(statList, s...)
199
// StatDir gathers information of given directory by depth-first.
200
// It returns slice of file list and includes subdirectories if enabled;
201
// it returns error and nil slice when error occurs in underlying functions,
202
// or given path is not a directory or does not exist.
204
// Slice does not include given path itself.
205
// If subdirectories is enabled, they will have suffix '/'.
206
func StatDir(rootPath string, includeDir ...bool) ([]string, error) {
207
if isDir, err := IsDir(rootPath); err != nil {
210
return nil, errors.New("not a directory or does not exist: " + rootPath)
213
isIncludeDir := false
214
if len(includeDir) != 0 {
215
isIncludeDir = includeDir[0]
217
return statDir(rootPath, "", isIncludeDir, false, false)
220
func isOSWindows() bool {
221
return runtime.GOOS == "windows"
224
var driveLetterRegexp = regexp.MustCompile("/[A-Za-z]:/")
226
// FileURLToPath extracts the path information from a file://... url.
227
// It returns an error only if the URL is not a file URL.
228
func FileURLToPath(u *url.URL) (string, error) {
229
if u.Scheme != "file" {
230
return "", errors.New("URL scheme is not 'file': " + u.String())
239
// If it looks like there's a Windows drive letter at the beginning, strip off the leading slash.
240
if driveLetterRegexp.MatchString(path) {
246
// HomeDir returns path of '~'(in Linux) on Windows,
247
// it returns error when the variable does not exist.
248
func HomeDir() (home string, err error) {
249
// TODO: some users run Gitea with mismatched uid and "HOME=xxx" (they set HOME=xxx by environment manually)
250
// TODO: when running gitea as a sub command inside git, the HOME directory is not the user's home directory
251
// so at the moment we can not use `user.Current().HomeDir`
253
home = os.Getenv("USERPROFILE")
255
home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
258
home = os.Getenv("HOME")
262
return "", errors.New("cannot get home directory")
268
// CommonSkip will check a provided name to see if it represents file or directory that should not be watched
269
func CommonSkip(name string) bool {
278
return name[1:] == "humbs.db"
280
return name[1:] == "esktop.ini"
286
// IsReadmeFileName reports whether name looks like a README file
288
func IsReadmeFileName(name string) bool {
289
name = strings.ToLower(name)
292
} else if len(name) == 6 {
293
return name == "readme"
295
return name[:7] == "readme."
298
// IsReadmeFileExtension reports whether name looks like a README file
299
// based on its name. It will look through the provided extensions and check if the file matches
300
// one of the extensions and provide the index in the extension list.
301
// If the filename is `readme.` with an unmatched extension it will match with the index equaling
302
// the length of the provided extension list.
303
// Note that the '.' should be provided in ext, e.g ".md"
304
func IsReadmeFileExtension(name string, ext ...string) (int, bool) {
305
name = strings.ToLower(name)
306
if len(name) < 6 || name[:6] != "readme" {
310
for i, extension := range ext {
311
extension = strings.ToLower(extension)
312
if name[6:] == extension {
318
return len(ext), true