382 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			382 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
| // 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] }
 |