diff --git a/README.md b/README.md index 80e9927..f1793fb 100644 --- a/README.md +++ b/README.md @@ -380,7 +380,6 @@ The following is a short list of possible backends we hope someone will implement: * SSH -* ZIP * TAR * S3 diff --git a/zipfs/file.go b/zipfs/file.go new file mode 100644 index 0000000..94f9ddc --- /dev/null +++ b/zipfs/file.go @@ -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) + 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 } diff --git a/zipfs/fs.go b/zipfs/fs.go new file mode 100644 index 0000000..72887f9 --- /dev/null +++ b/zipfs/fs.go @@ -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) { + 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 } diff --git a/zipfs/testdata/t.zip b/zipfs/testdata/t.zip new file mode 100644 index 0000000..99e39a7 Binary files /dev/null and b/zipfs/testdata/t.zip differ diff --git a/zipfs/zipfs_test.go b/zipfs/zipfs_test.go new file mode 100644 index 0000000..bf781cc --- /dev/null +++ b/zipfs/zipfs_test.go @@ -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 , 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) + } + } +}