loading up the forgejo repo on tangled to test page performance
at forgejo 8.5 kB view raw
1// Copyright 2017 The Gitea Authors. All rights reserved. 2// SPDX-License-Identifier: MIT 3 4package util 5 6import ( 7 "errors" 8 "fmt" 9 "net/url" 10 "os" 11 "path" 12 "path/filepath" 13 "strings" 14) 15 16// PathJoinRel joins the path elements into a single path, each element is cleaned by path.Clean separately. 17// It only returns the following values (like path.Join), any redundant part (empty, relative dots, slashes) is removed. 18// It's caller's duty to make every element not bypass its own directly level, to avoid security issues. 19// 20// empty => `` 21// `` => `` 22// `..` => `.` 23// `dir` => `dir` 24// `/dir/` => `dir` 25// `foo\..\bar` => `foo\..\bar` 26// {`foo`, ``, `bar`} => `foo/bar` 27// {`foo`, `..`, `bar`} => `foo/bar` 28func PathJoinRel(elem ...string) string { 29 elems := make([]string, len(elem)) 30 for i, e := range elem { 31 if e == "" { 32 continue 33 } 34 elems[i] = path.Clean("/" + e) 35 } 36 p := path.Join(elems...) 37 switch p { 38 case "": 39 return "" 40 case "/": 41 return "." 42 } 43 return p[1:] 44} 45 46// PathJoinRelX joins the path elements into a single path like PathJoinRel, 47// and convert all backslashes to slashes. (X means "extended", also means the combination of `\` and `/`). 48// It's caller's duty to make every element not bypass its own directly level, to avoid security issues. 49// It returns similar results as PathJoinRel except: 50// 51// `foo\..\bar` => `bar` (because it's processed as `foo/../bar`) 52// 53// All backslashes are handled as slashes, the result only contains slashes. 54func PathJoinRelX(elem ...string) string { 55 elems := make([]string, len(elem)) 56 for i, e := range elem { 57 if e == "" { 58 continue 59 } 60 elems[i] = path.Clean("/" + strings.ReplaceAll(e, "\\", "/")) 61 } 62 return PathJoinRel(elems...) 63} 64 65const pathSeparator = string(os.PathSeparator) 66 67// FilePathJoinAbs joins the path elements into a single file path, each element is cleaned by filepath.Clean separately. 68// All slashes/backslashes are converted to path separators before cleaning, the result only contains path separators. 69// The first element must be an absolute path, caller should prepare the base path. 70// It's caller's duty to make every element not bypass its own directly level, to avoid security issues. 71// Like PathJoinRel, any redundant part (empty, relative dots, slashes) is removed. 72// 73// {`/foo`, ``, `bar`} => `/foo/bar` 74// {`/foo`, `..`, `bar`} => `/foo/bar` 75func FilePathJoinAbs(base string, sub ...string) string { 76 elems := make([]string, 1, len(sub)+1) 77 78 // POSIX filesystem can have `\` in file names. Windows: `\` and `/` are both used for path separators 79 // to keep the behavior consistent, we do not allow `\` in file names, replace all `\` with `/` 80 elems[0] = filepath.Clean(strings.ReplaceAll(base, "\\", pathSeparator)) 81 if !filepath.IsAbs(elems[0]) { 82 // This shouldn't happen. If there is really necessary to pass in relative path, return the full path with filepath.Abs() instead 83 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)) 84 } 85 for _, s := range sub { 86 if s == "" { 87 continue 88 } 89 elems = append(elems, filepath.Clean(pathSeparator+strings.ReplaceAll(s, "\\", pathSeparator))) 90 } 91 // the elems[0] must be an absolute path, just join them together 92 return filepath.Join(elems...) 93} 94 95// IsDir returns true if given path is a directory, 96// or returns false when it's a file or does not exist. 97func IsDir(dir string) (bool, error) { 98 f, err := os.Stat(dir) 99 if err == nil { 100 return f.IsDir(), nil 101 } 102 if os.IsNotExist(err) { 103 return false, nil 104 } 105 return false, err 106} 107 108// IsFile returns true if given path is a file, 109// or returns false when it's a directory or does not exist. 110func IsFile(filePath string) (bool, error) { 111 f, err := os.Stat(filePath) 112 if err == nil { 113 return !f.IsDir(), nil 114 } 115 if os.IsNotExist(err) { 116 return false, nil 117 } 118 return false, err 119} 120 121// IsExist checks whether a file or directory exists. 122// It returns false when the file or directory does not exist. 123func 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 134func statDir(dirPath, recPath string, includeDir, isDirOnly, followSymlinks bool) ([]string, error) { 135 dir, err := os.Open(dirPath) 136 if err != nil { 137 return nil, err 138 } 139 defer dir.Close() 140 141 fis, err := dir.Readdir(0) 142 if err != nil { 143 return nil, err 144 } 145 146 statList := make([]string, 0) 147 for _, fi := range fis { 148 if CommonSkip(fi.Name()) { 149 continue 150 } 151 152 relPath := path.Join(recPath, fi.Name()) 153 curPath := path.Join(dirPath, fi.Name()) 154 if fi.IsDir() { 155 if includeDir { 156 statList = append(statList, relPath+"/") 157 } 158 s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks) 159 if err != nil { 160 return nil, err 161 } 162 statList = append(statList, s...) 163 } else if !isDirOnly { 164 statList = append(statList, relPath) 165 } else if followSymlinks && fi.Mode()&os.ModeSymlink != 0 { 166 link, err := os.Readlink(curPath) 167 if err != nil { 168 return nil, err 169 } 170 171 isDir, err := IsDir(link) 172 if err != nil { 173 return nil, err 174 } 175 if isDir { 176 if includeDir { 177 statList = append(statList, relPath+"/") 178 } 179 s, err := statDir(curPath, relPath, includeDir, isDirOnly, followSymlinks) 180 if err != nil { 181 return nil, err 182 } 183 statList = append(statList, s...) 184 } 185 } 186 } 187 return statList, nil 188} 189 190// StatDir gathers information of given directory by depth-first. 191// It returns slice of file list and includes subdirectories if enabled; 192// it returns error and nil slice when error occurs in underlying functions, 193// or given path is not a directory or does not exist. 194// 195// Slice does not include given path itself. 196// If subdirectories is enabled, they will have suffix '/'. 197func StatDir(rootPath string, includeDir ...bool) ([]string, error) { 198 if isDir, err := IsDir(rootPath); err != nil { 199 return nil, err 200 } else if !isDir { 201 return nil, errors.New("not a directory or does not exist: " + rootPath) 202 } 203 204 isIncludeDir := false 205 if len(includeDir) != 0 { 206 isIncludeDir = includeDir[0] 207 } 208 return statDir(rootPath, "", isIncludeDir, false, false) 209} 210 211// FileURLToPath extracts the path information from a file://... url. 212// It returns an error only if the URL is not a file URL. 213func FileURLToPath(u *url.URL) (string, error) { 214 if u.Scheme != "file" { 215 return "", errors.New("URL scheme is not 'file': " + u.String()) 216 } 217 218 return u.Path, nil 219} 220 221// HomeDir returns path of '~'(in Linux) on Windows, 222// it returns error when the variable does not exist. 223func HomeDir() (home string, err error) { 224 // TODO: some users run Gitea with mismatched uid and "HOME=xxx" (they set HOME=xxx by environment manually) 225 // TODO: when running gitea as a sub command inside git, the HOME directory is not the user's home directory 226 // so at the moment we can not use `user.Current().HomeDir` 227 home = os.Getenv("HOME") 228 229 if home == "" { 230 return "", errors.New("cannot get home directory") 231 } 232 233 return home, nil 234} 235 236// CommonSkip will check a provided name to see if it represents file or directory that should not be watched 237func CommonSkip(name string) bool { 238 if name == "" { 239 return true 240 } 241 242 switch name[0] { 243 case '.': 244 return true 245 case 't', 'T': 246 return name[1:] == "humbs.db" 247 case 'd', 'D': 248 return name[1:] == "esktop.ini" 249 } 250 251 return false 252} 253 254// IsReadmeFileName reports whether name looks like a README file 255// based on its name. 256func IsReadmeFileName(name string) bool { 257 name = strings.ToLower(name) 258 if len(name) < 6 { 259 return false 260 } else if len(name) == 6 { 261 return name == "readme" 262 } 263 return name[:7] == "readme." 264} 265 266// IsReadmeFileExtension reports whether name looks like a README file 267// based on its name. It will look through the provided extensions and check if the file matches 268// one of the extensions and provide the index in the extension list. 269// If the filename is `readme.` with an unmatched extension it will match with the index equaling 270// the length of the provided extension list. 271// Note that the '.' should be provided in ext, e.g ".md" 272func IsReadmeFileExtension(name string, ext ...string) (int, bool) { 273 name = strings.ToLower(name) 274 if len(name) < 6 || name[:6] != "readme" { 275 return 0, false 276 } 277 278 for i, extension := range ext { 279 extension = strings.ToLower(extension) 280 if name[6:] == extension { 281 return i, true 282 } 283 } 284 285 if name[6] == '.' { 286 return len(ext), true 287 } 288 289 return 0, false 290}