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 }