/
binfs.go
179 lines (164 loc) · 4.56 KB
/
binfs.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// Package binfs is filesystem over registered binary data.
//
// This pacakge is used by ./cmd/gitfs to generate files that
// contain static content of a filesystem.
package binfs
import (
"bytes"
"compress/gzip"
"encoding/base64"
"encoding/gob"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"github.com/pkg/errors"
"github.com/posener/gitfs/fsutil"
"github.com/posener/gitfs/internal/tree"
)
// EncodeVersion is the current encoding version.
const EncodeVersion = 1
// data maps registered projects (through `Register()` call)
// to the corresponding filesystem that they represent.
var data map[string]http.FileSystem
// fsStorage stores all filesystem structure and all file contents.
type fsStorage struct {
// Files maps all file paths from root of the filesystem to
// their contents.
Files map[string][]byte
// Dirs is the set of paths of directories in the filesystem.
Dirs map[string]bool
}
func init() {
data = make(map[string]http.FileSystem)
gob.Register(fsStorage{})
}
// Register a filesystem under the project name.
// It panics if anything goes wrong.
func Register(project string, version int, encoded string) {
if data[project] != nil {
panic(fmt.Sprintf("Project %s registered multiple times", project))
}
var (
fs http.FileSystem
err error
)
switch version {
case 1:
fs, err = decodeV1(encoded)
default:
panic(fmt.Sprintf(`Registered filesystem is from future version %d.
The current gitfs suports versions up to %d.
Please update github.com/posener/gitfs.`, version, EncodeVersion))
}
if err != nil {
panic(fmt.Sprintf("Failed decoding project %q: %s", project, err))
}
data[project] = fs
}
// Match returns wether project exists in registered binaries.
// The matching is done also over the project `ref`.
func Match(project string) bool {
_, ok := data[project]
return ok
}
// Get returns filesystem of a registered project.
func Get(project string) http.FileSystem {
return data[project]
}
// encode converts a filesystem to an encoded string. All filesystem structure
// and file content is stored.
//
// Note: modifying this function should probably increase EncodeVersion const,
// and should probably add a new `decode` function for the new version.
func encode(fs http.FileSystem) (string, error) {
// storage is an object that contains all filesystem information.
storage := newFSStorage()
// Walk the provided filesystem, and add all its content to storage.
walker := fsutil.Walk(fs, "")
for walker.Step() {
path := walker.Path()
if path == "" {
continue
}
if walker.Stat().IsDir() {
storage.Dirs[path] = true
} else {
b, err := readFile(fs, path)
if err != nil {
return "", err
}
storage.Files[path] = b
}
log.Printf("Encoded path: %s", path)
}
if err := walker.Err(); err != nil {
return "", errors.Wrap(err, "walking filesystem")
}
// Encode the storage object into a string.
// storage object -> GOB -> gzip -> base64.
var buf bytes.Buffer
w := gzip.NewWriter(&buf)
err := gob.NewEncoder(w).Encode(storage)
if err != nil {
return "", errors.Wrap(err, "encoding gob")
}
err = w.Close()
if err != nil {
return "", errors.Wrap(err, "close gzip")
}
s := base64.StdEncoding.EncodeToString(buf.Bytes())
log.Printf("Encoded size: %d", len(s))
return s, err
}
// decodeV1 returns a filesystem from data that was encoded in V1.
func decodeV1(data string) (tree.Tree, error) {
var storage fsStorage
b, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return nil, errors.Wrap(err, "decoding base64")
}
var r io.ReadCloser
r, err = gzip.NewReader(bytes.NewReader(b))
if err != nil {
// Fallback to non-zipped version.
log.Printf(
"Decoding gzip: %s. Falling back to non-gzip loading.",
err)
r = ioutil.NopCloser(bytes.NewReader(b))
}
defer r.Close()
err = gob.NewDecoder(r).Decode(&storage)
if err != nil {
return nil, errors.Wrap(err, "decoding gob")
}
t := make(tree.Tree)
for dir := range storage.Dirs {
t.AddDir(dir)
}
for path, content := range storage.Files {
t.AddFileContent(path, content)
}
return t, err
}
// readFile is a utility function that reads content of the file
// denoted by path from the provided filesystem.
func readFile(fs http.FileSystem, path string) ([]byte, error) {
f, err := fs.Open(path)
if err != nil {
return nil, errors.Wrapf(err, "opening file %s", path)
}
defer f.Close()
b, err := ioutil.ReadAll(f)
if err != nil {
return nil, errors.Wrapf(err, "reading file content %s", path)
}
return b, nil
}
func newFSStorage() fsStorage {
return fsStorage{
Files: make(map[string][]byte),
Dirs: make(map[string]bool),
}
}