Skip to content

Commit

Permalink
improve the performance of fs.Open() by unzipping in fs.New() and sim…
Browse files Browse the repository at this point in the history
…plifying http.File wrapper (rakyll#23)
  • Loading branch information
matthewdale authored and Jaana B. Dogan committed Apr 10, 2017
1 parent a2b6c35 commit 89fe345
Showing 1 changed file with 64 additions and 64 deletions.
128 changes: 64 additions & 64 deletions fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,23 @@ import (
"archive/zip"
"bytes"
"errors"
"io"
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
"sync"
)

var zipData string

// file holds unzipped read-only file contents and file metadata.
type file struct {
os.FileInfo
data []byte
}

type statikFS struct {
files map[string]*zip.File
files map[string]file
}

// Register registers zip contents data, later used to initialize
Expand All @@ -40,107 +45,102 @@ func Register(data string) {
}

// New creates a new file system with the registered zip contents data.
// It unzips all files and stores them in an in-memory map.
func New() (http.FileSystem, error) {
if zipData == "" {
return nil, errors.New("statik/fs: No zip data registered.")
return nil, errors.New("statik/fs: no zip data registered")
}
zipReader, err := zip.NewReader(strings.NewReader(zipData), int64(len(zipData)))
if err != nil {
return nil, err
}
files := make(map[string]*zip.File)
for _, file := range zipReader.File {
files["/"+file.Name] = file
files := make(map[string]file)
for _, zipFile := range zipReader.File {
unzipped, err := unzip(zipFile)
if err != nil {
return nil, fmt.Errorf("statik/fs: error unzipping file %q: %s", zipFile.Name, err)
}
files["/"+zipFile.Name] = file{
FileInfo: zipFile.FileInfo(),
data: unzipped,
}
}
return &statikFS{files: files}, nil
}

// Opens a file, unzip the contents and initializes
// readers. Returns os.ErrNotExists if file is not
// found in the archive.
func unzip(zf *zip.File) ([]byte, error) {
rc, err := zf.Open()
if err != nil {
return nil, err
}
defer rc.Close()
return ioutil.ReadAll(rc)
}

// Open returns a file matching the given file name, or os.ErrNotExists if
// no file matching the given file name is found in the archive.
// If a directory is requested, Open returns the file named "index.html"
// in the requested directory, if that file exists.
func (fs *statikFS) Open(name string) (http.File, error) {
name = strings.Replace(name, "//", "/", -1)
f, ok := fs.files[name]

if ok {
return newHTTPFile(f, false), nil
}
// The file doesn't match, but maybe it's a directory,
// thus we should look for index.html
indexName := strings.Replace(name+"/index.html", "//", "/", -1)
f, ok = fs.files[indexName]
if !ok {
indexName := strings.Replace(name+"/index.html", "//", "/", -1)
f, ok = fs.files[indexName]

if !ok {
return nil, os.ErrNotExist
}

return newFile(f, true)
return nil, os.ErrNotExist
}
return newFile(f, false)
return newHTTPFile(f, true), nil
}

var nopCloser = ioutil.NopCloser(nil)

func newFile(zf *zip.File, isDir bool) (*file, error) {
rc, err := zf.Open()
if err != nil {
return nil, err
}
defer rc.Close()
all, err := ioutil.ReadAll(rc)
if err != nil {
return nil, err
func newHTTPFile(file file, isDir bool) *httpFile {
return &httpFile{
file: file,
reader: bytes.NewReader(file.data),
isDir: isDir,
}
return &file{
FileInfo: zf.FileInfo(),
data: all,
readerAt: bytes.NewReader(all),
Closer: nopCloser,
isDir: isDir,
}, nil
}

// Represents an HTTP file, acts as a bridge between
// zip.File and http.File.
type file struct {
os.FileInfo
io.Closer

data []byte // non-nil if regular file
reader *io.SectionReader
readerAt io.ReaderAt // over data
isDir bool
// httpFile represents an HTTP file and acts as a bridge
// between file and http.File.
type httpFile struct {
file

once sync.Once
reader *bytes.Reader
isDir bool
}

func (f *file) newReader() {
f.reader = io.NewSectionReader(f.readerAt, 0, f.FileInfo.Size())
}

// Reads bytes into p, returns the number of read bytes.
func (f *file) Read(p []byte) (n int, err error) {
f.once.Do(f.newReader)
// Read reads bytes into p, returns the number of read bytes.
func (f *httpFile) Read(p []byte) (n int, err error) {
return f.reader.Read(p)
}

// Seeks to the offset.
func (f *file) Seek(offset int64, whence int) (ret int64, err error) {
f.once.Do(f.newReader)
// Seek seeks to the offset.
func (f *httpFile) Seek(offset int64, whence int) (ret int64, err error) {
return f.reader.Seek(offset, whence)
}

// Stats the file.
func (f *file) Stat() (os.FileInfo, error) {
// Stat stats the file.
func (f *httpFile) Stat() (os.FileInfo, error) {
return f, nil
}

// IsDir returns true if the file location represents a directory.
func (f *file) IsDir() bool {
func (f *httpFile) IsDir() bool {
return f.isDir
}

// Returns an empty slice of files, directory
// Readdir returns an empty slice of files, directory
// listing is disabled.
func (f *file) Readdir(count int) ([]os.FileInfo, error) {
func (f *httpFile) Readdir(count int) ([]os.FileInfo, error) {
// directory listing is disabled.
return make([]os.FileInfo, 0), nil
}

func (f *httpFile) Close() error {
return nil
}

0 comments on commit 89fe345

Please sign in to comment.