mirror of https://github.com/spf13/afero.git
Add zipfs, an archive/zip-based read-only filesystem
This commit is contained in:
parent
f1155579b0
commit
4f00b06400
|
@ -380,7 +380,6 @@ The following is a short list of possible backends we hope someone will
|
||||||
implement:
|
implement:
|
||||||
|
|
||||||
* SSH
|
* SSH
|
||||||
* ZIP
|
|
||||||
* TAR
|
* TAR
|
||||||
* S3
|
* S3
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
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))
|
||||||
|
n, _ := io.ReadFull(f.reader, buf)
|
||||||
|
if n > 0 {
|
||||||
|
f.buf = append(f.buf, buf[:n]...)
|
||||||
|
}
|
||||||
|
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 { return f.zipfile.Name }
|
||||||
|
|
||||||
|
func (f *File) getDirEntries() (map[string]*zip.File, error) {
|
||||||
|
if !f.isdir {
|
||||||
|
return nil, syscall.ENOTDIR
|
||||||
|
}
|
||||||
|
name := string(filepath.Separator)
|
||||||
|
if f.zipfile != nil {
|
||||||
|
name = filepath.Join(splitpath(f.zipfile.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) { 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 }
|
|
@ -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 }
|
Binary file not shown.
|
@ -0,0 +1,88 @@
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}{
|
||||||
|
{"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue