Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add zipfs, an archive/zip-based read-only filesystem #146

Merged
merged 5 commits into from
Apr 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,6 @@ The following is a short list of possible backends we hope someone will
implement:

* SSH
* ZIP
* TAR
* S3

Expand Down
165 changes: 165 additions & 0 deletions zipfs/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package zipfs

import (
"archive/zip"
"io"
"os"
"path/filepath"
"syscall"

"github.com/spf13/afero"
)

type File struct {
fs *Fs
zipfile *zip.File
reader io.ReadCloser
offset int64
isdir, closed bool
buf []byte
}

func (f *File) fillBuffer(offset int64) (err error) {
if f.reader == nil {
if f.reader, err = f.zipfile.Open(); err != nil {
return
}
}
if offset > int64(f.zipfile.UncompressedSize64) {
offset = int64(f.zipfile.UncompressedSize64)
err = io.EOF
}
if len(f.buf) >= int(offset) {
return
}
buf := make([]byte, int(offset)-len(f.buf))
if n, readErr := io.ReadFull(f.reader, buf); n > 0 {
f.buf = append(f.buf, buf[:n]...)
} else if readErr != nil {
err = readErr
}
return
}

func (f *File) Close() (err error) {
f.zipfile = nil
f.closed = true
f.buf = nil
if f.reader != nil {
err = f.reader.Close()
f.reader = nil
}
return
}

func (f *File) Read(p []byte) (n int, err error) {
if f.isdir {
return 0, syscall.EISDIR
}
if f.closed {
return 0, afero.ErrFileClosed
}
err = f.fillBuffer(f.offset + int64(len(p)))
n = copy(p, f.buf[f.offset:])
f.offset += int64(len(p))
return
}

func (f *File) ReadAt(p []byte, off int64) (n int, err error) {
if f.isdir {
return 0, syscall.EISDIR
}
if f.closed {
return 0, afero.ErrFileClosed
}
err = f.fillBuffer(off + int64(len(p)))
n = copy(p, f.buf[int(off):])
return
}

func (f *File) Seek(offset int64, whence int) (int64, error) {
if f.isdir {
return 0, syscall.EISDIR
}
if f.closed {
return 0, afero.ErrFileClosed
}
switch whence {
case os.SEEK_SET:
case os.SEEK_CUR:
offset += f.offset
case os.SEEK_END:
offset += int64(f.zipfile.UncompressedSize64)
hillu marked this conversation as resolved.
Show resolved Hide resolved
default:
return 0, syscall.EINVAL
}
if offset < 0 || offset > int64(f.zipfile.UncompressedSize64) {
return 0, afero.ErrOutOfRange
}
f.offset = offset
return offset, nil
}

func (f *File) Write(p []byte) (n int, err error) { return 0, syscall.EPERM }

func (f *File) WriteAt(p []byte, off int64) (n int, err error) { return 0, syscall.EPERM }

func (f *File) Name() string {
if f.zipfile == nil {
return string(filepath.Separator)
}
return filepath.Join(splitpath(f.zipfile.Name))
}

func (f *File) getDirEntries() (map[string]*zip.File, error) {
if !f.isdir {
return nil, syscall.ENOTDIR
}
name := f.Name()
entries, ok := f.fs.files[name]
if !ok {
return nil, &os.PathError{Op: "readdir", Path: name, Err: syscall.ENOENT}
}
return entries, nil
}

func (f *File) Readdir(count int) (fi []os.FileInfo, err error) {
zipfiles, err := f.getDirEntries()
if err != nil {
return nil, err
}
for _, zipfile := range zipfiles {
fi = append(fi, zipfile.FileInfo())
if count >= 0 && len(fi) >= count {
break
}
}
return
}

func (f *File) Readdirnames(count int) (names []string, err error) {
zipfiles, err := f.getDirEntries()
if err != nil {
return nil, err
}
for filename, _ := range zipfiles {
names = append(names, filename)
if count >= 0 && len(names) >= count {
break
}
}
return
}

func (f *File) Stat() (os.FileInfo, error) {
if f.zipfile == nil {
return &pseudoRoot{}, nil
}
return f.zipfile.FileInfo(), nil
}

func (f *File) Sync() error { return nil }

func (f *File) Truncate(size int64) error { return syscall.EPERM }

func (f *File) WriteString(s string) (ret int, err error) { return 0, syscall.EPERM }
111 changes: 111 additions & 0 deletions zipfs/fs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package zipfs

import (
"archive/zip"
"os"
"path/filepath"
"syscall"
"time"

"github.com/spf13/afero"
)

type Fs struct {
r *zip.Reader
files map[string]map[string]*zip.File
}

func splitpath(name string) (dir, file string) {
0xmichalis marked this conversation as resolved.
Show resolved Hide resolved
name = filepath.ToSlash(name)
if len(name) == 0 || name[0] != '/' {
name = "/" + name
}
name = filepath.Clean(name)
dir, file = filepath.Split(name)
dir = filepath.Clean(dir)
return
}

func New(r *zip.Reader) afero.Fs {
fs := &Fs{r: r, files: make(map[string]map[string]*zip.File)}
for _, file := range r.File {
d, f := splitpath(file.Name)
if _, ok := fs.files[d]; !ok {
fs.files[d] = make(map[string]*zip.File)
}
if _, ok := fs.files[d][f]; !ok {
fs.files[d][f] = file
}
if file.FileInfo().IsDir() {
dirname := filepath.Join(d, f)
if _, ok := fs.files[dirname]; !ok {
fs.files[dirname] = make(map[string]*zip.File)
}
}
}
return fs
}

func (fs *Fs) Create(name string) (afero.File, error) { return nil, syscall.EPERM }

func (fs *Fs) Mkdir(name string, perm os.FileMode) error { return syscall.EPERM }

