├── LICENSE ├── README.md ├── tarfs.go └── tarfs_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 omeid 4 | 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/omeid/go-tarfs?status.svg)](https://godoc.org/github.com/omeid/go-tarfs) [![Build Status](https://drone.io/github.com/omeid/go-tarfs/status.png)](https://drone.io/github.com/omeid/go-tarfs/latest) 2 | # tarfs 3 | In-memory http.FileSystem from tar archives. 4 | 5 | ### Why? 6 | If you have multiple assets for your program that you don't want to [embed](https://github.com/omeid/go-resources) them in your binary but still want an easier way to ship them along your binary, tarfs is your friend. 7 | 8 | ### Usage 9 | See the [GoDoc](https://godoc.org/github.com/omeid/go-tarfs) 10 | 11 | 12 | ### Contributing 13 | Please consider opening an issue first, or just send a pull request. :) 14 | 15 | ### Credits 16 | See [Contributors](https://github.com/omeid/go-tarfs/graphs/contributors). 17 | 18 | ### LICENSE 19 | [MIT](LICENSE). 20 | 21 | ### TODO 22 | - Add more tests 23 | 24 | ### SEE ALSO 25 | - [github.com/omeid/go-resources](http://godoc.org/github.com/omeid/go-resources) 26 | - [x/tools/godoc/vfs/zipfs](http://godoc.org/golang.org/x/tools/godoc/vfs/zipfs) 27 | - [github.com/tsuru/tusru/fs](http://godoc.org/github.com/tsuru/tsuru/fs) 28 | -------------------------------------------------------------------------------- /tarfs.go: -------------------------------------------------------------------------------- 1 | // In memory http.FileSystem from tar archives 2 | package tarfs 3 | 4 | import ( 5 | "archive/tar" 6 | "bytes" 7 | "errors" 8 | "io" 9 | "io/ioutil" 10 | "net/http" 11 | "os" 12 | "path" 13 | "path/filepath" 14 | "strings" 15 | ) 16 | 17 | // New returns an http.FileSystem that holds all the files in the tar, 18 | // It reads the whole archive from the Reader. It is the caller's responsibility to call Close on the Reader when done. 19 | func New(tarstream io.Reader) (http.FileSystem, error) { 20 | tr := tar.NewReader(tarstream) 21 | 22 | tarfs := tarfs{make(map[string]file)} 23 | // Iterate through the files in the archive. 24 | for { 25 | hdr, err := tr.Next() 26 | if err == io.EOF { 27 | // end of tar archive 28 | break 29 | } 30 | if err != nil { 31 | return nil, err 32 | } 33 | data, err := ioutil.ReadAll(tr) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | tarfs.files[path.Join("/", hdr.Name)] = file{ 39 | data: data, 40 | fi: hdr.FileInfo(), 41 | } 42 | } 43 | return &tarfs, nil 44 | } 45 | 46 | type file struct { 47 | *bytes.Reader 48 | data []byte 49 | fi os.FileInfo 50 | 51 | files []os.FileInfo 52 | } 53 | 54 | type tarfs struct { 55 | files map[string]file 56 | } 57 | 58 | func (tf *tarfs) Open(name string) (http.File, error) { 59 | if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 || 60 | strings.Contains(name, "\x00") { 61 | return nil, errors.New("http: invalid character in file path") 62 | } 63 | f, ok := tf.files[path.Join("/", name)] 64 | if !ok { 65 | return nil, os.ErrNotExist 66 | } 67 | if f.fi.IsDir() { 68 | f.files = []os.FileInfo{} 69 | for path, file := range tf.files { 70 | if strings.HasPrefix(path, name) { 71 | s, _ := file.Stat() 72 | f.files = append(f.files, s) 73 | } 74 | } 75 | 76 | } 77 | f.Reader = bytes.NewReader(f.data) 78 | return &f, nil 79 | } 80 | 81 | // A noop-closer. 82 | func (f *file) Close() error { 83 | return nil 84 | } 85 | 86 | func (f *file) Readdir(count int) ([]os.FileInfo, error) { 87 | if f.fi.IsDir() && f.files != nil { 88 | return f.files, nil 89 | } 90 | return nil, os.ErrNotExist 91 | } 92 | 93 | func (f *file) Stat() (os.FileInfo, error) { 94 | return f.fi, nil 95 | } 96 | -------------------------------------------------------------------------------- /tarfs_test.go: -------------------------------------------------------------------------------- 1 | package tarfs 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "io/ioutil" 7 | "log" 8 | "testing" 9 | ) 10 | 11 | func TestOpen(t *testing.T) { 12 | 13 | // Create a buffer to write our archive to. 14 | buf := new(bytes.Buffer) 15 | 16 | // Create a new tar archive. 17 | tw := tar.NewWriter(buf) 18 | 19 | // Add some files to the archive. 20 | var files = []struct { 21 | Name string 22 | Body string 23 | AccessNames []string 24 | }{ 25 | { 26 | Name: "readme.txt", 27 | Body: "This archive contains some text files.", 28 | AccessNames: []string{ 29 | "readme.txt", 30 | "/readme.txt", 31 | "./readme.txt", 32 | "././readme.txt", 33 | "../readme.txt", 34 | }, 35 | }, 36 | { 37 | Name: "/gopher.txt", 38 | Body: "Gopher names:\nGeorge\nGeoffrey\nGonzo", 39 | AccessNames: []string{ 40 | "gopher.txt", 41 | "/gopher.txt", 42 | "./gopher.txt", 43 | "././gopher.txt", 44 | "../gopher.txt", 45 | }, 46 | }, 47 | { 48 | Name: "./todo.txt", 49 | Body: "Get animal handling licence.", 50 | AccessNames: []string{ 51 | "todo.txt", 52 | "/todo.txt", 53 | "./todo.txt", 54 | "././todo.txt", 55 | "../todo.txt", 56 | }, 57 | }, 58 | } 59 | for _, file := range files { 60 | hdr := &tar.Header{ 61 | Name: file.Name, 62 | Size: int64(len(file.Body)), 63 | } 64 | if err := tw.WriteHeader(hdr); err != nil { 65 | log.Fatalln(err) 66 | } 67 | if _, err := tw.Write([]byte(file.Body)); err != nil { 68 | log.Fatalln(err) 69 | } 70 | } 71 | // Make sure to check the error on Close. 72 | if err := tw.Close(); err != nil { 73 | log.Fatalln(err) 74 | } 75 | 76 | // Open the tar archive for reading. 77 | fs, err := New(bytes.NewReader(buf.Bytes())) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | for _, file := range files { 83 | for _, path := range file.AccessNames { 84 | f, err := fs.Open(path) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | content, _ := ioutil.ReadAll(f) 89 | if string(content) != file.Body { 90 | t.Fatalf("For '%s'\nExpected:\n%s\nGot:\n%s\n", file.Name, file.Body, content) 91 | } 92 | 93 | var ( 94 | s, _ = f.Stat() 95 | size = int64(len(file.Body)) 96 | got = s.Size() 97 | ) 98 | 99 | if size != got { 100 | t.Fatalf("For '%s'\nExpected Size:\n%v\nGot:\n%v\n", file.Name, size, got) 101 | } 102 | } 103 | } 104 | } 105 | --------------------------------------------------------------------------------