summaryrefslogtreecommitdiff
path: root/src/cmd/godoc/filesystem.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/godoc/filesystem.go')
-rw-r--r--src/cmd/godoc/filesystem.go522
1 files changed, 508 insertions, 14 deletions
diff --git a/src/cmd/godoc/filesystem.go b/src/cmd/godoc/filesystem.go
index 4e48c9e68..09d7b2463 100644
--- a/src/cmd/godoc/filesystem.go
+++ b/src/cmd/godoc/filesystem.go
@@ -12,16 +12,56 @@ import (
"fmt"
"io"
"io/ioutil"
+ "net/http"
"os"
+ pathpkg "path"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
)
+// fs is the file system that godoc reads from and serves.
+// It is a virtual file system that operates on slash-separated paths,
+// and its root corresponds to the Go distribution root: /src/pkg
+// holds the source tree, and so on. This means that the URLs served by
+// the godoc server are the same as the paths in the virtual file
+// system, which helps keep things simple.
+//
+// New file trees - implementations of FileSystem - can be added to
+// the virtual file system using nameSpace's Bind method.
+// The usual setup is to bind OS(runtime.GOROOT) to the root
+// of the name space and then bind any GOPATH/src directories
+// on top of /src/pkg, so that all sources are in /src/pkg.
+//
+// For more about name spaces, see the nameSpace type's
+// documentation below.
+//
+// The use of this virtual file system means that most code processing
+// paths can assume they are slash-separated and should be using
+// package path (often imported as pathpkg) to manipulate them,
+// even on Windows.
+//
+var fs = nameSpace{} // the underlying file system for godoc
+
+// Setting debugNS = true will enable debugging prints about
+// name space translations.
+const debugNS = false
+
// The FileSystem interface specifies the methods godoc is using
// to access the file system for which it serves documentation.
type FileSystem interface {
- Open(path string) (io.ReadCloser, error)
+ Open(path string) (readSeekCloser, error)
Lstat(path string) (os.FileInfo, error)
Stat(path string) (os.FileInfo, error)
ReadDir(path string) ([]os.FileInfo, error)
+ String() string
+}
+
+type readSeekCloser interface {
+ io.Reader
+ io.Seeker
+ io.Closer
}
// ReadFile reads the file named by path from fs and returns the contents.
@@ -34,16 +74,31 @@ func ReadFile(fs FileSystem, path string) ([]byte, error) {
return ioutil.ReadAll(rc)
}
-// ----------------------------------------------------------------------------
-// OS-specific FileSystem implementation
+// OS returns an implementation of FileSystem reading from the
+// tree rooted at root. Recording a root is convenient everywhere
+// but necessary on Windows, because the slash-separated path
+// passed to Open has no way to specify a drive letter. Using a root
+// lets code refer to OS(`c:\`), OS(`d:\`) and so on.
+func OS(root string) FileSystem {
+ return osFS(root)
+}
+
+type osFS string
-var OS FileSystem = osFS{}
+func (root osFS) String() string { return "os(" + string(root) + ")" }
-// osFS is the OS-specific implementation of FileSystem
-type osFS struct{}
+func (root osFS) resolve(path string) string {
+ // Clean the path so that it cannot possibly begin with ../.
+ // If it did, the result of filepath.Join would be outside the
+ // tree rooted at root. We probably won't ever see a path
+ // with .. in it, but be safe anyway.
+ path = pathpkg.Clean("/" + path)
-func (osFS) Open(path string) (io.ReadCloser, error) {
- f, err := os.Open(path)
+ return filepath.Join(string(root), path)
+}
+
+func (root osFS) Open(path string) (readSeekCloser, error) {
+ f, err := os.Open(root.resolve(path))
if err != nil {
return nil, err
}
@@ -57,14 +112,453 @@ func (osFS) Open(path string) (io.ReadCloser, error) {
return f, nil
}
-func (osFS) Lstat(path string) (os.FileInfo, error) {
- return os.Lstat(path)
+func (root osFS) Lstat(path string) (os.FileInfo, error) {
+ return os.Lstat(root.resolve(path))
+}
+
+func (root osFS) Stat(path string) (os.FileInfo, error) {
+ return os.Stat(root.resolve(path))
+}
+
+func (root osFS) ReadDir(path string) ([]os.FileInfo, error) {
+ return ioutil.ReadDir(root.resolve(path)) // is sorted
+}
+
+// hasPathPrefix returns true if x == y or x == y + "/" + more
+func hasPathPrefix(x, y string) bool {
+ return x == y || strings.HasPrefix(x, y) && (strings.HasSuffix(y, "/") || strings.HasPrefix(x[len(y):], "/"))
+}
+
+// A nameSpace is a file system made up of other file systems
+// mounted at specific locations in the name space.
+//
+// The representation is a map from mount point locations
+// to the list of file systems mounted at that location. A traditional
+// Unix mount table would use a single file system per mount point,
+// but we want to be able to mount multiple file systems on a single
+// mount point and have the system behave as if the union of those
+// file systems were present at the mount point.
+// For example, if the OS file system has a Go installation in
+// c:\Go and additional Go path trees in d:\Work1 and d:\Work2, then
+// this name space creates the view we want for the godoc server:
+//
+// nameSpace{
+// "/": {
+// {old: "/", fs: OS(`c:\Go`), new: "/"},
+// },
+// "/src/pkg": {
+// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
+// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
+// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
+// },
+// }
+//
+// This is created by executing:
+//
+// ns := nameSpace{}
+// ns.Bind("/", OS(`c:\Go`), "/", bindReplace)
+// ns.Bind("/src/pkg", OS(`d:\Work1`), "/src", bindAfter)
+// ns.Bind("/src/pkg", OS(`d:\Work2`), "/src", bindAfter)
+//
+// A particular mount point entry is a triple (old, fs, new), meaning that to
+// operate on a path beginning with old, replace that prefix (old) with new
+// and then pass that path to the FileSystem implementation fs.
+//
+// Given this name space, a ReadDir of /src/pkg/code will check each prefix
+// of the path for a mount point (first /src/pkg/code, then /src/pkg, then /src,
+// then /), stopping when it finds one. For the above example, /src/pkg/code
+// will find the mount point at /src/pkg:
+//
+// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
+// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
+// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
+//
+// ReadDir will when execute these three calls and merge the results:
+//
+// OS(`c:\Go`).ReadDir("/src/pkg/code")
+// OS(`d:\Work1').ReadDir("/src/code")
+// OS(`d:\Work2').ReadDir("/src/code")
+//
+// Note that the "/src/pkg" in "/src/pkg/code" has been replaced by
+// just "/src" in the final two calls.
+//
+// OS is itself an implementation of a file system: it implements
+// OS(`c:\Go`).ReadDir("/src/pkg/code") as ioutil.ReadDir(`c:\Go\src\pkg\code`).
+//
+// Because the new path is evaluated by fs (here OS(root)), another way
+// to read the mount table is to mentally combine fs+new, so that this table:
+//
+// {old: "/src/pkg", fs: OS(`c:\Go`), new: "/src/pkg"},
+// {old: "/src/pkg", fs: OS(`d:\Work1`), new: "/src"},
+// {old: "/src/pkg", fs: OS(`d:\Work2`), new: "/src"},
+//
+// reads as:
+//
+// "/src/pkg" -> c:\Go\src\pkg
+// "/src/pkg" -> d:\Work1\src
+// "/src/pkg" -> d:\Work2\src
+//
+// An invariant (a redundancy) of the name space representation is that
+// ns[mtpt][i].old is always equal to mtpt (in the example, ns["/src/pkg"]'s
+// mount table entries always have old == "/src/pkg"). The 'old' field is
+// useful to callers, because they receive just a []mountedFS and not any
+// other indication of which mount point was found.
+//
+type nameSpace map[string][]mountedFS
+
+// A mountedFS handles requests for path by replacing
+// a prefix 'old' with 'new' and then calling the fs methods.
+type mountedFS struct {
+ old string
+ fs FileSystem
+ new string
+}
+
+// translate translates path for use in m, replacing old with new.
+//
+// mountedFS{"/src/pkg", fs, "/src"}.translate("/src/pkg/code") == "/src/code".
+func (m mountedFS) translate(path string) string {
+ path = pathpkg.Clean("/" + path)
+ if !hasPathPrefix(path, m.old) {
+ panic("translate " + path + " but old=" + m.old)
+ }
+ return pathpkg.Join(m.new, path[len(m.old):])
+}
+
+func (nameSpace) String() string {
+ return "ns"
+}
+
+// Fprint writes a text representation of the name space to w.
+func (ns nameSpace) Fprint(w io.Writer) {
+ fmt.Fprint(w, "name space {\n")
+ var all []string
+ for mtpt := range ns {
+ all = append(all, mtpt)
+ }
+ sort.Strings(all)
+ for _, mtpt := range all {
+ fmt.Fprintf(w, "\t%s:\n", mtpt)
+ for _, m := range ns[mtpt] {
+ fmt.Fprintf(w, "\t\t%s %s\n", m.fs, m.new)
+ }
+ }
+ fmt.Fprint(w, "}\n")
+}
+
+// clean returns a cleaned, rooted path for evaluation.
+// It canonicalizes the path so that we can use string operations
+// to analyze it.
+func (nameSpace) clean(path string) string {
+ return pathpkg.Clean("/" + path)
+}
+
+// Bind causes references to old to redirect to the path new in newfs.
+// If mode is bindReplace, old redirections are discarded.
+// If mode is bindBefore, this redirection takes priority over existing ones,
+// but earlier ones are still consulted for paths that do not exist in newfs.
+// If mode is bindAfter, this redirection happens only after existing ones
+// have been tried and failed.
+
+const (
+ bindReplace = iota
+ bindBefore
+ bindAfter
+)
+
+func (ns nameSpace) Bind(old string, newfs FileSystem, new string, mode int) {
+ old = ns.clean(old)
+ new = ns.clean(new)
+ m := mountedFS{old, newfs, new}
+ var mtpt []mountedFS
+ switch mode {
+ case bindReplace:
+ mtpt = append(mtpt, m)
+ case bindAfter:
+ mtpt = append(mtpt, ns.resolve(old)...)
+ mtpt = append(mtpt, m)
+ case bindBefore:
+ mtpt = append(mtpt, m)
+ mtpt = append(mtpt, ns.resolve(old)...)
+ }
+
+ // Extend m.old, m.new in inherited mount point entries.
+ for i := range mtpt {
+ m := &mtpt[i]
+ if m.old != old {
+ if !hasPathPrefix(old, m.old) {
+ // This should not happen. If it does, panic so
+ // that we can see the call trace that led to it.
+ panic(fmt.Sprintf("invalid Bind: old=%q m={%q, %s, %q}", old, m.old, m.fs.String(), m.new))
+ }
+ suffix := old[len(m.old):]
+ m.old = pathpkg.Join(m.old, suffix)
+ m.new = pathpkg.Join(m.new, suffix)
+ }
+ }
+
+ ns[old] = mtpt
+}
+
+// resolve resolves a path to the list of mountedFS to use for path.
+func (ns nameSpace) resolve(path string) []mountedFS {
+ path = ns.clean(path)
+ for {
+ if m := ns[path]; m != nil {
+ if debugNS {
+ fmt.Printf("resolve %s: %v\n", path, m)
+ }
+ return m
+ }
+ if path == "/" {
+ break
+ }
+ path = pathpkg.Dir(path)
+ }
+ return nil
+}
+
+// Open implements the FileSystem Open method.
+func (ns nameSpace) Open(path string) (readSeekCloser, error) {
+ var err error
+ for _, m := range ns.resolve(path) {
+ if debugNS {
+ fmt.Printf("tx %s: %v\n", path, m.translate(path))
+ }
+ r, err1 := m.fs.Open(m.translate(path))
+ if err1 == nil {
+ return r, nil
+ }
+ if err == nil {
+ err = err1
+ }
+ }
+ if err == nil {
+ err = &os.PathError{Op: "open", Path: path, Err: os.ErrNotExist}
+ }
+ return nil, err
+}
+
+// stat implements the FileSystem Stat and Lstat methods.
+func (ns nameSpace) stat(path string, f func(FileSystem, string) (os.FileInfo, error)) (os.FileInfo, error) {
+ var err error
+ for _, m := range ns.resolve(path) {
+ fi, err1 := f(m.fs, m.translate(path))
+ if err1 == nil {
+ return fi, nil
+ }
+ if err == nil {
+ err = err1
+ }
+ }
+ if err == nil {
+ err = &os.PathError{Op: "stat", Path: path, Err: os.ErrNotExist}
+ }
+ return nil, err
+}
+
+func (ns nameSpace) Stat(path string) (os.FileInfo, error) {
+ return ns.stat(path, FileSystem.Stat)
+}
+
+func (ns nameSpace) Lstat(path string) (os.FileInfo, error) {
+ return ns.stat(path, FileSystem.Lstat)
+}
+
+// dirInfo is a trivial implementation of os.FileInfo for a directory.
+type dirInfo string
+
+func (d dirInfo) Name() string { return string(d) }
+func (d dirInfo) Size() int64 { return 0 }
+func (d dirInfo) Mode() os.FileMode { return os.ModeDir | 0555 }
+func (d dirInfo) ModTime() time.Time { return startTime }
+func (d dirInfo) IsDir() bool { return true }
+func (d dirInfo) Sys() interface{} { return nil }
+
+var startTime = time.Now()
+
+// ReadDir implements the FileSystem ReadDir method. It's where most of the magic is.
+// (The rest is in resolve.)
+//
+// Logically, ReadDir must return the union of all the directories that are named
+// by path. In order to avoid misinterpreting Go packages, of all the directories
+// that contain Go source code, we only include the files from the first,
+// but we include subdirectories from all.
+//
+// ReadDir must also return directory entries needed to reach mount points.
+// If the name space looks like the example in the type nameSpace comment,
+// but c:\Go does not have a src/pkg subdirectory, we still want to be able
+// to find that subdirectory, because we've mounted d:\Work1 and d:\Work2
+// there. So if we don't see "src" in the directory listing for c:\Go, we add an
+// entry for it before returning.
+//
+func (ns nameSpace) ReadDir(path string) ([]os.FileInfo, error) {
+ path = ns.clean(path)
+
+ var (
+ haveGo = false
+ haveName = map[string]bool{}
+ all []os.FileInfo
+ err error
+ first []os.FileInfo
+ )
+
+ for _, m := range ns.resolve(path) {
+ dir, err1 := m.fs.ReadDir(m.translate(path))
+ if err1 != nil {
+ if err == nil {
+ err = err1
+ }
+ continue
+ }
+
+ if dir == nil {
+ dir = []os.FileInfo{}
+ }
+
+ if first == nil {
+ first = dir
+ }
+
+ // If we don't yet have Go files in 'all' and this directory
+ // has some, add all the files from this directory.
+ // Otherwise, only add subdirectories.
+ useFiles := false
+ if !haveGo {
+ for _, d := range dir {
+ if strings.HasSuffix(d.Name(), ".go") {
+ useFiles = true
+ haveGo = true
+ break
+ }
+ }
+ }
+
+ for _, d := range dir {
+ name := d.Name()
+ if (d.IsDir() || useFiles) && !haveName[name] {
+ haveName[name] = true
+ all = append(all, d)
+ }
+ }
+ }
+
+ // We didn't find any directories containing Go files.
+ // If some directory returned successfully, use that.
+ if !haveGo {
+ for _, d := range first {
+ if !haveName[d.Name()] {
+ haveName[d.Name()] = true
+ all = append(all, d)
+ }
+ }
+ }
+
+ // Built union. Add any missing directories needed to reach mount points.
+ for old := range ns {
+ if hasPathPrefix(old, path) && old != path {
+ // Find next element after path in old.
+ elem := old[len(path):]
+ if strings.HasPrefix(elem, "/") {
+ elem = elem[1:]
+ }
+ if i := strings.Index(elem, "/"); i >= 0 {
+ elem = elem[:i]
+ }
+ if !haveName[elem] {
+ haveName[elem] = true
+ all = append(all, dirInfo(elem))
+ }
+ }
+ }
+
+ if len(all) == 0 {
+ return nil, err
+ }
+
+ sort.Sort(byName(all))
+ return all, nil
+}
+
+// byName implements sort.Interface.
+type byName []os.FileInfo
+
+func (f byName) Len() int { return len(f) }
+func (f byName) Less(i, j int) bool { return f[i].Name() < f[j].Name() }
+func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
+
+// An httpFS implements http.FileSystem using a FileSystem.
+type httpFS struct {
+ fs FileSystem
+}
+
+func (h *httpFS) Open(name string) (http.File, error) {
+ fi, err := h.fs.Stat(name)
+ if err != nil {
+ return nil, err
+ }
+ if fi.IsDir() {
+ return &httpDir{h.fs, name, nil}, nil
+ }
+ f, err := h.fs.Open(name)
+ if err != nil {
+ return nil, err
+ }
+ return &httpFile{h.fs, f, name}, nil
+}
+
+// httpDir implements http.File for a directory in a FileSystem.
+type httpDir struct {
+ fs FileSystem
+ name string
+ pending []os.FileInfo
+}
+
+func (h *httpDir) Close() error { return nil }
+func (h *httpDir) Stat() (os.FileInfo, error) { return h.fs.Stat(h.name) }
+func (h *httpDir) Read([]byte) (int, error) {
+ return 0, fmt.Errorf("cannot Read from directory %s", h.name)
+}
+
+func (h *httpDir) Seek(offset int64, whence int) (int64, error) {
+ if offset == 0 && whence == 0 {
+ h.pending = nil
+ return 0, nil
+ }
+ return 0, fmt.Errorf("unsupported Seek in directory %s", h.name)
+}
+
+func (h *httpDir) Readdir(count int) ([]os.FileInfo, error) {
+ if h.pending == nil {
+ d, err := h.fs.ReadDir(h.name)
+ if err != nil {
+ return nil, err
+ }
+ if d == nil {
+ d = []os.FileInfo{} // not nil
+ }
+ h.pending = d
+ }
+
+ if len(h.pending) == 0 && count > 0 {
+ return nil, io.EOF
+ }
+ if count <= 0 || count > len(h.pending) {
+ count = len(h.pending)
+ }
+ d := h.pending[:count]
+ h.pending = h.pending[count:]
+ return d, nil
}
-func (osFS) Stat(path string) (os.FileInfo, error) {
- return os.Stat(path)
+// httpFile implements http.File for a file (not directory) in a FileSystem.
+type httpFile struct {
+ fs FileSystem
+ readSeekCloser
+ name string
}
-func (osFS) ReadDir(path string) ([]os.FileInfo, error) {
- return ioutil.ReadDir(path) // is sorted
+func (h *httpFile) Stat() (os.FileInfo, error) { return h.fs.Stat(h.name) }
+func (h *httpFile) Readdir(int) ([]os.FileInfo, error) {
+ return nil, fmt.Errorf("cannot Readdir from file %s", h.name)
}