code.gitea.io/gitea@v1.22.3/modules/util/path.go (about) 1 // Copyright 2017 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package util 5 6 import ( 7 "errors" 8 "fmt" 9 "net/url" 10 "os" 11 "path" 12 "path/filepath" 13 "regexp" 14 "runtime" 15 "strings" 16 ) 17 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. 21 // 22 // empty => `` 23 // `` => `` 24 // `..` => `.` 25 // `dir` => `dir` 26 // `/dir/` => `dir` 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 { 33 if e == "" { 34 continue 35 } 36 elems[i] = path.Clean("/" + e) 37 } 38 p := path.Join(elems...) 39 if p == "" { 40 return "" 41 } else if p == "/" { 42 return "." 43 } 44 return p[1:] 45 } 46 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: 51 // 52 // `foo\..\bar` => `bar` (because it's processed as `foo/../bar`) 53 // 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 { 58 if e == "" { 59 continue 60 } 61 elems[i] = path.Clean("/" + strings.ReplaceAll(e, "\\", "/")) 62 } 63 return PathJoinRel(elems...) 64 } 65 66 const pathSeparator = string(os.PathSeparator) 67 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. 73 // 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) 78 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 `/` 81 if isOSWindows() { 82 elems[0] = filepath.Clean(base) 83 } else { 84 elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", pathSeparator)) 85 } 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)) 89 } 90 for _, s := range sub { 91 if s == "" { 92 continue 93 } 94 if isOSWindows() { 95 elems = append(elems, filepath.Clean(pathSeparator+s)) 96 } else { 97 elems = append(elems, filepath.Clean(pathSeparator+strings.ReplaceAll(s, "\\", pathSeparator))) 98 } 99 } 100 // the elems[0] must be an absolute path, just join them together 101 return filepath.Join(elems...) 102 } 103 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) 108 if err == nil { 109 return f.IsDir(), nil 110 } 111 if os.IsNotExist(err) { 112 return false, nil 113 } 114 return false, err 115 } 116 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) 121 if err == nil { 122 return !f.IsDir(), nil 123 } 124 if os.IsNotExist(err) { 125 return false, nil 126 } 127 return false, err 128 } 129 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) { 135 return true, nil 136 } 137 if os.IsNotExist(err) { 138 return false, nil 139 } 140 return false, err 141 } 142 143 func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) { 144 dir, err := os.Open(dirPath) 145 if err != nil { 146 return nil, err 147 } 148 defer dir.Close() 149 150 fis, err := dir.Readdir(0) 151 if err != nil { 152 return nil, err 153 } 154 155 statList := make([]string, 0) 156 for _, fi := range fis { 157 if CommonSkip(fi.Name()) { 158 continue 159 } 160 161 relPath := path.Join(recPath, fi.Name()) 162 curPath := path.Join(dirPath, fi.Name()) 163 if fi.IsDir() { 164 if includeDir { 165 statList = append(statList, relPath+"/") 166 } 167 s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks) 168 if err != nil { 169 return nil, err 170 } 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) 176 if err != nil { 177 return nil, err 178 } 179 180 isDir, err := IsDir(link) 181 if err != nil { 182 return nil, err 183 } 184 if isDir { 185 if includeDir { 186 statList = append(statList, relPath+"/") 187 } 188 s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks) 189 if err != nil { 190 return nil, err 191 } 192 statList = append(statList, s...) 193 } 194 } 195 } 196 return statList, nil 197 } 198 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. 203 // 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 { 208 return nil, err 209 } else if !isDir { 210 return nil, errors.New("not a directory or does not exist: " + rootPath) 211 } 212 213 isIncludeDir := false 214 if len(includeDir) != 0 { 215 isIncludeDir = includeDir[0] 216 } 217 return statDir(rootPath, "", isIncludeDir, false, false) 218 } 219 220 func isOSWindows() bool { 221 return runtime.GOOS == "windows" 222 } 223 224 var driveLetterRegexp = regexp.MustCompile("/[A-Za-z]:/") 225 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()) 231 } 232 233 path := u.Path 234 235 if !isOSWindows() { 236 return path, nil 237 } 238 239 // If it looks like there's a Windows drive letter at the beginning, strip off the leading slash. 240 if driveLetterRegexp.MatchString(path) { 241 return path[1:], nil 242 } 243 return path, nil 244 } 245 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` 252 if isOSWindows() { 253 home = os.Getenv("USERPROFILE") 254 if home == "" { 255 home = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 256 } 257 } else { 258 home = os.Getenv("HOME") 259 } 260 261 if home == "" { 262 return "", errors.New("cannot get home directory") 263 } 264 265 return home, nil 266 } 267 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 { 270 if name == "" { 271 return true 272 } 273 274 switch name[0] { 275 case '.': 276 return true 277 case 't', 'T': 278 return name[1:] == "humbs.db" 279 case 'd', 'D': 280 return name[1:] == "esktop.ini" 281 } 282 283 return false 284 } 285 286 // IsReadmeFileName reports whether name looks like a README file 287 // based on its name. 288 func IsReadmeFileName(name string) bool { 289 name = strings.ToLower(name) 290 if len(name) < 6 { 291 return false 292 } else if len(name) == 6 { 293 return name == "readme" 294 } 295 return name[:7] == "readme." 296 } 297 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" { 307 return 0, false 308 } 309 310 for i, extension := range ext { 311 extension = strings.ToLower(extension) 312 if name[6:] == extension { 313 return i, true 314 } 315 } 316 317 if name[6] == '.' { 318 return len(ext), true 319 } 320 321 return 0, false 322 }