code.gitea.io/gitea@v1.19.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 } else { 44 return p[1:] 45 } 46 } 47 48 // PathJoinRelX joins the path elements into a single path like PathJoinRel, 49 // and covert all backslashes to slashes. (X means "extended", also means the combination of `\` and `/`). 50 // It's caller's duty to make every element not bypass its own directly level, to avoid security issues. 51 // It returns similar results as PathJoinRel except: 52 // 53 // `foo\..\bar` => `bar` (because it's processed as `foo/../bar`) 54 // 55 // All backslashes are handled as slashes, the result only contains slashes. 56 func PathJoinRelX(elem ...string) string { 57 elems := make([]string, len(elem)) 58 for i, e := range elem { 59 if e == "" { 60 continue 61 } 62 elems[i] = path.Clean("/" + strings.ReplaceAll(e, "\\", "/")) 63 } 64 return PathJoinRel(elems...) 65 } 66 67 const pathSeparator = string(os.PathSeparator) 68 69 // FilePathJoinAbs joins the path elements into a single file path, each element is cleaned by filepath.Clean separately. 70 // All slashes/backslashes are converted to path separators before cleaning, the result only contains path separators. 71 // The first element must be an absolute path, caller should prepare the base path. 72 // It's caller's duty to make every element not bypass its own directly level, to avoid security issues. 73 // Like PathJoinRel, any redundant part (empty, relative dots, slashes) is removed. 74 // 75 // {`/foo`, ``, `bar`} => `/foo/bar` 76 // {`/foo`, `..`, `bar`} => `/foo/bar` 77 func FilePathJoinAbs(elem ...string) string { 78 elems := make([]string, len(elem)) 79 80 // POISX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators 81 // to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/` 82 if isOSWindows() { 83 elems[0] = filepath.Clean(elem[0]) 84 } else { 85 elems[0] = filepath.Clean(strings.ReplaceAll(elem[0], "\\", pathSeparator)) 86 } 87 if !filepath.IsAbs(elems[0]) { 88 // This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead 89 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 } 91 92 for i := 1; i < len(elem); i++ { 93 if elem[i] == "" { 94 continue 95 } 96 if isOSWindows() { 97 elems[i] = filepath.Clean(pathSeparator + elem[i]) 98 } else { 99 elems[i] = filepath.Clean(pathSeparator + strings.ReplaceAll(elem[i], "\\", pathSeparator)) 100 } 101 } 102 // the elems[0] must be an absolute path, just join them together 103 return filepath.Join(elems...) 104 } 105 106 // IsDir returns true if given path is a directory, 107 // or returns false when it's a file or does not exist. 108 func IsDir(dir string) (bool, error) { 109 f, err := os.Stat(dir) 110 if err == nil { 111 return f.IsDir(), nil 112 } 113 if os.IsNotExist(err) { 114 return false, nil 115 } 116 return false, err 117 } 118 119 // IsFile returns true if given path is a file, 120 // or returns false when it's a directory or does not exist. 121 func IsFile(filePath string) (bool, error) { 122 f, err := os.Stat(filePath) 123 if err == nil { 124 return !f.IsDir(), nil 125 } 126 if os.IsNotExist(err) { 127 return false, nil 128 } 129 return false, err 130 } 131 132 // IsExist checks whether a file or directory exists. 133 // It returns false when the file or directory does not exist. 134 func IsExist(path string) (bool, error) { 135 _, err := os.Stat(path) 136 if err == nil || os.IsExist(err) { 137 return true, nil 138 } 139 if os.IsNotExist(err) { 140 return false, nil 141 } 142 return false, err 143 } 144 145 func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) { 146 dir, err := os.Open(dirPath) 147 if err != nil { 148 return nil, err 149 } 150 defer dir.Close() 151 152 fis, err := dir.Readdir(0) 153 if err != nil { 154 return nil, err 155 } 156 157 statList := make([]string, 0) 158 for _, fi := range fis { 159 if CommonSkip(fi.Name()) { 160 continue 161 } 162 163 relPath := path.Join(recPath, fi.Name()) 164 curPath := path.Join(dirPath, fi.Name()) 165 if fi.IsDir() { 166 if includeDir { 167 statList = append(statList, relPath+"/") 168 } 169 s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks) 170 if err != nil { 171 return nil, err 172 } 173 statList = append(statList, s...) 174 } else if !isDirOnly { 175 statList = append(statList, relPath) 176 } else if followSymlinks && fi.Mode()&os.ModeSymlink != 0 { 177 link, err := os.Readlink(curPath) 178 if err != nil { 179 return nil, err 180 } 181 182 isDir, err := IsDir(link) 183 if err != nil { 184 return nil, err 185 } 186 if isDir { 187 if includeDir { 188 statList = append(statList, relPath+"/") 189 } 190 s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks) 191 if err != nil { 192 return nil, err 193 } 194 statList = append(statList, s...) 195 } 196 } 197 } 198 return statList, nil 199 } 200 201 // StatDir gathers information of given directory by depth-first. 202 // It returns slice of file list and includes subdirectories if enabled; 203 // it returns error and nil slice when error occurs in underlying functions, 204 // or given path is not a directory or does not exist. 205 // 206 // Slice does not include given path itself. 207 // If subdirectories is enabled, they will have suffix '/'. 208 func StatDir(rootPath string, includeDir ...bool) ([]string, error) { 209 if isDir, err := IsDir(rootPath); err != nil { 210 return nil, err 211 } else if !isDir { 212 return nil, errors.New("not a directory or does not exist: " + rootPath) 213 } 214 215 isIncludeDir := false 216 if len(includeDir) != 0 { 217 isIncludeDir = includeDir[0] 218 } 219 return statDir(rootPath, "", isIncludeDir, false, false) 220 } 221 222 func isOSWindows() bool { 223 return runtime.GOOS == "windows" 224 } 225 226 // FileURLToPath extracts the path information from a file://... url. 227 func FileURLToPath(u *url.URL) (string, error) { 228 if u.Scheme != "file" { 229 return "", errors.New("URL scheme is not 'file': " + u.String()) 230 } 231 232 path := u.Path 233 234 if !isOSWindows() { 235 return path, nil 236 } 237 238 // If it looks like there's a Windows drive letter at the beginning, strip off the leading slash. 239 re := regexp.MustCompile("/[A-Za-z]:/") 240 if re.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 }