// 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 are considered relative to // the root specified with NewHttpZipFS (even if the paths start with a '/'). // 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" "io" "net/http" "os" "path" "sort" "strings" "time" ) type fileInfo struct { name string mode os.FileMode size int64 mtime time.Time } func (fi *fileInfo) Name() string { return fi.name } func (fi *fileInfo) Mode() os.FileMode { return fi.mode } func (fi *fileInfo) Size() int64 { return fi.size } func (fi *fileInfo) ModTime() time.Time { return fi.mtime } func (fi *fileInfo) IsDir() bool { return fi.mode.IsDir() } func (fi *fileInfo) Sys() interface{} { return nil } // httpZipFile is the zip-file based implementation of http.File type httpZipFile struct { path string // absolute path within zip FS without leading '/' info os.FileInfo io.ReadCloser // nil for directory list zipList } func (f *httpZipFile) Close() error { if !f.info.IsDir() { return f.ReadCloser.Close() } f.list = nil return nil } func (f *httpZipFile) Stat() (os.FileInfo, error) { return f.info, nil } func (f *httpZipFile) Readdir(count int) ([]os.FileInfo, error) { var list []os.FileInfo dirname := f.path + "/" 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 os.FileMode var size int64 var mtime time.Time 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 = os.ModeDir // no size or mtime for directories } else { mode = 0 size = int64(e.UncompressedSize) mtime = e.ModTime() } // 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, &fileInfo{ name, mode, size, mtime, }) prevname = name count-- } } if count >= 0 && len(list) == 0 { return nil, io.EOF } return list, nil } func (f *httpZipFile) Seek(offset int64, whence int) (int64, 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(name string) (http.File, error) { // fs.root does not start with '/'. path := path.Join(fs.root, name) // path is clean index, exact := fs.list.lookup(path) if index < 0 || !strings.HasPrefix(path, fs.root) { // file not found or not under root return nil, fmt.Errorf("file not found: %s", name) } if exact { // exact match found - must be a file f := fs.list[index] rc, err := f.Open() if err != nil { return nil, err } return &httpZipFile{ path, &fileInfo{ name, 0, int64(f.UncompressedSize), f.ModTime(), }, rc, nil, }, nil } // not an exact match - must be a directory return &httpZipFile{ path, &fileInfo{ name, os.ModeDir, 0, // no size for directory time.Time{}, // no mtime for directory }, nil, fs.list[index:], }, nil } func (fs *httpZipFS) Close() error { fs.list = nil return fs.ReadCloser.Close() } // NewHttpZipFS creates a new http.FileSystem based on the contents of // the zip file rc restricted to the directory tree specified by root; // root must be an absolute path. 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)} }