Skip to content
62 changes: 53 additions & 9 deletions fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"net/http"
"os"
"path"
"sort"
"strings"
"time"
)
Expand All @@ -40,6 +41,7 @@ type file struct {

type statikFS struct {
files map[string]file
dirs map[string][]string
}

// Register registers zip contents data, later used to initialize
Expand All @@ -59,7 +61,8 @@ func New() (http.FileSystem, error) {
return nil, err
}
files := make(map[string]file, len(zipReader.File))
fs := &statikFS{files: files}
dirs := make(map[string][]string)
fs := &statikFS{files: files, dirs: dirs}
for _, zipFile := range zipReader.File {
fi := zipFile.FileInfo()
f := file{FileInfo: fi, fs: fs}
Expand All @@ -69,12 +72,26 @@ func New() (http.FileSystem, error) {
}
files["/"+zipFile.Name] = f
}
for fn := range files {
// go up directories recursively in order to care deep directory
for dn := path.Dir(fn); dn != fn; {
if _, ok := files[dn]; !ok {
files[dn] = file{FileInfo: dirInfo{dn}, fs: fs}
} else {
break
}
fn, dn = dn, path.Dir(dn)
}
}
for fn := range files {
dn := path.Dir(fn)
if _, ok := files[dn]; !ok {
files[dn] = file{FileInfo: dirInfo{dn}, fs: fs}
if fn != dn {
fs.dirs[dn] = append(fs.dirs[dn], path.Base(fn))
}
}
for _, s := range fs.dirs {
sort.Strings(s)
}
return fs, nil
}

Expand All @@ -84,7 +101,7 @@ type dirInfo struct {
name string
}

func (di dirInfo) Name() string { return di.name }
func (di dirInfo) Name() string { return path.Base(di.name) }
Comment thread
Songmu marked this conversation as resolved.
func (di dirInfo) Size() int64 { return 0 }
func (di dirInfo) Mode() os.FileMode { return 0755 | os.ModeDir }
func (di dirInfo) ModTime() time.Time { return time.Time{} }
Expand Down Expand Up @@ -126,6 +143,7 @@ type httpFile struct {

reader *bytes.Reader
isDir bool
dirIdx int
}

// Read reads bytes into p, returns the number of read bytes.
Expand Down Expand Up @@ -158,12 +176,38 @@ func (f *httpFile) Readdir(count int) ([]os.FileInfo, error) {
if !f.isDir {
return fis, nil
}
prefix := f.Name()
for fn, f := range f.file.fs.files {
if strings.HasPrefix(fn, prefix) && len(fn) > len(prefix) {
fis = append(fis, f.FileInfo)
}
di, ok := f.FileInfo.(dirInfo)
if !ok {
return nil, fmt.Errorf("failed to read directory: %q", f.Name())
}

// If count is positive, the specified number of files will be returned,
// and if negative, all remaining files will be returned.
// The reading position of which file is returned is held in dirIndex.
fnames := f.file.fs.dirs[di.name]
Comment thread
Songmu marked this conversation as resolved.
flen := len(fnames)

// If dirIdx reaches the end and the count is a positive value,
// an io.EOF error is returned.
// In other cases, no error will be returned even if, for example,
// you specified more counts than the number of remaining files.
start := f.dirIdx
if start >= flen && count > 0 {
return fis, io.EOF
}
var end int
if count < 0 {
end = flen
} else {
end = start + count
}
if end > flen {
end = flen
}
for i := start; i < end; i++ {
fis = append(fis, f.file.fs.files[path.Join(di.name, fnames[i])].FileInfo)
}
f.dirIdx += len(fis)
return fis, nil
}

Expand Down
156 changes: 155 additions & 1 deletion fs/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ package fs
import (
"archive/zip"
"bytes"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"reflect"
"sort"
"strings"
"sync"
"testing"
Expand All @@ -41,6 +44,8 @@ func TestOpen(t *testing.T) {
pixelGifHeader := mustFileHeader("../testdata/image/pixel.gif")
indexHTMLHeader := mustFileHeader("../testdata/index/index.html")
subdirIndexHTMLHeader := mustFileHeader("../testdata/index/sub_dir/index.html")
deepAHTMLHeader := mustFileHeader("../testdata/deep/a")
deepCHTMLHeader := mustFileHeader("../testdata/deep/aa/bb/c")
tests := []struct {
description string
zipData string
Expand Down Expand Up @@ -115,6 +120,43 @@ func TestOpen(t *testing.T) {
},
},
},
{
description: "listed all sub directories in deep directory",
zipData: mustZipTree("../testdata/deep"),
wantFiles: map[string]wantFile{
"/a": {
data: mustReadFile("../testdata/deep/a"),
isDir: false,
modTime: deepAHTMLHeader.ModTime(),
mode: deepAHTMLHeader.Mode(),
name: deepAHTMLHeader.Name,
size: int64(deepAHTMLHeader.UncompressedSize64),
},
"/aa/bb/c": {
data: mustReadFile("../testdata/deep/aa/bb/c"),
isDir: false,
modTime: deepCHTMLHeader.ModTime(),
mode: deepCHTMLHeader.Mode(),
name: deepCHTMLHeader.Name,
size: int64(deepCHTMLHeader.UncompressedSize64),
},
"/": {
isDir: true,
mode: os.ModeDir | 0755,
name: "/",
},
"/aa": {
isDir: true,
mode: os.ModeDir | 0755,
name: "/aa",
},
"/aa/bb": {
isDir: true,
mode: os.ModeDir | 0755,
name: "/aa/bb",
},
},
},
}
for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) {
Expand Down Expand Up @@ -155,7 +197,7 @@ func TestOpen(t *testing.T) {
if got, want := stat.Mode(), wantFile.mode; got != want {
t.Errorf("Mode(%v) = %v; want %v", name, got, want)
}
if got, want := stat.Name(), wantFile.name; got != want {
if got, want := stat.Name(), path.Base(wantFile.name); got != want {
t.Errorf("Name(%v) = %v; want %v", name, got, want)
}
if got, want := stat.Size(), wantFile.size; got != want {
Expand All @@ -166,6 +208,118 @@ func TestOpen(t *testing.T) {
}
}

func TestWalk(t *testing.T) {
Register(mustZipTree("../testdata/deep"))
fs, err := New()
if err != nil {
t.Errorf("New() = %v", err)
return
}
var files []string
err = Walk(fs, "/", func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
files = append(files, path)
return nil
})
if err != nil {
t.Errorf("Walk(fs, /) = %v", err)
return
}
wantDirs := []string{
"/",
"/a",
"/aa",
"/aa/bb",
"/aa/bb/c",
}
sort.Strings(files)
if !reflect.DeepEqual(files, wantDirs) {
t.Errorf("got: %v\nexpect: %v", files, wantDirs)
}
}

func TestHTTPFile_Readdir(t *testing.T) {
Register(mustZipTree("../testdata/readdir"))
fs, err := New()
if err != nil {
t.Errorf("New() = %v", err)
return
}
t.Run("Readdir(-1)", func(t *testing.T) {
dir, err := fs.Open("/")
if err != nil {
t.Errorf("fs.Open(/) = %v", err)
return
}
fis, err := dir.Readdir(-1)
if err != nil {
t.Errorf("dir.Readdir(-1) = %v", err)
return
}
if len(fis) != 3 {
t.Errorf("got: %d, expect: 3", len(fis))
}
})
t.Run("Readdir(>0)", func(t *testing.T) {
dir, err := fs.Open("/")
if err != nil {
t.Errorf("fs.Open(/) = %v", err)
return
}
fis, err := dir.Readdir(1)
if err != nil {
t.Errorf("dir.Readdir(1) = %v", err)
return
}
if len(fis) != 1 {
t.Errorf("got: %d, expect: 1", len(fis))
}
if fis[0].Name() != "aa" {
t.Errorf("got: %s, expect: aa", fis[0].Name())
}
fis, err = dir.Readdir(1)
if err != nil {
t.Errorf("dir.Readdir(1) = %v", err)
return
}
if len(fis) != 1 {
t.Errorf("got: %d, expect: 1", len(fis))
}
if fis[0].Name() != "bb" {
t.Errorf("got: %s, expect: bb", fis[0].Name())
}
fis, err = dir.Readdir(-1) // take rest entries
if err != nil {
t.Errorf("dir.Readdir(1) = %v", err)
return
}
if len(fis) != 1 {
t.Errorf("got: %d, expect: 1", len(fis))
}
if fis[0].Name() != "cc" {
t.Errorf("got: %s, expect: cc", fis[0].Name())
}
fis, err = dir.Readdir(-1)
if err != nil {
t.Errorf("dir.Readdir(1) = %v", err)
return
}
if len(fis) != 0 {
t.Errorf("got: %d, expect: 0", len(fis))
}
fis, err = dir.Readdir(1)
if err != io.EOF {
t.Errorf("error should be io.EOF, but: %s", err)
return
}
if len(fis) != 0 {
t.Errorf("got: %d, expect: 0", len(fis))
}
})
}

// Test that calling Open by many goroutines concurrently continues
// to return the expected result.
func TestOpen_Parallel(t *testing.T) {
Expand Down
Empty file added testdata/deep/a
Empty file.
Empty file added testdata/deep/aa/bb/c
Empty file.
Empty file added testdata/readdir/aa
Empty file.
Empty file added testdata/readdir/bb
Empty file.
Empty file added testdata/readdir/cc
Empty file.