diff --git a/lib/godoc/dirlist.html b/lib/godoc/dirlist.html
index 3c1e3aae01b..29b4b243578 100644
--- a/lib/godoc/dirlist.html
+++ b/lib/godoc/dirlist.html
@@ -18,11 +18,11 @@
{.repeated section @}
- {Name|html-esc}{@|dir/} |
+ {@|fileInfoName} |
|
- {Size|html-esc} |
+ {@|fileInfoSize} |
|
- {Mtime_ns|time} |
+ {@|fileInfoTime} |
{.end}
diff --git a/src/cmd/godoc/Makefile b/src/cmd/godoc/Makefile
index 69341fa4e63..f40d7170301 100644
--- a/src/cmd/godoc/Makefile
+++ b/src/cmd/godoc/Makefile
@@ -11,6 +11,7 @@ GOFILES=\
filesystem.go\
format.go\
godoc.go\
+ httpzip.go\
index.go\
main.go\
mapping.go\
diff --git a/src/cmd/godoc/filesystem.go b/src/cmd/godoc/filesystem.go
index e9b5fe3c821..a68c085927f 100644
--- a/src/cmd/godoc/filesystem.go
+++ b/src/cmd/godoc/filesystem.go
@@ -19,6 +19,7 @@ import (
type FileInfo interface {
Name() string
Size() int64
+ Mtime_ns() int64
IsRegular() bool
IsDirectory() bool
}
@@ -54,6 +55,10 @@ func (fi osFI) Size() int64 {
return fi.FileInfo.Size
}
+func (fi osFI) Mtime_ns() int64 {
+ return fi.FileInfo.Mtime_ns
+}
+
// osFS is the OS-specific implementation of FileSystem
type osFS struct{}
diff --git a/src/cmd/godoc/godoc.go b/src/cmd/godoc/godoc.go
index 67441f304ff..03ac1b98b71 100644
--- a/src/cmd/godoc/godoc.go
+++ b/src/cmd/godoc/godoc.go
@@ -67,11 +67,12 @@ var (
maxResults = flag.Int("maxresults", 10000, "maximum number of full text search results shown")
// file system mapping
- fs FileSystem // the underlying file system
- fsMap Mapping // user-defined mapping
- fsTree RWValue // *Directory tree of packages, updated with each sync
- pathFilter RWValue // filter used when building fsMap directory trees
- fsModified RWValue // timestamp of last call to invalidateIndex
+ fs FileSystem // the underlying file system for godoc
+ fsHttp http.FileSystem // the underlying file system for http
+ fsMap Mapping // user-defined mapping
+ fsTree RWValue // *Directory tree of packages, updated with each sync
+ pathFilter RWValue // filter used when building fsMap directory trees
+ fsModified RWValue // timestamp of last call to invalidateIndex
// http handlers
fileServer http.Handler // default file server
@@ -89,7 +90,7 @@ func initHandlers() {
}
fsMap.Init(paths)
- fileServer = http.FileServer(http.Dir(*goroot))
+ fileServer = http.FileServer(fsHttp)
cmdHandler = httpHandler{"/cmd/", filepath.Join(*goroot, "src", "cmd"), false}
pkgHandler = httpHandler{"/pkg/", filepath.Join(*goroot, "src", "pkg"), true}
}
@@ -565,24 +566,34 @@ func paddingFmt(w io.Writer, format string, x ...interface{}) {
}
}
-// Template formatter for "time" format.
-func timeFmt(w io.Writer, format string, x ...interface{}) {
- template.HTMLEscape(w, []byte(time.SecondsToLocalTime(x[0].(int64)/1e9).String()))
-}
-
-// Template formatter for "dir/" format.
-func dirslashFmt(w io.Writer, format string, x ...interface{}) {
- if x[0].(FileInfo).IsDirectory() {
- w.Write([]byte{'/'})
- }
-}
-
// Template formatter for "localname" format.
func localnameFmt(w io.Writer, format string, x ...interface{}) {
_, localname := filepath.Split(x[0].(string))
template.HTMLEscape(w, []byte(localname))
}
+// Template formatter for "fileInfoName" format.
+func fileInfoNameFmt(w io.Writer, format string, x ...interface{}) {
+ fi := x[0].(FileInfo)
+ template.HTMLEscape(w, []byte(fi.Name()))
+ if fi.IsDirectory() {
+ w.Write([]byte{'/'})
+ }
+}
+
+// Template formatter for "fileInfoSize" format.
+func fileInfoSizeFmt(w io.Writer, format string, x ...interface{}) {
+ fmt.Fprintf(w, "%d", x[0].(FileInfo).Size())
+}
+
+// Template formatter for "fileInfoTime" format.
+func fileInfoTimeFmt(w io.Writer, format string, x ...interface{}) {
+ if t := x[0].(FileInfo).Mtime_ns(); t != 0 {
+ template.HTMLEscape(w, []byte(time.SecondsToLocalTime(t/1e9).String()))
+ }
+ // don't print epoch if time is obviously not set
+}
+
// Template formatter for "numlines" format.
func numlinesFmt(w io.Writer, format string, x ...interface{}) {
list := x[0].([]int)
@@ -601,8 +612,9 @@ var fmap = template.FormatterMap{
"infoLine": infoLineFmt,
"infoSnippet": infoSnippetFmt,
"padding": paddingFmt,
- "time": timeFmt,
- "dir/": dirslashFmt,
+ "fileInfoName": fileInfoNameFmt,
+ "fileInfoSize": fileInfoSizeFmt,
+ "fileInfoTime": fileInfoTimeFmt,
"localname": localnameFmt,
"numlines": numlinesFmt,
}
diff --git a/src/cmd/godoc/httpzip.go b/src/cmd/godoc/httpzip.go
new file mode 100644
index 00000000000..97d85694305
--- /dev/null
+++ b/src/cmd/godoc/httpzip.go
@@ -0,0 +1,184 @@
+// 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.
+
+// This file provides an implementation of the http.FileSystem
+// interface based on the contents of a .zip file.
+//
+// Assumptions:
+//
+// - The file paths stored in the zip file must use a slash ('/') as path
+// separator; and they must be relative (i.e., they must not start with
+// a '/' - this is usually the case if the file was created w/o special
+// options).
+// - The zip file system treats the file paths found in the zip internally
+// 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.
+
+// TODO(gri) Should define a commonly used FileSystem API that is the same
+// for http and godoc. Then we only need one zip-file based file
+// system implementation.
+
+package main
+
+import (
+ "archive/zip"
+ "fmt"
+ "http"
+ "io"
+ "os"
+ "path"
+ "sort"
+ "strings"
+)
+
+// We cannot import syscall on app engine.
+// TODO(gri) Once we have a truly abstract FileInfo implementation
+// this won't be needed anymore.
+const (
+ S_IFDIR = 0x4000 // == syscall.S_IFDIR
+ S_IFREG = 0x8000 // == syscall.S_IFREG
+)
+
+// httpZipFile is the zip-file based implementation of http.File
+type httpZipFile struct {
+ info os.FileInfo
+ io.ReadCloser // nil for directory
+ list zipList
+}
+
+func (f *httpZipFile) Close() os.Error {
+ if f.info.IsRegular() {
+ return f.ReadCloser.Close()
+ }
+ f.list = nil
+ return nil
+}
+
+func (f *httpZipFile) Stat() (*os.FileInfo, os.Error) {
+ return &f.info, nil
+}
+
+func (f *httpZipFile) Readdir(count int) ([]os.FileInfo, os.Error) {
+ println("Readdir", f.info.Name)
+ if f.info.IsRegular() {
+ return nil, fmt.Errorf("Readdir called for regular file: %s", f.info.Name)
+ }
+
+ var list []os.FileInfo
+ dirname := zipPath(f.info.Name) + "/"
+ prevname := ""
+ for i, e := range f.list {
+ if count == 0 {
+ f.list = f.list[i:]
+ break
+ }
+ if !strings.HasPrefix(e.Name, dirname) {
+ f.list = nil
+ break // not in the same directory anymore
+ }
+ name := e.Name[len(dirname):] // local name
+ var mode uint32
+ var size, mtime_ns int64
+ if i := strings.IndexRune(name, '/'); i >= 0 {
+ // We infer directories from files in subdirectories.
+ // If we have x/y, return a directory entry for x.
+ name = name[0:i] // keep local directory name only
+ mode = S_IFDIR
+ // no size or mtime_ns for directories
+ } else {
+ mode = S_IFREG
+ size = int64(e.UncompressedSize)
+ mtime_ns = e.Mtime_ns()
+ }
+ // If we have x/y and x/z, don't return two directory entries for x.
+ // TODO(gri): It should be possible to do this more efficiently
+ // by determining the (fs.list) range of local directory entries
+ // (via two binary searches).
+ if name != prevname {
+ list = append(list, os.FileInfo{
+ Name: name,
+ Mode: mode,
+ Size: size,
+ Mtime_ns: mtime_ns,
+ })
+ prevname = name
+ count--
+ }
+ }
+
+ if count >= 0 && len(list) == 0 {
+ return nil, os.EOF
+ }
+
+ return list, nil
+}
+
+func (f *httpZipFile) Read(buf []byte) (int, os.Error) {
+ if f.info.IsRegular() {
+ return f.ReadCloser.Read(buf)
+ }
+ return 0, fmt.Errorf("Read called for directory: %s", f.info.Name)
+}
+
+func (f *httpZipFile) Seek(offset int64, whence int) (int64, os.Error) {
+ return 0, fmt.Errorf("Seek not implemented for zip file entry: %s", f.info.Name)
+}
+
+// httpZipFS is the zip-file based implementation of http.FileSystem
+type httpZipFS struct {
+ *zip.ReadCloser
+ list zipList
+ root string
+}
+
+func (fs *httpZipFS) Open(abspath string) (http.File, os.Error) {
+ name := path.Join(fs.root, abspath)
+ index := fs.list.lookup(name)
+ if index < 0 {
+ return nil, fmt.Errorf("file not found: %s", abspath)
+ }
+
+ if f := fs.list[index]; f.Name == name {
+ // exact match found - must be a file
+ rc, err := f.Open()
+ if err != nil {
+ return nil, err
+ }
+ return &httpZipFile{
+ os.FileInfo{
+ Name: abspath,
+ Mode: S_IFREG,
+ Size: int64(f.UncompressedSize),
+ Mtime_ns: f.Mtime_ns(),
+ },
+ rc,
+ nil,
+ }, nil
+ }
+
+ // not an exact match - must be a directory
+ println("opened directory", abspath, len(fs.list[index:]))
+ return &httpZipFile{
+ os.FileInfo{
+ Name: abspath,
+ Mode: S_IFDIR,
+ // no size or mtime_ns for directories
+ },
+ nil,
+ fs.list[index:],
+ }, nil
+}
+
+func (fs *httpZipFS) Close() os.Error {
+ fs.list = nil
+ return fs.ReadCloser.Close()
+}
+
+func NewHttpZipFS(rc *zip.ReadCloser, root string) http.FileSystem {
+ list := make(zipList, len(rc.File))
+ copy(list, rc.File) // sort a copy of rc.File
+ sort.Sort(list)
+ return &httpZipFS{rc, list, zipPath(root)}
+}
diff --git a/src/cmd/godoc/main.go b/src/cmd/godoc/main.go
index f9847196599..6f7d9d78dc7 100644
--- a/src/cmd/godoc/main.go
+++ b/src/cmd/godoc/main.go
@@ -49,7 +49,7 @@ const defaultAddr = ":6060" // default webserver address
var (
// file system to serve
- // (with e.g.: zip -r go.zip $GOROOT -i \*.go -i \*.html -i \*.css -i \*.js -i \*.txt -i \*.c -i \*.h -i \*.s -i \*.png -i \*.jpg -i \*.sh)
+ // (with e.g.: zip -r go.zip $GOROOT -i \*.go -i \*.html -i \*.css -i \*.js -i \*.txt -i \*.c -i \*.h -i \*.s -i \*.png -i \*.jpg -i \*.sh -i favicon.ico)
zipfile = flag.String("zip", "", "zip file providing the file system to serve; disabled if empty")
// periodic sync
@@ -219,19 +219,6 @@ func main() {
flag.Usage = usage
flag.Parse()
- // Clean goroot: normalize path separator.
- *goroot = filepath.Clean(*goroot)
-
- // Determine file system to use.
- fs = OS
- if *zipfile != "" {
- rc, err := zip.OpenReader(*zipfile)
- if err != nil {
- log.Fatalf("%s: %s\n", *zipfile, err)
- }
- fs = NewZipFS(rc)
- }
-
// Check usage: either server and no args, or command line and args
if (*httpAddr != "") != (flag.NArg() == 0) {
usage()
@@ -241,6 +228,27 @@ func main() {
log.Fatalf("negative tabwidth %d", *tabwidth)
}
+ // Clean goroot: normalize path separator.
+ *goroot = filepath.Clean(*goroot)
+
+ // Determine file system to use.
+ // TODO(gri) - fs and fsHttp should really be the same. Try to unify.
+ // - fsHttp doesn't need to be set up in command-line mode,
+ // same is true for the http handlers in initHandlers.
+ if *zipfile == "" {
+ // use file system of underlying OS
+ fs = OS
+ fsHttp = http.Dir(*goroot)
+ } else {
+ // use file system specified via .zip file
+ rc, err := zip.OpenReader(*zipfile)
+ if err != nil {
+ log.Fatalf("%s: %s\n", *zipfile, err)
+ }
+ fs = NewZipFS(rc)
+ fsHttp = NewHttpZipFS(rc, *goroot)
+ }
+
initHandlers()
readTemplates()
diff --git a/src/cmd/godoc/zip.go b/src/cmd/godoc/zip.go
index b2257998d7c..eac6992387b 100644
--- a/src/cmd/godoc/zip.go
+++ b/src/cmd/godoc/zip.go
@@ -40,12 +40,19 @@ func (fi zipFI) Name() string {
}
func (fi zipFI) Size() int64 {
- if fi.file != nil {
- return int64(fi.file.UncompressedSize)
+ if f := fi.file; f != nil {
+ return int64(f.UncompressedSize)
}
return 0 // directory
}
+func (fi zipFI) Mtime_ns() int64 {
+ if f := fi.file; f != nil {
+ return f.Mtime_ns()
+ }
+ return 0 // directory has no modified time entry
+}
+
func (fi zipFI) IsDirectory() bool {
return fi.file == nil
}