github.com/posener/gitfs@v1.2.2-0.20200410105819-ea4e48d73ab9/gitfs.go (about)

     1  // Package gitfs is a complete solution for static files in Go code.
     2  //
     3  // When Go code uses non-Go files, they are not packaged into the binary.
     4  // The common approach to the problem, as implemented by
     5  // (go-bindata) https://github.com/kevinburke/go-bindata
     6  // is to convert all the required static files into Go code, which
     7  // eventually compiled into the binary.
     8  //
     9  // This library takes a different approach, in which the static files are not
    10  // required to be "binary-packed", and even no required to be in the same repository
    11  // as the Go code. This package enables loading static content from a remote
    12  // git repository, or packing it to the binary if desired or loaded
    13  // from local path for development process. The transition from remote repository
    14  // to binary packed content, to local content is completely smooth.
    15  //
    16  // *The API is simple and minimalistic*. The `New` method returns a (sub)tree
    17  // of a Git repository, represented by the standard `http.FileSystem` interface.
    18  // This object enables anything that is possible to do with a regular filesystem,
    19  // such as opening a file or listing a directory.
    20  // Additionally, the ./fsutil package provides enhancements over the `http.FileSystem`
    21  // object (They can work with any object that implements the interface) such
    22  // as loading Go templates in the standard way, walking over the filesystem,
    23  // and applying glob patterns on a filesystem.
    24  //
    25  // Supported features:
    26  //
    27  // * Loading of specific version/tag/branch.
    28  //
    29  // * For debug purposes, the files can be loaded from local path instead of the
    30  // remote repository.
    31  //
    32  // * Files are loaded lazily by default or they can be preloaded if required.
    33  //
    34  // * Files can be packed to the Go binary using a command line tool.
    35  //
    36  // * This project is using the standard `http.FileSystem` interface.
    37  //
    38  // * In ./fsutil there are some general useful tools around the
    39  // `http.FileSystem` interace.
    40  //
    41  // Usage
    42  //
    43  // To create a filesystem using the `New` function, provide the Git
    44  // project with the pattern: `github.com/<owner>/<repo>(/<path>)?(@<ref>)?`.
    45  // If no `path` is specified, the root of the project will be used.
    46  // `ref` can be any git branch using `heads/<branch name>` or any
    47  // git tag using `tags/<tag>`. If the tag is of Semver format, the `tags/`
    48  // prefix is not required. If no `ref` is specified, the default branch will
    49  // be used.
    50  //
    51  // In the following example, the repository `github.com/x/y` at tag v1.2.3
    52  // and internal path "static" is loaded:
    53  //
    54  // 	fs, err := gitfs.New(ctx, "github.com/x/y/static@v1.2.3")
    55  //
    56  // The variable `fs` implements the `http.FileSystem` interface.
    57  // Reading a file from the repository can be done using the `Open` method.
    58  // This function accepts a path, relative to the root of the defined
    59  // filesystem.
    60  //
    61  // 	f, err := fs.Open("index.html")
    62  //
    63  // The `fs` variable can be used in anything that accept the standard interface.
    64  // For example, it can be used for serving static content using the standard
    65  // library:
    66  //
    67  // 	http.Handle("/", http.FileServer(fs))
    68  //
    69  // Private Repositories
    70  //
    71  // When used with private github repository, the Github API calls should be
    72  // instrumented with the appropriate credentials. The credentials can be
    73  // passed by providing an HTTP client.
    74  //
    75  // For example, to use a Github Token from environnement variable `GITHUB_TOKEN`:
    76  //
    77  // 	token := os.Getenv("GITHUB_TOKEN")
    78  // 	client := oauth2.NewClient(
    79  // 		context.Background(),
    80  // 		oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}))
    81  // 	fs, err := gitfs.New(ctx, "github.com/x/y", gitfs.OptClient(client))
    82  //
    83  // Development
    84  //
    85  // For quick development workflows, it is easier and faster to use local static
    86  // content and not remote content that was pushed to a remote repository.
    87  // This is enabled by the `OptLocal` option. To use this option only in
    88  // local development and not in production system, it can be used as follow:
    89  //
    90  // 	local := os.Getenv("LOCAL_DEBUG")
    91  // 	fs, err := gitfs.New(ctx, "github.com/x/y", gitfs.OptLocal(local))
    92  //
    93  // In this example, we stored the value for `OptLocal` in an environment
    94  // variable. As a result, when running the program with `LOCAL_DEBUG=.`
    95  // local files will be used, while running without it will result in using
    96  // the remote files. (the value of the environment variable should point
    97  // to any directory within the github project).
    98  //
    99  // Binary Packing
   100  //
   101  // Using gitfs does not mean that files are required to be remotely fetched.
   102  // When binary packing of the files is needed, a command line tool can pack
   103  // them for you.
   104  //
   105  // To get the tool run: `go get github.com/posener/gitfs/cmd/gitfs`.
   106  //
   107  // Running the tool is by `gitfs <patterns>`. This generates a `gitfs.go`
   108  // file in the current directory that contains all the used filesystems' data.
   109  // This will cause all `gitfs.New` calls to automatically use the packed data,
   110  // insted of fetching the data on runtime.
   111  //
   112  // By default, a test will also be generated with the code. This test fails
   113  // when the local files are modified without updating the binary content.
   114  //
   115  // Use binary-packing with `go generate`: To generate all filesystems used
   116  // by a project add `//go:generate gitfs ./...` in the root of the project.
   117  // To generate only a specific filesystem add `//go:generate gitfs $GOFILE` in
   118  // the file it is being used.
   119  //
   120  // An interesting anecdote is that gitfs command is using itself for generating
   121  // its own templates.
   122  //
   123  // Excluding files
   124  //
   125  // Files exclusion can be done by including only specific files using a glob
   126  // pattern with `OptGlob` option, using the Glob options. This will affect
   127  // both local loading of files, remote loading and binary packing (may
   128  // reduce binary size). For example:
   129  //
   130  // 	fs, err := gitfs.New(ctx,
   131  // 		"github.com/x/y/templates",
   132  // 		gitfs.OptGlob("*.gotmpl", "*/*.gotmpl"))
   133  package gitfs
   134  
   135  import (
   136  	"context"
   137  	"net/http"
   138  
   139  	"github.com/pkg/errors"
   140  	"github.com/posener/gitfs/fsutil"
   141  	"github.com/posener/gitfs/internal/binfs"
   142  	"github.com/posener/gitfs/internal/githubfs"
   143  	"github.com/posener/gitfs/internal/localfs"
   144  	"github.com/posener/gitfs/internal/log"
   145  )
   146  
   147  // OptClient sets up an HTTP client to perform request to the remote repository.
   148  // This client can be used for authorization credentials.
   149  func OptClient(client *http.Client) option {
   150  	return func(c *config) {
   151  		c.client = client
   152  	}
   153  }
   154  
   155  // OptLocal result in looking for local git repository before accessing remote
   156  // repository. The given path should be contained in a git repository which
   157  // has a remote URL that matches the requested project.
   158  func OptLocal(path string) option {
   159  	return func(c *config) {
   160  		c.localPath = path
   161  	}
   162  }
   163  
   164  // OptPrefetch sets prefetching all files in the filesystem when it is initially
   165  // loaded.
   166  func OptPrefetch(prefetch bool) option {
   167  	return func(c *config) {
   168  		c.prefetch = prefetch
   169  	}
   170  }
   171  
   172  // OptGlob define glob patterns for which only matching files and directories
   173  // will be included in the filesystem.
   174  func OptGlob(patterns ...string) option {
   175  	return func(c *config) {
   176  		c.patterns = patterns
   177  	}
   178  }
   179  
   180  // New returns a new git filesystem for the given project.
   181  //
   182  // Github:
   183  // If the given project is a github project (of the form github.com/<owner>/<repo>(@<ref>)?(#<path>)? ),
   184  // the returned filesystem will be fetching files from the given project.
   185  // ref is optional and can be any github ref:
   186  //  * `heads/<branch name>` for a branch.
   187  //  * `tags/<tag>` for releases or git tags.
   188  //  * `<version>` for Semver compatible releases (e.g. v1.2.3).
   189  // If no ref is set, the default branch will be used.
   190  func New(ctx context.Context, project string, opts ...option) (http.FileSystem, error) {
   191  	var c config
   192  	for _, opt := range opts {
   193  		opt(&c)
   194  	}
   195  
   196  	switch {
   197  	case c.localPath != "":
   198  		log.Printf("FileSystem %q from local directory", project)
   199  		fs, err := localfs.New(project, c.localPath)
   200  		if err != nil {
   201  			return nil, err
   202  		}
   203  		return fsutil.Glob(fs, c.patterns...)
   204  	case binfs.Match(project):
   205  		log.Printf("FileSystem %q from binary", project)
   206  		return binfs.Get(project), nil
   207  	case githubfs.Match(project):
   208  		log.Printf("FileSystem %q from remote Github repository", project)
   209  		return githubfs.New(ctx, c.client, project, c.prefetch, c.patterns)
   210  	default:
   211  		return nil, errors.Errorf("project %q not supported", project)
   212  	}
   213  }
   214  
   215  // WithContext applies context to an http.File if it implements the
   216  // contexter interface.
   217  //
   218  // Usage example:
   219  //
   220  // 	f, err := fs.Open("file")
   221  // 	// Handle err...
   222  // 	f = gitfs.WithContext(f, ctx)
   223  // 	_, err = f.Read(...)
   224  func WithContext(f http.File, ctx context.Context) http.File {
   225  	fCtx, ok := f.(contexter)
   226  	if !ok {
   227  		return f
   228  	}
   229  	return fCtx.WithContext(ctx)
   230  }
   231  
   232  // SetLogger sets informative logging for gitfs. If nil, no logging
   233  // will be done.
   234  func SetLogger(logger log.Logger) {
   235  	log.Log = logger
   236  }
   237  
   238  type config struct {
   239  	client    *http.Client
   240  	localPath string
   241  	prefetch  bool
   242  	patterns  []string
   243  }
   244  
   245  type option func(*config)
   246  
   247  type contexter interface {
   248  	WithContext(ctx context.Context) http.File
   249  }