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