github.com/hairyhenderson/gomplate/v4@v4.0.0-pre-2.0.20240520121557-362f058f0c93/internal/datafs/fsys.go (about)

     1  package datafs
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/fs"
     7  	"net/url"
     8  	"strings"
     9  
    10  	"github.com/hairyhenderson/go-fsimpl"
    11  	"github.com/hairyhenderson/go-fsimpl/vaultfs/vaultauth"
    12  	"github.com/hairyhenderson/gomplate/v4/internal/urlhelpers"
    13  )
    14  
    15  type fsProviderCtxKey struct{}
    16  
    17  // ContextWithFSProvider returns a context with the given FSProvider. Should
    18  // only be used in tests.
    19  func ContextWithFSProvider(ctx context.Context, fsp fsimpl.FSProvider) context.Context {
    20  	return context.WithValue(ctx, fsProviderCtxKey{}, fsp)
    21  }
    22  
    23  // FSProviderFromContext returns the FSProvider from the context, if any
    24  func FSProviderFromContext(ctx context.Context) fsimpl.FSProvider {
    25  	if fsp, ok := ctx.Value(fsProviderCtxKey{}).(fsimpl.FSProvider); ok {
    26  		return fsp
    27  	}
    28  
    29  	return nil
    30  }
    31  
    32  // FSysForPath returns an [io/fs.FS] for the given path (which may be an URL),
    33  // rooted at /. A [fsimpl.FSProvider] is required to be present in ctx,
    34  // otherwise an error is returned.
    35  func FSysForPath(ctx context.Context, path string) (fs.FS, error) {
    36  	u, err := urlhelpers.ParseSourceURL(path)
    37  	if err != nil {
    38  		return nil, err
    39  	}
    40  
    41  	fsp := FSProviderFromContext(ctx)
    42  	if fsp == nil {
    43  		return nil, fmt.Errorf("no filesystem provider in context")
    44  	}
    45  
    46  	origPath := u.Path
    47  
    48  	switch u.Scheme {
    49  	case "git+file", "git+http", "git+https", "git+ssh", "git":
    50  		// git URLs are special - they have double-slashes that separate a repo from
    51  		// a path in the repo. A missing double-slash means the path is the root.
    52  		u.Path, _, _ = strings.Cut(u.Path, "//")
    53  	}
    54  
    55  	switch u.Scheme {
    56  	case "git+http", "git+https", "git+ssh", "git":
    57  		// no-op, these are handled
    58  	case "", "file", "git+file":
    59  		// default to "/" so we have a rooted filesystem for all schemes, but also
    60  		// support volumes on Windows
    61  		root, name, rerr := ResolveLocalPath(nil, u.Path)
    62  		if rerr != nil {
    63  			return nil, fmt.Errorf("resolve local path %q: %w", origPath, rerr)
    64  		}
    65  
    66  		// windows absolute paths need a slash between the volume and path
    67  		if root != "" && root[0] != '/' {
    68  			u.Path = root + "/" + name
    69  		} else {
    70  			u.Path = root + name
    71  		}
    72  
    73  		// if this is a drive letter, add a trailing slash
    74  		if len(u.Path) == 2 && u.Path[0] != '/' && u.Path[1] == ':' {
    75  			u.Path += "/"
    76  		} else if u.Path[0] != '/' {
    77  			u.Path += "/"
    78  		}
    79  
    80  		// if this starts with a drive letter, add a leading slash
    81  		// NOPE - this breaks lots of things
    82  		// if len(u.Path) > 2 && u.Path[0] != '/' && u.Path[1] == ':' {
    83  		// 	u.Path = "/" + u.Path
    84  		// }
    85  	default:
    86  		u.Path = "/"
    87  	}
    88  
    89  	fsys, err := fsp.New(u)
    90  	if err != nil {
    91  		return nil, fmt.Errorf("filesystem provider for %q unavailable: %w", path, err)
    92  	}
    93  
    94  	// inject vault auth methods if needed
    95  	switch u.Scheme {
    96  	case "vault", "vault+http", "vault+https":
    97  		fileFsys, err := fsp.New(&url.URL{Scheme: "file", Path: "/"})
    98  		if err != nil {
    99  			return nil, fmt.Errorf("filesystem provider for %q unavailable: %w", path, err)
   100  		}
   101  		fsys = vaultauth.WithAuthMethod(compositeVaultAuthMethod(fileFsys), fsys)
   102  	}
   103  
   104  	return fsys, nil
   105  }
   106  
   107  type fsp struct {
   108  	newFunc func(*url.URL) (fs.FS, error)
   109  	schemes []string
   110  }
   111  
   112  func (p fsp) Schemes() []string {
   113  	return p.schemes
   114  }
   115  
   116  func (p fsp) New(u *url.URL) (fs.FS, error) {
   117  	return p.newFunc(u)
   118  }
   119  
   120  // WrappedFSProvider is an FSProvider that returns the given fs.FS
   121  func WrappedFSProvider(fsys fs.FS, schemes ...string) fsimpl.FSProvider {
   122  	return fsp{
   123  		newFunc: func(_ *url.URL) (fs.FS, error) { return fsys, nil },
   124  		schemes: schemes,
   125  	}
   126  }