func (fs *Fs) MkdirAll(path string, perm os.FileMode) error { return syscall.EPERM }

func (fs *Fs) Open(name string) (afero.File, error) {
d, f := splitpath(name)
if f == "" {
return &File{fs: fs, isdir: true}, nil
}
if _, ok := fs.files[d]; !ok {
return nil, &os.PathError{Op: "stat", Path: name, Err: syscall.ENOENT}
}
file, ok := fs.files[d][f]
if !ok {
return nil, &os.PathError{Op: "stat", Path: name, Err: syscall.ENOENT}
}
return &File{fs: fs, zipfile: file, isdir: file.FileInfo().IsDir()}, nil
}

func (fs *Fs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
if flag != os.O_RDONLY {
return nil, syscall.EPERM
}
return fs.Open(name)
}

func (fs *Fs) Remove(name string) error { return syscall.EPERM }

func (fs *Fs) RemoveAll(path string) error { return syscall.EPERM }

func (fs *Fs) Rename(oldname, newname string) error { return syscall.EPERM }

type pseudoRoot struct{}

func (p *pseudoRoot) Name() string { return string(filepath.Separator) }
func (p *pseudoRoot) Size() int64 { return 0 }
func (p *pseudoRoot) Mode() os.FileMode { return os.ModeDir | os.ModePerm }
func (p *pseudoRoot) ModTime() time.Time { return time.Now() }
func (p *pseudoRoot) IsDir() bool { return true }
func (p *pseudoRoot) Sys() interface{} { return nil }

func (fs *Fs) Stat(name string) (os.FileInfo, error) {
d, f := splitpath(name)
if f == "" {
return &pseudoRoot{}, nil
}
if _, ok := fs.files[d]; !ok {
return nil, &os.PathError{Op: "stat", Path: name, Err: syscall.ENOENT}
}
file, ok := fs.files[d][f]
if !ok {
return nil, &os.PathError{Op: "stat", Path: name, Err: syscall.ENOENT}
}
return file.FileInfo(), nil
}

func (fs *Fs) Name() string { return "zipfs" }

func (fs *Fs) Chmod(name string, mode os.FileMode) error { return syscall.EPERM }

func (fs *Fs) Chtimes(name string, atime time.Time, mtime time.Time) error { return syscall.EPERM }
Binary file added zipfs/testdata/t.zip
Binary file not shown.
103 changes: 103 additions & 0 deletions zipfs/zipfs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package zipfs

import (
"github.com/spf13/afero"

"archive/zip"
"path/filepath"
"reflect"
"testing"
)

func TestZipFS(t *testing.T) {
zrc, err := zip.OpenReader("testdata/t.zip")
if err != nil {
t.Fatal(err)
}
zfs := New(&zrc.Reader)
a := &afero.Afero{Fs: zfs}

buf, err := a.ReadFile("testFile")
if err != nil {
t.Error(err)
}
if len(buf) != 8192 {
t.Errorf("short read: %d != 8192", len(buf))
}

buf = make([]byte, 8)
f, err := a.Open("testFile")
if err != nil {
t.Error(err)
}
if n, err := f.ReadAt(buf, 4092); err != nil {
t.Error(err)
} else if n != 8 {
t.Errorf("expected to read 8 bytes, got %d", n)
} else if string(buf) != "aaaabbbb" {
t.Errorf("expected to get <aaaabbbb>, got <%s>", string(buf))
}

d, err := a.Open("/")
if d == nil {
t.Error(`Open("/") returns nil`)
}
if err != nil {
t.Errorf(`Open("/"): err = %v`, err)
}
if s, _ := d.Stat(); !s.IsDir() {
t.Error(`expected root ("/") to be a directory`)
}
if n := d.Name(); n != string(filepath.Separator) {
t.Errorf("Wrong Name() of root directory: Expected: '%c', got '%s'", filepath.Separator, n)
}

buf = make([]byte, 8192)
if n, err := f.Read(buf); err != nil {
t.Error(err)
} else if n != 8192 {
t.Errorf("expected to read 8192 bytes, got %d", n)
} else if buf[4095] != 'a' || buf[4096] != 'b' {
t.Error("got wrong contents")
}

for _, s := range []struct {
path string
dir bool
}{
{"/", true},
{"testDir1", true},
{"testDir1/testFile", false},
{"testFile", false},
{"sub", true},
{"sub/testDir2", true},
{"sub/testDir2/testFile", false},
} {
if dir, _ := a.IsDir(s.path); dir == s.dir {
t.Logf("%s: directory check ok", s.path)
} else {
t.Errorf("%s: directory check NOT ok: %t, expected %t", s.path, dir, s.dir)
}
}

for _, s := range []struct {
glob string
entries []string
}{
{filepath.FromSlash("/*"), []string{filepath.FromSlash("/sub"), filepath.FromSlash("/testDir1"), filepath.FromSlash("/testFile")}},
{filepath.FromSlash("*"), []string{filepath.FromSlash("sub"), filepath.FromSlash("testDir1"), filepath.FromSlash("testFile")}},
{filepath.FromSlash("sub/*"), []string{filepath.FromSlash("sub/testDir2")}},
{filepath.FromSlash("sub/testDir2/*"), []string{filepath.FromSlash("sub/testDir2/testFile")}},
{filepath.FromSlash("testDir1/*"), []string{filepath.FromSlash("testDir1/testFile")}},
} {
entries, err := afero.Glob(zfs, s.glob)
if err != nil {
t.Error(err)
}
if reflect.DeepEqual(entries, s.entries) {
t.Logf("glob: %s: glob ok", s.glob)
} else {
t.Errorf("glob: %s: got %#v, expected %#v", s.glob, entries, s.entries)
}
}
}