diff --git a/fs/fs.go b/fs/fs.go index 0cd95434..345aa002 100644 --- a/fs/fs.go +++ b/fs/fs.go @@ -25,6 +25,7 @@ import ( "net/http" "os" "path" + "sort" "strings" "time" ) @@ -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 @@ -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} @@ -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 } @@ -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) } 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{} } @@ -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. @@ -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] + 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 } diff --git a/fs/fs_test.go b/fs/fs_test.go index 7bea8349..36148db3 100644 --- a/fs/fs_test.go +++ b/fs/fs_test.go @@ -16,10 +16,13 @@ package fs import ( "archive/zip" "bytes" + "io" "io/ioutil" "os" + "path" "path/filepath" "reflect" + "sort" "strings" "sync" "testing" @@ -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 @@ -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) { @@ -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 { @@ -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) { diff --git a/testdata/deep/a b/testdata/deep/a new file mode 100644 index 00000000..e69de29b diff --git a/testdata/deep/aa/bb/c b/testdata/deep/aa/bb/c new file mode 100644 index 00000000..e69de29b diff --git a/testdata/readdir/aa b/testdata/readdir/aa new file mode 100644 index 00000000..e69de29b diff --git a/testdata/readdir/bb b/testdata/readdir/bb new file mode 100644 index 00000000..e69de29b diff --git a/testdata/readdir/cc b/testdata/readdir/cc new file mode 100644 index 00000000..e69de29b