-
Notifications
You must be signed in to change notification settings - Fork 514
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add zipfs, an archive/zip-based read-only filesystem
- Loading branch information
Showing
4 changed files
with
356 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 := "/" | ||
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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
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 "/" } | ||
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
} |