github.com/xushiwei/go@v0.0.0-20130601165731-2b9d83f45bc9/src/cmd/godoc/filesystem.go (about) 1 // Copyright 2011 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // This file defines types for abstract file system access and 6 // provides an implementation accessing the file system of the 7 // underlying OS. 8 9 package main 10 11 import ( 12 "fmt" 13 "io" 14 "io/ioutil" 15 "net/http" 16 "os" 17 pathpkg "path" 18 "path/filepath" 19 "sort" 20 "strings" 21 "time" 22 ) 23 24 // fs is the file system that godoc reads from and serves. 25 // It is a virtual file system that operates on slash-separated paths, 26 // and its root corresponds to the Go distribution root: /src/pkg 27 // holds the source tree, and so on. This means that the URLs served by 28 // the godoc server are the same as the paths in the virtual file 29 // system, which helps keep things simple. 30 // 31 // New file trees - implementations of FileSystem - can be added to 32 // the virtual file system using nameSpace's Bind method. 33 // The usual setup is to bind OS(runtime.GOROOT) to the root 34 // of the name space and then bind any GOPATH/src directories 35 // on top of /src/pkg, so that all sources are in /src/pkg. 36 // 37 // For more about name spaces, see the nameSpace type's 38 // documentation below. 39 // 40 // The use of this virtual file system means that most code processing 41 // paths can assume they are slash-separated and should be using 42 // package path (often imported as pathpkg) to manipulate them, 43 // even on Windows. 44 // 45 var fs = nameSpace{} // the underlying file system for godoc 46 47 // Setting debugNS = true will enable debugging prints about 48 // name space translations. 49 const debugNS = false 50 51 // The FileSystem interface specifies the methods godoc is using 52 // to access the file system for which it serves documentation. 53 type FileSystem interface { 54 Open(path string) (readSeekCloser, error) 55 Lstat(path string) (os.FileInfo, error) 56 Stat(path string) (os.FileInfo, error) 57 ReadDir(path string) ([]os.FileInfo, error) 58 String() string 59 } 60 61 type readSeekCloser interface { 62 io.Reader 63 io.Seeker 64 io.Closer 65 } 66 67 // ReadFile reads the file named by path from fs and returns the contents. 68 func ReadFile(fs FileSystem, path string) ([]byte, error) { 69 rc, err := fs.Open(path) 70 if err != nil { 71 return nil, err 72 } 73 defer rc.Close() 74 return ioutil.ReadAll(rc) 75 } 76 77 // OS returns an implementation of FileSystem reading from the 78 // tree rooted at root. Recording a root is convenient everywhere 79 // but necessary on Windows, because the slash-separated path 80 // passed to Open has no way to specify a drive letter. Using a root 81 // lets code refer to OS(`c:\`), OS(`d:\`) and so on. 82 func OS(root string) FileSystem { 83 return osFS(root) 84 } 85 86 type osFS string 87 88 func (root osFS) String() string { return "os(" + string(root) + ")" } 89 90 func (root osFS) resolve(path string) string { 91 // Clean the path so that it cannot possibly begin with ../. 92 // If it did, the result of filepath.Join would be outside the 93 // tree rooted at root. We probably won't ever see a path 94 // with .. in it, but be safe anyway. 95 path = pathpkg.Clean("/" + path) 96 97 return filepath.Join(string(root), path) 98 } 99 100 func (root osFS) Open(path string) (readSeekCloser, error) { 101 f, err := os.Open(root.resolve(path)) 102 if err != nil { 103 return nil, err 104 } 105 fi, err := f.Stat() 106 if err != nil { 107 return nil, err 108 } 109 if fi.IsDir() { 110 return nil, fmt.Errorf("Open: %s is a directory", path) 111 } 112 return f, nil 113 } 114 115 func (root osFS) Lstat(path string) (os.FileInfo, error) { 116 return os.Lstat(root.resolve(path)) 117 } 118 119 func (root osFS) Stat(path string) (os.FileInfo, error) { 120 return os.Stat(root.resolve(path)) 121 } 122 123 func (root osFS) ReadDir(path string) ([]os.FileInfo, error) { 124 return ioutil.ReadDir(root.resolve(path)) // is sorted 125 } 126 127 // hasPathPrefix returns true if x == y or x == y + "/" + more 128 func hasPathPrefix(x, y string) bool { 129 return x == y || strings.HasPrefix(x, y) && (strings.HasSuffix(y, "/") || strings.HasPrefix(x[len(y):], "/")) 130 } 131 132 // A nameSpace is a file system made up of other file systems 133 // mounted at specific locations in the name space. 134 // 135 // The representation is a map from mount point locations 136 // to the list of file systems mounted at that location. A traditional 137 // Unix mount table would use a single file system per mount point, 138 // but we want to be able to mount multiple file systems on a single 139 // mount point and have the system behave as if the union of those 140 // file systems were present at the mount point. 141 // For example, if the OS file system has a Go installation in 142 // c:\Go and additional Go path trees in d:\Work1 and d:\Work2, then 143 // this name space creates the view we want for the godoc server: 144 // 145 // nameSpace{ 146 // "/": { 147 // {old: "/", fs: OS(`c:\Go`), new: "/"}, 148 // }, 149 // "/src/pkg": { 150 // {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"}, 151 // {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"}, 152 // {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"}, 153 // }, 154 // } 155 // 156 // This is created by executing: 157 // 158 // ns := nameSpace{} 159 // ns.Bind("/", OS(`c:\Go`), "/", bindReplace) 160 // ns.Bind("/src/pkg", OS(`d:\Work1`), "/src", bindAfter) 161 // ns.Bind("/src/pkg", OS(`d:\Work2`), "/src", bindAfter) 162 // 163 // A particular mount point entry is a triple (old, fs, new), meaning that to 164 // operate on a path beginning with old, replace that prefix (old) with new 165 // and then pass that path to the FileSystem implementation fs. 166 // 167 // Given this name space, a ReadDir of /src/pkg/code will check each prefix 168 // of the path for a mount point (first /src/pkg/code, then /src/pkg, then /src, 169 // then /), stopping when it finds one. For the above example, /src/pkg/code 170 // will find the mount point at /src/pkg: 171 // 172 // {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"}, 173 // {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"}, 174 // {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"}, 175 // 176 // ReadDir will when execute these three calls and merge the results: 177 // 178 // OS(`c:\Go`).ReadDir("/src/pkg/code") 179 // OS(`d:\Work1').ReadDir("/src/code") 180 // OS(`d:\Work2').ReadDir("/src/code") 181 // 182 // Note that the "/src/pkg" in "/src/pkg/code" has been replaced by 183 // just "/src" in the final two calls. 184 // 185 // OS is itself an implementation of a file system: it implements 186 // OS(`c:\Go`).ReadDir("/src/pkg/code") as ioutil.ReadDir(`c:\Go\src\pkg\code`). 187 // 188 // Because the new path is evaluated by fs (here OS(root)), another way 189 // to read the mount table is to mentally combine fs+new, so that this table: 190 // 191 // {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"}, 192 // {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"}, 193 // {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"}, 194 // 195 // reads as: 196 // 197 // "/src/pkg" -> c:\Go\src\pkg 198 // "/src/pkg" -> d:\Work1\src 199 // "/src/pkg" -> d:\Work2\src 200 // 201 // An invariant (a redundancy) of the name space representation is that 202 // ns[mtpt][i].old is always equal to mtpt (in the example, ns["/src/pkg"]'s 203 // mount table entries always have old == "/src/pkg"). The 'old' field is 204 // useful to callers, because they receive just a []mountedFS and not any 205 // other indication of which mount point was found. 206 // 207 type nameSpace map[string][]mountedFS 208 209 // A mountedFS handles requests for path by replacing 210 // a prefix 'old' with 'new' and then calling the fs methods. 211 type mountedFS struct { 212 old string 213 fs FileSystem 214 new string 215 } 216 217 // translate translates path for use in m, replacing old with new. 218 // 219 // mountedFS{"/src/pkg", fs, "/src"}.translate("/src/pkg/code") == "/src/code". 220 func (m mountedFS) translate(path string) string { 221 path = pathpkg.Clean("/" + path) 222 if !hasPathPrefix(path, m.old) { 223 panic("translate " + path + " but old=" + m.old) 224 } 225 return pathpkg.Join(m.new, path[len(m.old):]) 226 } 227 228 func (nameSpace) String() string { 229 return "ns" 230 } 231 232 // Fprint writes a text representation of the name space to w. 233 func (ns nameSpace) Fprint(w io.Writer) { 234 fmt.Fprint(w, "name space {\n") 235 var all []string 236 for mtpt := range ns { 237 all = append(all, mtpt) 238 } 239 sort.Strings(all) 240 for _, mtpt := range all { 241 fmt.Fprintf(w, "\t%s:\n", mtpt) 242 for _, m := range ns[mtpt] { 243 fmt.Fprintf(w, "\t\t%s %s\n", m.fs, m.new) 244 } 245 } 246 fmt.Fprint(w, "}\n") 247 } 248 249 // clean returns a cleaned, rooted path for evaluation. 250 // It canonicalizes the path so that we can use string operations 251 // to analyze it. 252 func (nameSpace) clean(path string) string { 253 return pathpkg.Clean("/" + path) 254 } 255 256 // Bind causes references to old to redirect to the path new in newfs. 257 // If mode is bindReplace, old redirections are discarded. 258 // If mode is bindBefore, this redirection takes priority over existing ones, 259 // but earlier ones are still consulted for paths that do not exist in newfs. 260 // If mode is bindAfter, this redirection happens only after existing ones 261 // have been tried and failed. 262 263 const ( 264 bindReplace = iota 265 bindBefore 266 bindAfter 267 ) 268 269 func (ns nameSpace) Bind(old string, newfs FileSystem, new string, mode int) { 270 old = ns.clean(old) 271 new = ns.clean(new) 272 m := mountedFS{old, newfs, new} 273 var mtpt []mountedFS 274 switch mode { 275 case bindReplace: 276 mtpt = append(mtpt, m) 277 case bindAfter: 278 mtpt = append(mtpt, ns.resolve(old)...) 279 mtpt = append(mtpt, m) 280 case bindBefore: 281 mtpt = append(mtpt, m) 282 mtpt = append(mtpt, ns.resolve(old)...) 283 } 284 285 // Extend m.old, m.new in inherited mount point entries. 286 for i := range mtpt { 287 m := &mtpt[i] 288 if m.old != old { 289 if !hasPathPrefix(old, m.old) { 290 // This should not happen. If it does, panic so 291 // that we can see the call trace that led to it. 292 panic(fmt.Sprintf("invalid Bind: old=%q m={%q, %s, %q}", old, m.old, m.fs.String(), m.new)) 293 } 294 suffix := old[len(m.old):] 295 m.old = pathpkg.Join(m.old, suffix) 296 m.new = pathpkg.Join(m.new, suffix) 297 } 298 } 299 300 ns[old] = mtpt 301 } 302 303 // resolve resolves a path to the list of mountedFS to use for path. 304 func (ns nameSpace) resolve(path string) []mountedFS { 305 path = ns.clean(path) 306 for { 307 if m := ns[path]; m != nil { 308 if debugNS { 309 fmt.Printf("resolve %s: %v\n", path, m) 310 } 311 return m 312 } 313 if path == "/" { 314 break 315 } 316 path = pathpkg.Dir(path) 317 } 318 return nil 319 } 320 321 // Open implements the FileSystem Open method. 322 func (ns nameSpace) Open(path string) (readSeekCloser, error) { 323 var err error 324 for _, m := range ns.resolve(path) { 325 if debugNS { 326 fmt.Printf("tx %s: %v\n", path, m.translate(path)) 327 } 328 r, err1 := m.fs.Open(m.translate(path)) 329 if err1 == nil { 330 return r, nil 331 } 332 if err == nil { 333 err = err1 334 } 335 } 336 if err == nil { 337 err = &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist} 338 } 339 return nil, err 340 } 341 342 // stat implements the FileSystem Stat and Lstat methods. 343 func (ns nameSpace) stat(path string, f func(FileSystem, string) (os.FileInfo, error)) (os.FileInfo, error) { 344 var err error 345 for _, m := range ns.resolve(path) { 346 fi, err1 := f(m.fs, m.translate(path)) 347 if err1 == nil { 348 return fi, nil 349 } 350 if err == nil { 351 err = err1 352 } 353 } 354 if err == nil { 355 err = &os.PathError{Op: "stat", Path: path, Err: os.ErrNotExist} 356 } 357 return nil, err 358 } 359 360 func (ns nameSpace) Stat(path string) (os.FileInfo, error) { 361 return ns.stat(path, FileSystem.Stat) 362 } 363 364 func (ns nameSpace) Lstat(path string) (os.FileInfo, error) { 365 return ns.stat(path, FileSystem.Lstat) 366 } 367 368 // dirInfo is a trivial implementation of os.FileInfo for a directory. 369 type dirInfo string 370 371 func (d dirInfo) Name() string { return string(d) } 372 func (d dirInfo) Size() int64 { return 0 } 373 func (d dirInfo) Mode() os.FileMode { return os.ModeDir | 0555 } 374 func (d dirInfo) ModTime() time.Time { return startTime } 375 func (d dirInfo) IsDir() bool { return true } 376 func (d dirInfo) Sys() interface{} { return nil } 377 378 var startTime = time.Now() 379 380 // ReadDir implements the FileSystem ReadDir method. It's where most of the magic is. 381 // (The rest is in resolve.) 382 // 383 // Logically, ReadDir must return the union of all the directories that are named 384 // by path. In order to avoid misinterpreting Go packages, of all the directories 385 // that contain Go source code, we only include the files from the first, 386 // but we include subdirectories from all. 387 // 388 // ReadDir must also return directory entries needed to reach mount points. 389 // If the name space looks like the example in the type nameSpace comment, 390 // but c:\Go does not have a src/pkg subdirectory, we still want to be able 391 // to find that subdirectory, because we've mounted d:\Work1 and d:\Work2 392 // there. So if we don't see "src" in the directory listing for c:\Go, we add an 393 // entry for it before returning. 394 // 395 func (ns nameSpace) ReadDir(path string) ([]os.FileInfo, error) { 396 path = ns.clean(path) 397 398 var ( 399 haveGo = false 400 haveName = map[string]bool{} 401 all []os.FileInfo 402 err error 403 first []os.FileInfo 404 ) 405 406 for _, m := range ns.resolve(path) { 407 dir, err1 := m.fs.ReadDir(m.translate(path)) 408 if err1 != nil { 409 if err == nil { 410 err = err1 411 } 412 continue 413 } 414 415 if dir == nil { 416 dir = []os.FileInfo{} 417 } 418 419 if first == nil { 420 first = dir 421 } 422 423 // If we don't yet have Go files in 'all' and this directory 424 // has some, add all the files from this directory. 425 // Otherwise, only add subdirectories. 426 useFiles := false 427 if !haveGo { 428 for _, d := range dir { 429 if strings.HasSuffix(d.Name(), ".go") { 430 useFiles = true 431 haveGo = true 432 break 433 } 434 } 435 } 436 437 for _, d := range dir { 438 name := d.Name() 439 if (d.IsDir() || useFiles) && !haveName[name] { 440 haveName[name] = true 441 all = append(all, d) 442 } 443 } 444 } 445 446 // We didn't find any directories containing Go files. 447 // If some directory returned successfully, use that. 448 if !haveGo { 449 for _, d := range first { 450 if !haveName[d.Name()] { 451 haveName[d.Name()] = true 452 all = append(all, d) 453 } 454 } 455 } 456 457 // Built union. Add any missing directories needed to reach mount points. 458 for old := range ns { 459 if hasPathPrefix(old, path) && old != path { 460 // Find next element after path in old. 461 elem := old[len(path):] 462 elem = strings.TrimPrefix(elem, "/") 463 if i := strings.Index(elem, "/"); i >= 0 { 464 elem = elem[:i] 465 } 466 if !haveName[elem] { 467 haveName[elem] = true 468 all = append(all, dirInfo(elem)) 469 } 470 } 471 } 472 473 if len(all) == 0 { 474 return nil, err 475 } 476 477 sort.Sort(byName(all)) 478 return all, nil 479 } 480 481 // byName implements sort.Interface. 482 type byName []os.FileInfo 483 484 func (f byName) Len() int { return len(f) } 485 func (f byName) Less(i, j int) bool { return f[i].Name() < f[j].Name() } 486 func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] } 487 488 // An httpFS implements http.FileSystem using a FileSystem. 489 type httpFS struct { 490 fs FileSystem 491 } 492 493 func (h *httpFS) Open(name string) (http.File, error) { 494 fi, err := h.fs.Stat(name) 495 if err != nil { 496 return nil, err 497 } 498 if fi.IsDir() { 499 return &httpDir{h.fs, name, nil}, nil 500 } 501 f, err := h.fs.Open(name) 502 if err != nil { 503 return nil, err 504 } 505 return &httpFile{h.fs, f, name}, nil 506 } 507 508 // httpDir implements http.File for a directory in a FileSystem. 509 type httpDir struct { 510 fs FileSystem 511 name string 512 pending []os.FileInfo 513 } 514 515 func (h *httpDir) Close() error { return nil } 516 func (h *httpDir) Stat() (os.FileInfo, error) { return h.fs.Stat(h.name) } 517 func (h *httpDir) Read([]byte) (int, error) { 518 return 0, fmt.Errorf("cannot Read from directory %s", h.name) 519 } 520 521 func (h *httpDir) Seek(offset int64, whence int) (int64, error) { 522 if offset == 0 && whence == 0 { 523 h.pending = nil 524 return 0, nil 525 } 526 return 0, fmt.Errorf("unsupported Seek in directory %s", h.name) 527 } 528 529 func (h *httpDir) Readdir(count int) ([]os.FileInfo, error) { 530 if h.pending == nil { 531 d, err := h.fs.ReadDir(h.name) 532 if err != nil { 533 return nil, err 534 } 535 if d == nil { 536 d = []os.FileInfo{} // not nil 537 } 538 h.pending = d 539 } 540 541 if len(h.pending) == 0 && count > 0 { 542 return nil, io.EOF 543 } 544 if count <= 0 || count > len(h.pending) { 545 count = len(h.pending) 546 } 547 d := h.pending[:count] 548 h.pending = h.pending[count:] 549 return d, nil 550 } 551 552 // httpFile implements http.File for a file (not directory) in a FileSystem. 553 type httpFile struct { 554 fs FileSystem 555 readSeekCloser 556 name string 557 } 558 559 func (h *httpFile) Stat() (os.FileInfo, error) { return h.fs.Stat(h.name) } 560 func (h *httpFile) Readdir(int) ([]os.FileInfo, error) { 561 return nil, fmt.Errorf("cannot Readdir from file %s", h.name) 562 }