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() }