// 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" "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 { path string // absolute path within zip FS without leading '/' 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) { 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 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) 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(name string) (http.File, os.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, os.FileInfo{ Name: name, Mode: S_IFREG, Size: int64(f.UncompressedSize), Mtime_ns: f.Mtime_ns(), }, rc, nil, }, nil } // not an exact match - must be a directory return &httpZipFile{ path, os.FileInfo{ Name: name, 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() } // 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)} }