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  }