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  }