github.com/hairyhenderson/gomplate/v4@v4.0.0-pre-2.0.20240520121557-362f058f0c93/internal/datafs/envfs.go (about) 1 package datafs 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "io/fs" 8 "net/url" 9 "os" 10 "strings" 11 "time" 12 13 "github.com/hairyhenderson/go-fsimpl" 14 ) 15 16 // NewEnvFS returns a filesystem (an fs.FS) that can be used to read data from 17 // environment variables. 18 func NewEnvFS(_ *url.URL) (fs.FS, error) { 19 return &envFS{locfs: os.DirFS("/")}, nil 20 } 21 22 type envFS struct { 23 locfs fs.FS 24 } 25 26 //nolint:gochecknoglobals 27 var EnvFS = fsimpl.FSProviderFunc(NewEnvFS, "env") 28 29 var _ fs.FS = (*envFS)(nil) 30 31 func (f *envFS) Open(name string) (fs.File, error) { 32 if !fs.ValidPath(name) { 33 return nil, &fs.PathError{ 34 Op: "open", 35 Path: name, 36 Err: fs.ErrInvalid, 37 } 38 } 39 40 return &envFile{locfs: f.locfs, name: name}, nil 41 } 42 43 type envFile struct { 44 locfs fs.FS 45 body io.Reader 46 name string 47 48 dirents []fs.DirEntry 49 diroff int 50 } 51 52 var ( 53 _ fs.File = (*envFile)(nil) 54 _ fs.ReadDirFile = (*envFile)(nil) 55 ) 56 57 // overridable env functions 58 var ( 59 lookupEnv = os.LookupEnv 60 environ = os.Environ 61 ) 62 63 func (e *envFile) Close() error { 64 e.body = nil 65 return nil 66 } 67 68 func (e *envFile) envReader() (int, io.Reader, error) { 69 v, found := lookupEnv(e.name) 70 if found { 71 return len(v), bytes.NewBufferString(v), nil 72 } 73 74 fname, found := lookupEnv(e.name + "_FILE") 75 if found && fname != "" { 76 fname = strings.TrimPrefix(fname, "/") 77 78 b, err := fs.ReadFile(e.locfs, fname) 79 if err != nil { 80 return 0, nil, err 81 } 82 83 b = bytes.TrimSpace(b) 84 85 return len(b), bytes.NewBuffer(b), nil 86 } 87 88 return 0, nil, fs.ErrNotExist 89 } 90 91 func (e *envFile) Stat() (fs.FileInfo, error) { 92 n, _, err := e.envReader() 93 if err != nil { 94 return nil, err 95 } 96 97 return FileInfo(e.name, int64(n), 0o444, time.Time{}, ""), nil 98 } 99 100 func (e *envFile) Read(p []byte) (int, error) { 101 if e.body == nil { 102 _, r, err := e.envReader() 103 if err != nil { 104 return 0, err 105 } 106 e.body = r 107 } 108 109 return e.body.Read(p) 110 } 111 112 func (e *envFile) ReadDir(n int) ([]fs.DirEntry, error) { 113 // envFS has no concept of subdirectories, but we can support a root 114 // directory by listing all environment variables. 115 if e.name != "." { 116 return nil, fmt.Errorf("%s: not a directory", e.name) 117 } 118 119 if e.dirents == nil { 120 envs := environ() 121 e.dirents = make([]fs.DirEntry, 0, len(envs)) 122 for _, env := range envs { 123 parts := strings.SplitN(env, "=", 2) 124 name, value := parts[0], parts[1] 125 126 if name == "" { 127 // this might be a Windows =C: style env var, so skip it 128 continue 129 } 130 131 e.dirents = append(e.dirents, FileInfoDirEntry( 132 FileInfo(name, int64(len(value)), 0o444, time.Time{}, ""), 133 )) 134 } 135 } 136 137 if n > 0 && e.diroff >= len(e.dirents) { 138 return nil, io.EOF 139 } 140 141 low := e.diroff 142 high := e.diroff + n 143 144 // clamp high at the max, and ensure it's higher than low 145 if high >= len(e.dirents) || high <= low { 146 high = len(e.dirents) 147 } 148 149 entries := make([]fs.DirEntry, high-low) 150 copy(entries, e.dirents[e.diroff:]) 151 152 e.diroff = high 153 154 return entries, nil 155 } 156 157 // FileInfo/DirInfo/FileInfoDirEntry/etc are taken from go-fsimpl's internal 158 // package, and may be exported in the future... 159 160 // FileInfo creates a static fs.FileInfo with the given properties. 161 // The result is also a fs.DirEntry and can be safely cast. 162 func FileInfo(name string, size int64, mode fs.FileMode, modTime time.Time, contentType string) fs.FileInfo { 163 return &staticFileInfo{ 164 name: name, 165 size: size, 166 mode: mode, 167 modTime: modTime, 168 contentType: contentType, 169 } 170 } 171 172 // DirInfo creates a fs.FileInfo for a directory with the given name. Use 173 // FileInfo to set other values. 174 func DirInfo(name string, modTime time.Time) fs.FileInfo { 175 return FileInfo(name, 0, fs.ModeDir, modTime, "") 176 } 177 178 type staticFileInfo struct { 179 modTime time.Time 180 name string 181 contentType string 182 size int64 183 mode fs.FileMode 184 } 185 186 var ( 187 _ fs.FileInfo = (*staticFileInfo)(nil) 188 _ fs.DirEntry = (*staticFileInfo)(nil) 189 ) 190 191 func (fi staticFileInfo) ContentType() string { return fi.contentType } 192 func (fi staticFileInfo) IsDir() bool { return fi.Mode().IsDir() } 193 func (fi staticFileInfo) Mode() fs.FileMode { return fi.mode } 194 func (fi *staticFileInfo) ModTime() time.Time { return fi.modTime } 195 func (fi staticFileInfo) Name() string { return fi.name } 196 func (fi staticFileInfo) Size() int64 { return fi.size } 197 func (fi staticFileInfo) Sys() interface{} { return nil } 198 func (fi *staticFileInfo) Info() (fs.FileInfo, error) { return fi, nil } 199 func (fi staticFileInfo) Type() fs.FileMode { return fi.Mode().Type() } 200 201 // FileInfoDirEntry adapts a fs.FileInfo into a fs.DirEntry. If it doesn't 202 // already implement fs.DirEntry, it will be wrapped to always return the 203 // same fs.FileInfo. 204 func FileInfoDirEntry(fi fs.FileInfo) fs.DirEntry { 205 de, ok := fi.(fs.DirEntry) 206 if ok { 207 return de 208 } 209 210 return &fileinfoDirEntry{fi} 211 } 212 213 // a wrapper to make a fs.FileInfo into an fs.DirEntry 214 type fileinfoDirEntry struct { 215 fs.FileInfo 216 } 217 218 var _ fs.DirEntry = (*fileinfoDirEntry)(nil) 219 220 func (fi *fileinfoDirEntry) Info() (fs.FileInfo, error) { return fi, nil } 221 func (fi *fileinfoDirEntry) Type() fs.FileMode { return fi.Mode().Type() }