From 5ff0687cc92e3b8806ec366c7c6e847adb6e03b6 Mon Sep 17 00:00:00 2001 From: Andrew Gerrand Date: Wed, 17 Jul 2013 15:02:27 +1000 Subject: [PATCH] go.tools/godoc: move vfs code to vfs package R=bradfitz CC=golang-dev https://golang.org/cl/11414043 --- cmd/godoc/filesystem.go | 518 +------------------ cmd/godoc/godoc.go | 3 +- cmd/godoc/main.go | 17 +- godoc/vfs/httpfs/httpfs.go | 94 ++++ godoc/vfs/namespace.go | 381 ++++++++++++++ godoc/vfs/os.go | 63 +++ godoc/vfs/vfs.go | 17 +- cmd/godoc/zip.go => godoc/vfs/zipfs/zipfs.go | 7 +- 8 files changed, 571 insertions(+), 529 deletions(-) create mode 100644 godoc/vfs/httpfs/httpfs.go create mode 100644 godoc/vfs/namespace.go create mode 100644 godoc/vfs/os.go rename cmd/godoc/zip.go => godoc/vfs/zipfs/zipfs.go (97%) diff --git a/cmd/godoc/filesystem.go b/cmd/godoc/filesystem.go index 66793801..bc54e5b3 100644 --- a/cmd/godoc/filesystem.go +++ b/cmd/godoc/filesystem.go @@ -9,16 +9,7 @@ package main import ( - "fmt" - "io" "io/ioutil" - "net/http" - "os" - pathpkg "path" - "path/filepath" - "sort" - "strings" - "time" "code.google.com/p/go.tools/godoc/vfs" ) @@ -36,32 +27,18 @@ import ( // 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. +// For more about name spaces, see the NameSpace type's +// documentation in code.google.com/p/go.tools/godoc/vfs. // // 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) (vfs.ReadSeekCloser, error) - Lstat(path string) (os.FileInfo, error) - Stat(path string) (os.FileInfo, error) - ReadDir(path string) ([]os.FileInfo, error) - String() string -} +var fs = vfs.NameSpace{} // the underlying file system for godoc // ReadFile reads the file named by path from fs and returns the contents. -func ReadFile(fs FileSystem, path string) ([]byte, error) { +func ReadFile(fs vfs.FileSystem, path string) ([]byte, error) { rc, err := fs.Open(path) if err != nil { return nil, err @@ -69,490 +46,3 @@ func ReadFile(fs FileSystem, path string) ([]byte, error) { defer rc.Close() return ioutil.ReadAll(rc) } - -// 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 - -func (root osFS) String() string { return "os(" + string(root) + ")" } - -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) - - return filepath.Join(string(root), path) -} - -func (root osFS) Open(path string) (vfs.ReadSeekCloser, error) { - f, err := os.Open(root.resolve(path)) - if err != nil { - return nil, err - } - fi, err := f.Stat() - if err != nil { - return nil, err - } - if fi.IsDir() { - return nil, fmt.Errorf("Open: %s is a directory", path) - } - return f, nil -} - -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) (vfs.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):] - elem = strings.TrimPrefix(elem, "/") - 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 -} - -// httpFile implements http.File for a file (not directory) in a FileSystem. -type httpFile struct { - fs FileSystem - vfs.ReadSeekCloser - name string -} - -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) -} diff --git a/cmd/godoc/godoc.go b/cmd/godoc/godoc.go index 84c8fa8f..1f40111a 100644 --- a/cmd/godoc/godoc.go +++ b/cmd/godoc/godoc.go @@ -34,6 +34,7 @@ import ( "unicode/utf8" "code.google.com/p/go.tools/godoc/util" + "code.google.com/p/go.tools/godoc/vfs/httpfs" ) // ---------------------------------------------------------------------------- @@ -77,7 +78,7 @@ var ( ) func initHandlers() { - fileServer = http.FileServer(&httpFS{fs}) + fileServer = http.FileServer(httpfs.New(fs)) cmdHandler = docServer{"/cmd/", "/src/cmd"} pkgHandler = docServer{"/pkg/", "/src/pkg"} } diff --git a/cmd/godoc/main.go b/cmd/godoc/main.go index 0d1546b1..618a74ea 100644 --- a/cmd/godoc/main.go +++ b/cmd/godoc/main.go @@ -48,6 +48,9 @@ import ( "regexp" "runtime" "strings" + + "code.google.com/p/go.tools/godoc/vfs" + "code.google.com/p/go.tools/godoc/vfs/zipfs" ) const defaultAddr = ":6060" // default webserver address @@ -172,9 +175,9 @@ func main() { // same is true for the http handlers in initHandlers. if *zipfile == "" { // use file system of underlying OS - fs.Bind("/", OS(*goroot), "/", bindReplace) + fs.Bind("/", vfs.OS(*goroot), "/", vfs.BindReplace) if *templateDir != "" { - fs.Bind("/lib/godoc", OS(*templateDir), "/", bindBefore) + fs.Bind("/lib/godoc", vfs.OS(*templateDir), "/", vfs.BindBefore) } } else { // use file system specified via .zip file (path separator must be '/') @@ -183,12 +186,12 @@ func main() { log.Fatalf("%s: %s\n", *zipfile, err) } defer rc.Close() // be nice (e.g., -writeIndex mode) - fs.Bind("/", NewZipFS(rc, *zipfile), *goroot, bindReplace) + fs.Bind("/", zipvfs.New(rc, *zipfile), *goroot, vfs.BindReplace) } // Bind $GOPATH trees into Go root. for _, p := range filepath.SplitList(build.Default.GOPATH) { - fs.Bind("/src/pkg", OS(p), "/src", bindAfter) + fs.Bind("/src/pkg", vfs.OS(p), "/src", vfs.BindAfter) } readTemplates() @@ -339,18 +342,18 @@ func main() { var forceCmd bool var abspath, relpath string if filepath.IsAbs(path) { - fs.Bind(target, OS(path), "/", bindReplace) + fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace) abspath = target } else if build.IsLocalImport(path) { cwd, _ := os.Getwd() // ignore errors path = filepath.Join(cwd, path) - fs.Bind(target, OS(path), "/", bindReplace) + fs.Bind(target, vfs.OS(path), "/", vfs.BindReplace) abspath = target } else if strings.HasPrefix(path, cmdPrefix) { path = strings.TrimPrefix(path, cmdPrefix) forceCmd = true } else if bp, _ := build.Import(path, "", build.FindOnly); bp.Dir != "" && bp.ImportPath != "" { - fs.Bind(target, OS(bp.Dir), "/", bindReplace) + fs.Bind(target, vfs.OS(bp.Dir), "/", vfs.BindReplace) abspath = target relpath = bp.ImportPath } else { diff --git a/godoc/vfs/httpfs/httpfs.go b/godoc/vfs/httpfs/httpfs.go new file mode 100644 index 00000000..51a4fff2 --- /dev/null +++ b/godoc/vfs/httpfs/httpfs.go @@ -0,0 +1,94 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package httpfs implements http.FileSystem using a godoc vfs.FileSystem. +package httpfs + +import ( + "fmt" + "io" + "net/http" + "os" + + "code.google.com/p/go.tools/godoc/vfs" +) + +func New(fs vfs.FileSystem) http.FileSystem { + return &httpFS{fs} +} + +type httpFS struct { + fs vfs.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 vfs.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 +} + +// httpFile implements http.File for a file (not directory) in a FileSystem. +type httpFile struct { + fs vfs.FileSystem + vfs.ReadSeekCloser + name string +} + +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) +} diff --git a/godoc/vfs/namespace.go b/godoc/vfs/namespace.go new file mode 100644 index 00000000..dbba20cb --- /dev/null +++ b/godoc/vfs/namespace.go @@ -0,0 +1,381 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vfs + +import ( + "fmt" + "io" + "os" + pathpkg "path" + "sort" + "strings" + "time" +) + +// Setting debugNS = true will enable debugging prints about +// name space translations. +const debugNS = false + +// 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 +} + +// 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):], "/")) +} + +// 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) +} + +type BindMode int + +const ( + BindReplace BindMode = iota + BindBefore + BindAfter +) + +// 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. +func (ns NameSpace) Bind(old string, newfs FileSystem, new string, mode BindMode) { + 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):] + elem = strings.TrimPrefix(elem, "/") + 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] } diff --git a/godoc/vfs/os.go b/godoc/vfs/os.go new file mode 100644 index 00000000..40636909 --- /dev/null +++ b/godoc/vfs/os.go @@ -0,0 +1,63 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package vfs + +import ( + "fmt" + "io/ioutil" + "os" + pathpkg "path" + "path/filepath" +) + +// 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 + +func (root osFS) String() string { return "os(" + string(root) + ")" } + +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) + + 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 + } + fi, err := f.Stat() + if err != nil { + return nil, err + } + if fi.IsDir() { + return nil, fmt.Errorf("Open: %s is a directory", path) + } + return f, nil +} + +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 +} diff --git a/godoc/vfs/vfs.go b/godoc/vfs/vfs.go index 55ba231b..cfe9a9c1 100644 --- a/godoc/vfs/vfs.go +++ b/godoc/vfs/vfs.go @@ -2,15 +2,26 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package vfs defines virtual filesystem types. +// Package vfs defines types for abstract file system access and provides an +// implementation accessing the file system of the underlying OS. package vfs import ( "io" + "os" ) -// Opener is a minimal virtual filesystem that can only open -// regular files. +// The FileSystem interface specifies the methods godoc is using +// to access the file system for which it serves documentation. +type FileSystem interface { + Opener + Lstat(path string) (os.FileInfo, error) + Stat(path string) (os.FileInfo, error) + ReadDir(path string) ([]os.FileInfo, error) + String() string +} + +// Opener is a minimal virtual filesystem that can only open regular files. type Opener interface { Open(name string) (ReadSeekCloser, error) } diff --git a/cmd/godoc/zip.go b/godoc/vfs/zipfs/zipfs.go similarity index 97% rename from cmd/godoc/zip.go rename to godoc/vfs/zipfs/zipfs.go index 899f6914..521bdfee 100644 --- a/cmd/godoc/zip.go +++ b/godoc/vfs/zipfs/zipfs.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// This file provides an implementation of the FileSystem +// Package zipfs file provides an implementation of the FileSystem // interface based on the contents of a .zip file. // // Assumptions: @@ -15,8 +15,7 @@ // like absolute paths w/o a leading '/'; i.e., the paths are considered // relative to the root of the file system. // - All path arguments to file system methods must be absolute paths. - -package main +package zipvfs import ( "archive/zip" @@ -190,7 +189,7 @@ func (fs *zipFS) ReadDir(abspath string) ([]os.FileInfo, error) { return list, nil } -func NewZipFS(rc *zip.ReadCloser, name string) FileSystem { +func New(rc *zip.ReadCloser, name string) vfs.FileSystem { list := make(zipList, len(rc.File)) copy(list, rc.File) // sort a copy of rc.File sort.Sort(list)